Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@
<dependency>
<groupId>io.ipinfo</groupId>
<artifactId>ipinfo-api</artifactId>
<version>3.1.0</version>
<version>3.2.0</version>
<scope>compile</scope>
</dependency>
<dependency>
Expand Down
111 changes: 111 additions & 0 deletions src/main/java/io/ipinfo/spring/IPinfoCoreSpring.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
package io.ipinfo.spring;

import io.ipinfo.api.IPinfoCore;
import io.ipinfo.api.model.IPResponseCore;
import io.ipinfo.spring.strategies.attribute.AttributeStrategy;
import io.ipinfo.spring.strategies.attribute.SessionAttributeStrategy;
import io.ipinfo.spring.strategies.interceptor.BotInterceptorStrategy;
import io.ipinfo.spring.strategies.interceptor.InterceptorStrategy;
import io.ipinfo.spring.strategies.ip.IPStrategy;
import io.ipinfo.spring.strategies.ip.SimpleIPStrategy;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.web.servlet.HandlerInterceptor;

public class IPinfoCoreSpring implements HandlerInterceptor {

public static final String ATTRIBUTE_KEY =
"IPinfoOfficialSparkWrapper.IPResponseCore";
private final IPinfoCore ii;
private final AttributeStrategy attributeStrategy;
private final IPStrategy ipStrategy;
private final InterceptorStrategy interceptorStrategy;

IPinfoCoreSpring(
IPinfoCore ii,
AttributeStrategy attributeStrategy,
IPStrategy ipStrategy,
InterceptorStrategy interceptorStrategy
) {
this.ii = ii;
this.attributeStrategy = attributeStrategy;
this.ipStrategy = ipStrategy;
this.interceptorStrategy = interceptorStrategy;
}

public static void main(String... args) {
System.out.println(
"This library is not meant to be run as a standalone jar."
);
System.exit(0);
}

@Override
public boolean preHandle(
HttpServletRequest request,
HttpServletResponse response,
Object handler
) throws Exception {
if (!interceptorStrategy.shouldRun(request)) {
return true;
}

// Don't waste an API call if we already have it.
// This should only happen for RequestAttributeStrategy and potentially
// other implementations.
if (attributeStrategy.hasCoreAttribute(request)) {
return true;
}

String ip = ipStrategy.getIPAddress(request);
if (ip == null) {
return true;
}

IPResponseCore ipResponse = ii.lookupIP(ip);
attributeStrategy.storeCoreAttribute(request, ipResponse);

return true;
}

public static class Builder {

private IPinfoCore ii = new IPinfoCore.Builder().build();
private AttributeStrategy attributeStrategy =
new SessionAttributeStrategy();
private IPStrategy ipStrategy = new SimpleIPStrategy();
private InterceptorStrategy interceptorStrategy =
new BotInterceptorStrategy();

public Builder setIPinfo(IPinfoCore ii) {
this.ii = ii;
return this;
}

public Builder attributeStrategy(AttributeStrategy attributeStrategy) {
this.attributeStrategy = attributeStrategy;
return this;
}

public Builder ipStrategy(IPStrategy ipStrategy) {
this.ipStrategy = ipStrategy;
return this;
}

public Builder interceptorStrategy(
InterceptorStrategy interceptorStrategy
) {
this.interceptorStrategy = interceptorStrategy;
return this;
}

public IPinfoCoreSpring build() {
return new IPinfoCoreSpring(
ii,
attributeStrategy,
ipStrategy,
interceptorStrategy
);
}
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package io.ipinfo.spring.strategies.attribute;

import io.ipinfo.api.model.IPResponse;
import io.ipinfo.api.model.IPResponseCore;
import io.ipinfo.api.model.IPResponseLite;
import jakarta.servlet.http.HttpServletRequest;

Expand Down Expand Up @@ -35,4 +36,23 @@ default IPResponseLite getLiteAttribute(HttpServletRequest request) {
default boolean hasLiteAttribute(HttpServletRequest request) {
return getLiteAttribute(request) != null;
}

default void storeCoreAttribute(
HttpServletRequest request,
IPResponseCore response
) {
throw new UnsupportedOperationException(
"This strategy does not support IPResponseCore."
);
}

default IPResponseCore getCoreAttribute(HttpServletRequest request) {
throw new UnsupportedOperationException(
"This strategy does not support IPResponseCore."
);
}

default boolean hasCoreAttribute(HttpServletRequest request) {
return getCoreAttribute(request) != null;
}
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
package io.ipinfo.spring.strategies.attribute;

import io.ipinfo.api.model.IPResponse;
import io.ipinfo.api.model.IPResponseCore;
import io.ipinfo.api.model.IPResponseLite;
import io.ipinfo.spring.IPinfoCoreSpring;
import io.ipinfo.spring.IPinfoLiteSpring;
import io.ipinfo.spring.IPinfoSpring;
import jakarta.servlet.http.HttpServletRequest;
Expand Down Expand Up @@ -35,4 +37,19 @@ public IPResponseLite getLiteAttribute(HttpServletRequest request) {
IPinfoLiteSpring.ATTRIBUTE_KEY
);
}

@Override
public void storeCoreAttribute(
HttpServletRequest request,
IPResponseCore response
) {
request.setAttribute(IPinfoCoreSpring.ATTRIBUTE_KEY, response);
}

@Override
public IPResponseCore getCoreAttribute(HttpServletRequest request) {
return (IPResponseCore) request.getAttribute(
IPinfoCoreSpring.ATTRIBUTE_KEY
);
}
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
package io.ipinfo.spring.strategies.attribute;

import io.ipinfo.api.model.IPResponse;
import io.ipinfo.api.model.IPResponseCore;
import io.ipinfo.api.model.IPResponseLite;
import io.ipinfo.spring.IPinfoCoreSpring;
import io.ipinfo.spring.IPinfoLiteSpring;
import io.ipinfo.spring.IPinfoSpring;
import jakarta.servlet.http.HttpServletRequest;
Expand Down Expand Up @@ -39,4 +41,21 @@ public IPResponseLite getLiteAttribute(HttpServletRequest request) {
.getSession()
.getAttribute(IPinfoLiteSpring.ATTRIBUTE_KEY);
}

@Override
public void storeCoreAttribute(
HttpServletRequest request,
IPResponseCore response
) {
request
.getSession()
.setAttribute(IPinfoCoreSpring.ATTRIBUTE_KEY, response);
}

@Override
public IPResponseCore getCoreAttribute(HttpServletRequest request) {
return (IPResponseCore) request
.getSession()
.getAttribute(IPinfoCoreSpring.ATTRIBUTE_KEY);
}
}
176 changes: 176 additions & 0 deletions src/test/java/io/ipinfo/spring/IPinfoCoreSpringTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
package io.ipinfo.spring;

import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.*;

import io.ipinfo.api.IPinfoCore;
import io.ipinfo.api.errors.RateLimitedException;
import io.ipinfo.api.model.ASN;
import io.ipinfo.api.model.Geo;
import io.ipinfo.api.model.IPResponseCore;
import io.ipinfo.spring.strategies.attribute.AttributeStrategy;
import io.ipinfo.spring.strategies.interceptor.InterceptorStrategy;
import io.ipinfo.spring.strategies.ip.IPStrategy;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.mock.web.MockHttpServletResponse;

@ExtendWith(MockitoExtension.class)
class IPinfoCoreSpringTest {

@Mock
private IPinfoCore mockIPinfoClient;

@Mock
private AttributeStrategy mockAttributeStrategy;

@Mock
private IPStrategy mockIpStrategy;

@Mock
private InterceptorStrategy mockInterceptorStrategy;

@InjectMocks
private IPinfoCoreSpring ipinfoSpring;

private MockHttpServletRequest request;
private MockHttpServletResponse response;
private Object handler;

private IPResponseCore dummyIPResponse;

@BeforeEach
void setUp() {
request = new MockHttpServletRequest();
response = new MockHttpServletResponse();
handler = new Object();

new IPResponseCore(
"8.8.8.8",
new Geo(
"Mountain View",
"California",
"CA",
"United States",
"US",
"North America",
"NA",
37.4056,
-122.0775,
"America/Los_Angeles",
"94043"
),
new ASN("AS15169", "Google LLC", "google.com", "", "hosting"),
false,
true,
true,
false,
false
);
}

@Test
@DisplayName("should skip processing if interceptorStrategy returns false")
void preHandle_shouldSkipIfInterceptorStrategyFalse() throws Exception {
when(mockInterceptorStrategy.shouldRun(request)).thenReturn(false);

boolean result = ipinfoSpring.preHandle(request, response, handler);

assertTrue(result, "preHandle should return true to continue chain");
// Verify that no other strategies were called if shouldRun returned false
verify(mockInterceptorStrategy).shouldRun(request);
verifyNoInteractions(
mockAttributeStrategy,
mockIpStrategy,
mockIPinfoClient
);
}

@Test
@DisplayName(
"should skip processing if attributeStrategy already has attribute"
)
void preHandle_shouldSkipIfHasCoreAttribute() throws Exception {
when(mockInterceptorStrategy.shouldRun(request)).thenReturn(true);
when(mockAttributeStrategy.hasCoreAttribute(request)).thenReturn(true);

boolean result = ipinfoSpring.preHandle(request, response, handler);

assertTrue(result, "preHandle should return true to continue chain");
verify(mockInterceptorStrategy).shouldRun(request);
verify(mockAttributeStrategy).hasCoreAttribute(request);
// Verify no IP lookup or storage occurred
verifyNoInteractions(mockIpStrategy, mockIPinfoClient);
}

@Test
@DisplayName("should skip processing if IPStrategy returns null IP")
void preHandle_shouldSkipIfIpIsNull() throws Exception {
when(mockInterceptorStrategy.shouldRun(request)).thenReturn(true);
when(mockAttributeStrategy.hasCoreAttribute(request)).thenReturn(false);
when(mockIpStrategy.getIPAddress(request)).thenReturn(null);

boolean result = ipinfoSpring.preHandle(request, response, handler);

assertTrue(result, "preHandle should return true to continue chain");
verify(mockInterceptorStrategy).shouldRun(request);
verify(mockAttributeStrategy).hasCoreAttribute(request);
verify(mockIpStrategy).getIPAddress(request);
// Verify no IP lookup or storage occurred
verifyNoInteractions(mockIPinfoClient);
verify(mockAttributeStrategy, never()).storeCoreAttribute(any(), any());
}

@Test
@DisplayName(
"should perform IP lookup and store attribute if all conditions met"
)
void preHandle_shouldProcessAndStore() throws Exception {
String testIp = "8.8.8.8";
when(mockInterceptorStrategy.shouldRun(request)).thenReturn(true);
when(mockAttributeStrategy.hasCoreAttribute(request)).thenReturn(false);
when(mockIpStrategy.getIPAddress(request)).thenReturn(testIp);
when(mockIPinfoClient.lookupIP(testIp)).thenReturn(dummyIPResponse);

boolean result = ipinfoSpring.preHandle(request, response, handler);

assertTrue(result, "preHandle should return true to continue chain");
verify(mockInterceptorStrategy).shouldRun(request);
verify(mockAttributeStrategy).hasCoreAttribute(request);
verify(mockIpStrategy).getIPAddress(request);
verify(mockIPinfoClient).lookupIP(testIp);
verify(mockAttributeStrategy).storeCoreAttribute(
request,
dummyIPResponse
);
}

@Test
@DisplayName("should rethrow RateLimitedException during lookup")
void preHandle_shouldRethrowRateLimitedException() throws Exception {
String testIp = "invalid.ip";
when(mockInterceptorStrategy.shouldRun(request)).thenReturn(true);
when(mockAttributeStrategy.hasCoreAttribute(request)).thenReturn(false);
when(mockIpStrategy.getIPAddress(request)).thenReturn(testIp);
// Simulate a RateLimitedException during lookup
when(mockIPinfoClient.lookupIP(testIp)).thenThrow(
new RateLimitedException()
);

assertThrows(RateLimitedException.class, () ->
ipinfoSpring.preHandle(request, response, handler)
);

verify(mockInterceptorStrategy).shouldRun(request);
verify(mockAttributeStrategy).hasCoreAttribute(request);
verify(mockIpStrategy).getIPAddress(request);
verify(mockIPinfoClient).lookupIP(testIp);
verify(mockAttributeStrategy, never()).storeCoreAttribute(any(), any());
}
}