diff --git a/src/main/java/io/ipinfo/spring/IPinfoPlusSpring.java b/src/main/java/io/ipinfo/spring/IPinfoPlusSpring.java new file mode 100644 index 0000000..a1d1f11 --- /dev/null +++ b/src/main/java/io/ipinfo/spring/IPinfoPlusSpring.java @@ -0,0 +1,111 @@ +package io.ipinfo.spring; + +import io.ipinfo.api.IPinfoPlus; +import io.ipinfo.api.model.IPResponsePlus; +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 IPinfoPlusSpring implements HandlerInterceptor { + + public static final String ATTRIBUTE_KEY = + "IPinfoOfficialSparkWrapper.IPResponsePlus"; + private final IPinfoPlus ii; + private final AttributeStrategy attributeStrategy; + private final IPStrategy ipStrategy; + private final InterceptorStrategy interceptorStrategy; + + IPinfoPlusSpring( + IPinfoPlus 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.hasPlusAttribute(request)) { + return true; + } + + String ip = ipStrategy.getIPAddress(request); + if (ip == null) { + return true; + } + + IPResponsePlus ipResponse = ii.lookupIP(ip); + attributeStrategy.storePlusAttribute(request, ipResponse); + + return true; + } + + public static class Builder { + + private IPinfoPlus ii = new IPinfoPlus.Builder().build(); + private AttributeStrategy attributeStrategy = + new SessionAttributeStrategy(); + private IPStrategy ipStrategy = new SimpleIPStrategy(); + private InterceptorStrategy interceptorStrategy = + new BotInterceptorStrategy(); + + public Builder setIPinfo(IPinfoPlus 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 IPinfoPlusSpring build() { + return new IPinfoPlusSpring( + 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 20c41bc..4b44cc5 100644 --- a/src/main/java/io/ipinfo/spring/strategies/attribute/AttributeStrategy.java +++ b/src/main/java/io/ipinfo/spring/strategies/attribute/AttributeStrategy.java @@ -3,6 +3,7 @@ import io.ipinfo.api.model.IPResponse; import io.ipinfo.api.model.IPResponseCore; import io.ipinfo.api.model.IPResponseLite; +import io.ipinfo.api.model.IPResponsePlus; import jakarta.servlet.http.HttpServletRequest; public interface AttributeStrategy { @@ -55,4 +56,23 @@ default IPResponseCore getCoreAttribute(HttpServletRequest request) { default boolean hasCoreAttribute(HttpServletRequest request) { return getCoreAttribute(request) != null; } + + default void storePlusAttribute( + HttpServletRequest request, + IPResponsePlus response + ) { + throw new UnsupportedOperationException( + "This strategy does not support IPResponsePlus." + ); + } + + default IPResponsePlus getPlusAttribute(HttpServletRequest request) { + throw new UnsupportedOperationException( + "This strategy does not support IPResponsePlus." + ); + } + + default boolean hasPlusAttribute(HttpServletRequest request) { + return getPlusAttribute(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 f2eb395..ebe8c16 100644 --- a/src/main/java/io/ipinfo/spring/strategies/attribute/RequestAttributeStrategy.java +++ b/src/main/java/io/ipinfo/spring/strategies/attribute/RequestAttributeStrategy.java @@ -3,8 +3,10 @@ import io.ipinfo.api.model.IPResponse; import io.ipinfo.api.model.IPResponseCore; import io.ipinfo.api.model.IPResponseLite; +import io.ipinfo.api.model.IPResponsePlus; import io.ipinfo.spring.IPinfoCoreSpring; import io.ipinfo.spring.IPinfoLiteSpring; +import io.ipinfo.spring.IPinfoPlusSpring; import io.ipinfo.spring.IPinfoSpring; import jakarta.servlet.http.HttpServletRequest; @@ -52,4 +54,19 @@ public IPResponseCore getCoreAttribute(HttpServletRequest request) { IPinfoCoreSpring.ATTRIBUTE_KEY ); } + + @Override + public void storePlusAttribute( + HttpServletRequest request, + IPResponsePlus response + ) { + request.setAttribute(IPinfoPlusSpring.ATTRIBUTE_KEY, response); + } + + @Override + public IPResponsePlus getPlusAttribute(HttpServletRequest request) { + return (IPResponsePlus) request.getAttribute( + IPinfoPlusSpring.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 d37fe0a..4af0377 100644 --- a/src/main/java/io/ipinfo/spring/strategies/attribute/SessionAttributeStrategy.java +++ b/src/main/java/io/ipinfo/spring/strategies/attribute/SessionAttributeStrategy.java @@ -3,8 +3,10 @@ import io.ipinfo.api.model.IPResponse; import io.ipinfo.api.model.IPResponseCore; import io.ipinfo.api.model.IPResponseLite; +import io.ipinfo.api.model.IPResponsePlus; import io.ipinfo.spring.IPinfoCoreSpring; import io.ipinfo.spring.IPinfoLiteSpring; +import io.ipinfo.spring.IPinfoPlusSpring; import io.ipinfo.spring.IPinfoSpring; import jakarta.servlet.http.HttpServletRequest; @@ -58,4 +60,21 @@ public IPResponseCore getCoreAttribute(HttpServletRequest request) { .getSession() .getAttribute(IPinfoCoreSpring.ATTRIBUTE_KEY); } + + @Override + public void storePlusAttribute( + HttpServletRequest request, + IPResponsePlus response + ) { + request + .getSession() + .setAttribute(IPinfoPlusSpring.ATTRIBUTE_KEY, response); + } + + @Override + public IPResponsePlus getPlusAttribute(HttpServletRequest request) { + return (IPResponsePlus) request + .getSession() + .getAttribute(IPinfoPlusSpring.ATTRIBUTE_KEY); + } } diff --git a/src/test/java/io/ipinfo/spring/IPinfoPlusSpringTest.java b/src/test/java/io/ipinfo/spring/IPinfoPlusSpringTest.java new file mode 100644 index 0000000..17e58a6 --- /dev/null +++ b/src/test/java/io/ipinfo/spring/IPinfoPlusSpringTest.java @@ -0,0 +1,201 @@ +package io.ipinfo.spring; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +import io.ipinfo.api.IPinfoPlus; +import io.ipinfo.api.errors.RateLimitedException; +import io.ipinfo.api.model.ASNPlus; +import io.ipinfo.api.model.Abuse; +import io.ipinfo.api.model.Anonymous; +import io.ipinfo.api.model.Company; +import io.ipinfo.api.model.Domains; +import io.ipinfo.api.model.GeoPlus; +import io.ipinfo.api.model.IPResponsePlus; +import io.ipinfo.api.model.Mobile; +import io.ipinfo.api.model.Privacy; +import io.ipinfo.spring.strategies.attribute.AttributeStrategy; +import io.ipinfo.spring.strategies.interceptor.InterceptorStrategy; +import io.ipinfo.spring.strategies.ip.IPStrategy; +import java.util.ArrayList; +import java.util.List; +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 IPinfoPlusSpringTest { + + @Mock + private IPinfoPlus mockIPinfoClient; + + @Mock + private AttributeStrategy mockAttributeStrategy; + + @Mock + private IPStrategy mockIpStrategy; + + @Mock + private InterceptorStrategy mockInterceptorStrategy; + + @InjectMocks + private IPinfoPlusSpring ipinfoSpring; + + private MockHttpServletRequest request; + private MockHttpServletResponse response; + private Object handler; + + private IPResponsePlus dummyIPResponse; + + @BeforeEach + void setUp() { + request = new MockHttpServletRequest(); + response = new MockHttpServletResponse(); + handler = new Object(); + + new IPResponsePlus( + "8.8.8.8", + "dns.google", + new GeoPlus( + "Mountain View", + "California", + "CA", + "United States", + "US", + "North America", + "NA", + 37.4056, + -122.0775, + "America/Los_Angeles", + "94043", + "807", + "5375480", + 50, + "" + ), + new ASNPlus( + "AS15169", + "Google LLC", + "google.com", + "hosting", + "2021-05-01" + ), + new Mobile(), + new Anonymous(false, false, false, false), + new Abuse("", "", "", "", "", ""), + new Company("", "", ""), + new Privacy(false, false, false, false, false, ""), + new Domains("", new ArrayList()), + 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_shouldSkipIfHasPlusAttribute() throws Exception { + when(mockInterceptorStrategy.shouldRun(request)).thenReturn(true); + when(mockAttributeStrategy.hasPlusAttribute(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).hasPlusAttribute(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.hasPlusAttribute(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).hasPlusAttribute(request); + verify(mockIpStrategy).getIPAddress(request); + // Verify no IP lookup or storage occurred + verifyNoInteractions(mockIPinfoClient); + verify(mockAttributeStrategy, never()).storePlusAttribute(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.hasPlusAttribute(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).hasPlusAttribute(request); + verify(mockIpStrategy).getIPAddress(request); + verify(mockIPinfoClient).lookupIP(testIp); + verify(mockAttributeStrategy).storePlusAttribute( + 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.hasPlusAttribute(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).hasPlusAttribute(request); + verify(mockIpStrategy).getIPAddress(request); + verify(mockIPinfoClient).lookupIP(testIp); + verify(mockAttributeStrategy, never()).storePlusAttribute(any(), any()); + } +}