diff --git a/pom.xml b/pom.xml index 8c889f8..c2b8ef1 100644 --- a/pom.xml +++ b/pom.xml @@ -57,7 +57,7 @@ io.ipinfo ipinfo-api - 3.1.0 + 3.2.0 compile diff --git a/src/main/java/io/ipinfo/spring/IPinfoCoreSpring.java b/src/main/java/io/ipinfo/spring/IPinfoCoreSpring.java new file mode 100644 index 0000000..aa8b836 --- /dev/null +++ b/src/main/java/io/ipinfo/spring/IPinfoCoreSpring.java @@ -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 + ); + } + } +} 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 24fced4..20c41bc 100644 --- a/src/main/java/io/ipinfo/spring/strategies/attribute/AttributeStrategy.java +++ b/src/main/java/io/ipinfo/spring/strategies/attribute/AttributeStrategy.java @@ -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; @@ -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; + } } 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 4143154..f2eb395 100644 --- a/src/main/java/io/ipinfo/spring/strategies/attribute/RequestAttributeStrategy.java +++ b/src/main/java/io/ipinfo/spring/strategies/attribute/RequestAttributeStrategy.java @@ -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; @@ -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 + ); + } } 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 b83cac2..d37fe0a 100644 --- a/src/main/java/io/ipinfo/spring/strategies/attribute/SessionAttributeStrategy.java +++ b/src/main/java/io/ipinfo/spring/strategies/attribute/SessionAttributeStrategy.java @@ -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; @@ -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); + } } diff --git a/src/test/java/io/ipinfo/spring/IPinfoCoreSpringTest.java b/src/test/java/io/ipinfo/spring/IPinfoCoreSpringTest.java new file mode 100644 index 0000000..da7e934 --- /dev/null +++ b/src/test/java/io/ipinfo/spring/IPinfoCoreSpringTest.java @@ -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()); + } +}