diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
new file mode 100644
index 0000000..b36d753
--- /dev/null
+++ b/.github/workflows/test.yml
@@ -0,0 +1,26 @@
+name: Test
+
+on:
+ push:
+ branches:
+ - "master"
+ pull_request:
+ branches:
+ - "master"
+
+jobs:
+ build:
+ runs-on: ubuntu-latest
+
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: Set up JDK 17
+ uses: actions/setup-java@v4
+ with:
+ java-version: "17"
+ distribution: "temurin"
+ cache: maven
+
+ - name: Run Maven Tests
+ run: mvn -B test
diff --git a/README.md b/README.md
index fcf10ee..11b190f 100644
--- a/README.md
+++ b/README.md
@@ -25,7 +25,7 @@ fields and additional request volumes see
[Click here to view the Java Spring SDK's API documentation](https://ipinfo.github.io/spring/).
-⚠️ Note: This library does not currently support our newest free API https://ipinfo.io/lite. If you’d like to use IPinfo Lite, you can call the [endpoint directly](https://ipinfo.io/developers/lite-api) using your preferred HTTP client. Developers are also welcome to contribute support for Lite by submitting a pull request.
+The library also supports the Lite API, see the [Lite API section](#lite-api) for more info.
## Usage
@@ -174,6 +174,30 @@ The `AttributeStrategy` allows the middleware to know where to store the
Any exceptions such as `RateLimitedException` is passed through Spring's error
handling system.
+### Lite API
+
+The library gives the possibility to use the [Lite API](https://ipinfo.io/developers/lite-api) too, authentication with your token is still required.
+
+The returned details are slightly different from the Core API.
+
+To use the Lite API you must use the `IPinfoLiteSpring`, it works in the same way as the `IPinfoSpring` class.
+
+```java
+ IPinfoLiteSpring ipinfoSpring = new IPinfoLiteSpring.Builder()
+ // Set the IPinfo instance. By default we provide one, however you're
+ // allowed to change this here. Also provide your IPinfo Access Token here.
+ .setIPinfo(new IPinfoLite.Builder().setToken("IPINFO ACCESS TOKEN").build())
+ // Set the InterceptorStrategy. By default we use
+ // BotInterceptorStrategy.
+ .interceptorStrategy(new BotInterceptorStrategy())
+ // Set the IPStrategy. By default we use SimpleIPStrategy.
+ .ipStrategy(new SimpleIPStrategy())
+ // Set the AttributeStrategy. By default we use SessionAttributeStrategy.
+ .attributeStrategy(new SessionAttributeStrategy())
+ // Finally build it.
+ .build();
+```
+
### Other Libraries
There are [official IPinfo client libraries](https://ipinfo.io/developers/libraries) available for many languages including PHP, Python, Go, Java, Ruby, and many popular frameworks such as Django, Rails, and Laravel. There are also many third-party libraries and integrations available for our API.
diff --git a/pom.xml b/pom.xml
index 45f6328..ab69513 100644
--- a/pom.xml
+++ b/pom.xml
@@ -68,35 +68,59 @@
io.ipinfo
ipinfo-api
- 2.2.1
+ 3.1.0
compile
com.google.code.gson
gson
- 2.8.7
+ 2.13.1
compile
org.junit.jupiter
junit-jupiter-api
- 5.2.0
+ 6.0.0-M2
test
org.springframework
spring-webmvc
- 6.0.10
+ 7.0.0-M7
org.springframework
spring-core
- 6.0.10
+ 7.0.0-M7
org.apache.tomcat.embed
tomcat-embed-core
- 10.1.11
+ 11.0.9
+
+
+ org.junit.jupiter
+ junit-jupiter-engine
+ 6.0.0-M2
+ test
+
+
+ org.mockito
+ mockito-core
+ 5.18.0
+ test
+
+
+ org.mockito
+ mockito-junit-jupiter
+ 5.18.0
+ test
+
+
+ org.springframework
+ spring-test
+ 7.0.0-M7
+ test
diff --git a/src/main/java/io/ipinfo/spring/IPinfoLiteSpring.java b/src/main/java/io/ipinfo/spring/IPinfoLiteSpring.java
new file mode 100644
index 0000000..b66fe06
--- /dev/null
+++ b/src/main/java/io/ipinfo/spring/IPinfoLiteSpring.java
@@ -0,0 +1,111 @@
+package io.ipinfo.spring;
+
+import io.ipinfo.api.IPinfoLite;
+import io.ipinfo.api.model.IPResponseLite;
+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 IPinfoLiteSpring implements HandlerInterceptor {
+
+ public static final String ATTRIBUTE_KEY =
+ "IPinfoOfficialSparkWrapper.IPResponseLite";
+ private final IPinfoLite ii;
+ private final AttributeStrategy attributeStrategy;
+ private final IPStrategy ipStrategy;
+ private final InterceptorStrategy interceptorStrategy;
+
+ IPinfoLiteSpring(
+ IPinfoLite 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.hasLiteAttribute(request)) {
+ return true;
+ }
+
+ String ip = ipStrategy.getIPAddress(request);
+ if (ip == null) {
+ return true;
+ }
+
+ IPResponseLite ipResponse = ii.lookupIP(ip);
+ attributeStrategy.storeLiteAttribute(request, ipResponse);
+
+ return true;
+ }
+
+ public static class Builder {
+
+ private IPinfoLite ii = new IPinfoLite.Builder().build();
+ private AttributeStrategy attributeStrategy =
+ new SessionAttributeStrategy();
+ private IPStrategy ipStrategy = new SimpleIPStrategy();
+ private InterceptorStrategy interceptorStrategy =
+ new BotInterceptorStrategy();
+
+ public Builder setIPinfo(IPinfoLite 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 IPinfoLiteSpring build() {
+ return new IPinfoLiteSpring(
+ ii,
+ attributeStrategy,
+ ipStrategy,
+ interceptorStrategy
+ );
+ }
+ }
+}
diff --git a/src/main/java/io/ipinfo/spring/strategies/attribute/AttributeStrategy.java b/src/main/java/io/ipinfo/spring/strategies/attribute/AttributeStrategy.java
index 56abcfd..24fced4 100644
--- a/src/main/java/io/ipinfo/spring/strategies/attribute/AttributeStrategy.java
+++ b/src/main/java/io/ipinfo/spring/strategies/attribute/AttributeStrategy.java
@@ -1,7 +1,7 @@
package io.ipinfo.spring.strategies.attribute;
import io.ipinfo.api.model.IPResponse;
-
+import io.ipinfo.api.model.IPResponseLite;
import jakarta.servlet.http.HttpServletRequest;
public interface AttributeStrategy {
@@ -16,4 +16,23 @@ default boolean hasAttribute(HttpServletRequest request) {
return false;
}
+
+ default void storeLiteAttribute(
+ HttpServletRequest request,
+ IPResponseLite response
+ ) {
+ throw new UnsupportedOperationException(
+ "This strategy does not support IPResponseLite."
+ );
+ }
+
+ default IPResponseLite getLiteAttribute(HttpServletRequest request) {
+ throw new UnsupportedOperationException(
+ "This strategy does not support IPResponseLite."
+ );
+ }
+
+ default boolean hasLiteAttribute(HttpServletRequest request) {
+ return getLiteAttribute(request) != null;
+ }
}
diff --git a/src/main/java/io/ipinfo/spring/strategies/attribute/RequestAttributeStrategy.java b/src/main/java/io/ipinfo/spring/strategies/attribute/RequestAttributeStrategy.java
index afbf081..4143154 100644
--- a/src/main/java/io/ipinfo/spring/strategies/attribute/RequestAttributeStrategy.java
+++ b/src/main/java/io/ipinfo/spring/strategies/attribute/RequestAttributeStrategy.java
@@ -1,13 +1,18 @@
package io.ipinfo.spring.strategies.attribute;
import io.ipinfo.api.model.IPResponse;
+import io.ipinfo.api.model.IPResponseLite;
+import io.ipinfo.spring.IPinfoLiteSpring;
import io.ipinfo.spring.IPinfoSpring;
-
import jakarta.servlet.http.HttpServletRequest;
public class RequestAttributeStrategy implements AttributeStrategy {
+
@Override
- public void storeAttribute(HttpServletRequest request, IPResponse response) {
+ public void storeAttribute(
+ HttpServletRequest request,
+ IPResponse response
+ ) {
request.setAttribute(IPinfoSpring.ATTRIBUTE_KEY, response);
}
@@ -15,4 +20,19 @@ public void storeAttribute(HttpServletRequest request, IPResponse response) {
public IPResponse getAttribute(HttpServletRequest request) {
return (IPResponse) request.getAttribute(IPinfoSpring.ATTRIBUTE_KEY);
}
+
+ @Override
+ public void storeLiteAttribute(
+ HttpServletRequest request,
+ IPResponseLite response
+ ) {
+ request.setAttribute(IPinfoLiteSpring.ATTRIBUTE_KEY, response);
+ }
+
+ @Override
+ public IPResponseLite getLiteAttribute(HttpServletRequest request) {
+ return (IPResponseLite) request.getAttribute(
+ IPinfoLiteSpring.ATTRIBUTE_KEY
+ );
+ }
}
diff --git a/src/main/java/io/ipinfo/spring/strategies/attribute/SessionAttributeStrategy.java b/src/main/java/io/ipinfo/spring/strategies/attribute/SessionAttributeStrategy.java
index f1bb6fa..b83cac2 100644
--- a/src/main/java/io/ipinfo/spring/strategies/attribute/SessionAttributeStrategy.java
+++ b/src/main/java/io/ipinfo/spring/strategies/attribute/SessionAttributeStrategy.java
@@ -1,18 +1,42 @@
package io.ipinfo.spring.strategies.attribute;
import io.ipinfo.api.model.IPResponse;
+import io.ipinfo.api.model.IPResponseLite;
+import io.ipinfo.spring.IPinfoLiteSpring;
import io.ipinfo.spring.IPinfoSpring;
-
import jakarta.servlet.http.HttpServletRequest;
public class SessionAttributeStrategy implements AttributeStrategy {
+
@Override
- public void storeAttribute(HttpServletRequest request, IPResponse response) {
+ public void storeAttribute(
+ HttpServletRequest request,
+ IPResponse response
+ ) {
request.getSession().setAttribute(IPinfoSpring.ATTRIBUTE_KEY, response);
}
@Override
public IPResponse getAttribute(HttpServletRequest request) {
- return (IPResponse) request.getSession().getAttribute(IPinfoSpring.ATTRIBUTE_KEY);
+ return (IPResponse) request
+ .getSession()
+ .getAttribute(IPinfoSpring.ATTRIBUTE_KEY);
+ }
+
+ @Override
+ public void storeLiteAttribute(
+ HttpServletRequest request,
+ IPResponseLite response
+ ) {
+ request
+ .getSession()
+ .setAttribute(IPinfoLiteSpring.ATTRIBUTE_KEY, response);
+ }
+
+ @Override
+ public IPResponseLite getLiteAttribute(HttpServletRequest request) {
+ return (IPResponseLite) request
+ .getSession()
+ .getAttribute(IPinfoLiteSpring.ATTRIBUTE_KEY);
}
}
diff --git a/src/test/java/io/ipinfo/spring/IPinfoLiteSpringTest.java b/src/test/java/io/ipinfo/spring/IPinfoLiteSpringTest.java
new file mode 100644
index 0000000..60a5956
--- /dev/null
+++ b/src/test/java/io/ipinfo/spring/IPinfoLiteSpringTest.java
@@ -0,0 +1,162 @@
+package io.ipinfo.spring;
+
+import static org.junit.jupiter.api.Assertions.*;
+import static org.mockito.Mockito.*;
+
+import io.ipinfo.api.IPinfoLite;
+import io.ipinfo.api.errors.RateLimitedException;
+import io.ipinfo.api.model.IPResponseLite;
+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 IPinfoLiteSpringTest {
+
+ @Mock
+ private IPinfoLite mockIPinfoClient;
+
+ @Mock
+ private AttributeStrategy mockAttributeStrategy;
+
+ @Mock
+ private IPStrategy mockIpStrategy;
+
+ @Mock
+ private InterceptorStrategy mockInterceptorStrategy;
+
+ @InjectMocks
+ private IPinfoLiteSpring ipinfoSpring;
+
+ private MockHttpServletRequest request;
+ private MockHttpServletResponse response;
+ private Object handler;
+
+ private IPResponseLite dummyIPResponse;
+
+ @BeforeEach
+ void setUp() {
+ request = new MockHttpServletRequest();
+ response = new MockHttpServletResponse();
+ handler = new Object();
+
+ dummyIPResponse = new IPResponseLite(
+ "8.8.8.8",
+ "AS15169",
+ "Google LLC",
+ "google.com",
+ "US",
+ "United States",
+ "NA",
+ "North America"
+ );
+ }
+
+ @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_shouldSkipIfHasLiteAttribute() throws Exception {
+ when(mockInterceptorStrategy.shouldRun(request)).thenReturn(true);
+ when(mockAttributeStrategy.hasLiteAttribute(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).hasLiteAttribute(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.hasLiteAttribute(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).hasLiteAttribute(request);
+ verify(mockIpStrategy).getIPAddress(request);
+ // Verify no IP lookup or storage occurred
+ verifyNoInteractions(mockIPinfoClient);
+ verify(mockAttributeStrategy, never()).storeLiteAttribute(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.hasLiteAttribute(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).hasLiteAttribute(request);
+ verify(mockIpStrategy).getIPAddress(request);
+ verify(mockIPinfoClient).lookupIP(testIp);
+ verify(mockAttributeStrategy).storeLiteAttribute(
+ 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.hasLiteAttribute(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).hasLiteAttribute(request);
+ verify(mockIpStrategy).getIPAddress(request);
+ verify(mockIPinfoClient).lookupIP(testIp);
+ verify(mockAttributeStrategy, never()).storeLiteAttribute(any(), any());
+ }
+}
diff --git a/src/test/java/io/ipinfo/spring/IPinfoSpringTest.java b/src/test/java/io/ipinfo/spring/IPinfoSpringTest.java
new file mode 100644
index 0000000..a9bfc49
--- /dev/null
+++ b/src/test/java/io/ipinfo/spring/IPinfoSpringTest.java
@@ -0,0 +1,169 @@
+package io.ipinfo.spring;
+
+import static org.junit.jupiter.api.Assertions.*;
+import static org.mockito.Mockito.*;
+
+import io.ipinfo.api.IPinfo;
+import io.ipinfo.api.errors.RateLimitedException;
+import io.ipinfo.api.model.IPResponse;
+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 IPinfoSpringTest {
+
+ @Mock
+ private IPinfo mockIPinfoClient;
+
+ @Mock
+ private AttributeStrategy mockAttributeStrategy;
+
+ @Mock
+ private IPStrategy mockIpStrategy;
+
+ @Mock
+ private InterceptorStrategy mockInterceptorStrategy;
+
+ @InjectMocks
+ private IPinfoSpring ipinfoSpring;
+
+ private MockHttpServletRequest request;
+ private MockHttpServletResponse response;
+ private Object handler;
+
+ private IPResponse dummyIPResponse;
+
+ @BeforeEach
+ void setUp() {
+ request = new MockHttpServletRequest();
+ response = new MockHttpServletResponse();
+ handler = new Object();
+
+ dummyIPResponse = new IPResponse(
+ "8.8.8.8",
+ "dns.google",
+ false,
+ false,
+ false,
+ "Mountain View",
+ "CA",
+ "United States",
+ "37.40599,-122.07851",
+ "AS15169 Google LLC",
+ "94043",
+ "America/Los_Angeles",
+ null,
+ null,
+ null,
+ null,
+ null,
+ null
+ );
+ }
+
+ @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_shouldSkipIfHasAttribute() throws Exception {
+ when(mockInterceptorStrategy.shouldRun(request)).thenReturn(true);
+ when(mockAttributeStrategy.hasAttribute(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).hasAttribute(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.hasAttribute(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).hasAttribute(request);
+ verify(mockIpStrategy).getIPAddress(request);
+ // Verify no IP lookup or storage occurred
+ verifyNoInteractions(mockIPinfoClient);
+ verify(mockAttributeStrategy, never()).storeAttribute(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.hasAttribute(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).hasAttribute(request);
+ verify(mockIpStrategy).getIPAddress(request);
+ verify(mockIPinfoClient).lookupIP(testIp);
+ verify(mockAttributeStrategy).storeAttribute(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.hasAttribute(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).hasAttribute(request);
+ verify(mockIpStrategy).getIPAddress(request);
+ verify(mockIPinfoClient).lookupIP(testIp);
+ verify(mockAttributeStrategy, never()).storeAttribute(any(), any());
+ }
+}