diff --git a/.circleci/config.yml b/.circleci/config.yml index 1d526ca0..b11bf633 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -1,13 +1,16 @@ -# Java Maven CircleCI 2.0 configuration file +# Java Maven CircleCI 2.1 configuration file # # Check https://circleci.com/docs/2.0/language-java/ for more details # -version: 2 +version: 2.1 + jobs: build: + parameters: + jdk-version: + type: string docker: - # specify the version you desire here - - image: cimg/openjdk:17.0.8 + - image: cimg/openjdk:<< parameters.jdk-version >> working_directory: ~/repo @@ -46,3 +49,13 @@ jobs: - store_test_results: path: target/surefire-reports +workflows: + build-and-test: + jobs: + - build: + matrix: + parameters: + jdk-version: + - "17.0" + - "21.0" + - "25.0" diff --git a/CHANGELOG.md b/CHANGELOG.md index ffadffe6..03ffdc10 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,29 @@ # Changelog -## master +## Unreleased + +## 3.0.0 (2026-09-08) + +- Add `CastleApiPaymentRequiredException` for HTTP 402 +- Add webhook signature verification (`Castle#verifyWebhookSignature`) and the `X-Castle-Signature` header constant +- Add the Events API: `eventsSchema`, `queryEvents` and `groupEvents` +- Add the privacy methods `requestUserData` and `deleteUserData` +- Set the default request timeout to 1000 milliseconds +- Add `CastleConfigurationBuilder#withTimeout(java.time.Duration)` +- Scope `system-stubs-jupiter` to tests +- Redact the `Authorization` header when HTTP request logging is enabled +- Add `Castle#close()` (`AutoCloseable`) to shut down the HTTP dispatcher and connection pool; it also clears the SDK singleton when this instance is the singleton +- Accept an empty response body in `CastleResponse` +- Return `Integer` from generated getters whose backing field is nullable (`BaseList#getDefaultItemArchivationTime`, `DeviceScreen#getScreen`, `IP#getAsn`, and the `ListItemsBatchResponse` counters) +- Remove `CastleUserDeviceContext` and `DeviceUserAgent` +- Remove the legacy `authenticate` and `track` endpoints +- Remove device management (`approve`, `report`, `userDevices`, `device`) and impersonation (`impersonateStart`, `impersonateEnd`) +- Remove the `recover` endpoint and the `removeUser` privacy method +- Remove the authenticate failover configuration (`AuthenticateFailoverStrategy`, `withAuthenticateFailoverStrategy`, the `failover_strategy` setting) and the `Verdict`/`CastleMessage` models +- Build and test against Java 17, 21 and 25 (CI matrix); bump JaCoCo to 0.8.15, Mockito to 5.23.0, System Stubs to 2.1.8, AssertJ to 3.27.7 and JUnit to 5.14.4 +- Upgrade runtime dependencies: Gson 2.14.0, Guava 33.6.0-jre, SLF4J 2.0.18, OkHttp 4.12.0, jackson-databind-nullable 0.2.10, swagger-annotations 1.6.16, ThreeTen-Backport 1.7.3; declare an explicit `jsr305` dependency for the JSR-305 annotations +- Upgrade test dependencies: Logback 1.5.34, JSONassert 1.5.3, spring-test 6.1.21; remove the unused System Rules dependency +- Upgrade build plugins: maven-compiler-plugin 3.14.0, maven-surefire-plugin 3.5.6, maven-source-plugin 3.4.0, maven-javadoc-plugin 3.12.0, versions-maven-plugin 2.18.0, maven-gpg-plugin 3.2.8 and central-publishing-maven-plugin 0.10.0 ## 2.6.1 (2025-07-28) diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md index c681fdca..f27bdd71 100644 --- a/DEVELOPMENT.md +++ b/DEVELOPMENT.md @@ -1,6 +1,6 @@ # Development -Use java8 for development +Use Java 17 or higher for development ## Setup @@ -12,7 +12,6 @@ To be able to run tests, create a file `castle_sdk.properties` in the folder `sr deny_list=TestDeny,Cookie timeout=100 backend_provider=OKHTTP - failover_strategy=CHALLENGE base_url=https://testing.api.dev.castle/v1/ ## Installing dependencies diff --git a/README.md b/README.md index c041f36d..1c39a934 100644 --- a/README.md +++ b/README.md @@ -4,10 +4,25 @@ # Requirements -- **Java Version:** This SDK requires **Java 17** or higher. +- **Java Version:** This SDK requires **Java 17** or higher, and is built and tested against Java 17, 21 and 25. + +# Supported APIs + +The SDK exposes the Castle API surface: + +| Group | Methods | +| --- | --- | +| Scoring | `risk`, `filter`, `log` | +| Lists | `createList`, `list`, `listAllLists`, `searchLists`, `updateList`, `deleteList` | +| List items | `createListItem`, `createOrUpdateListItems`, `searchListItems`, `countListItems`, `getListItem`, `updateListItem`, `archiveListItem`, `unarchiveListItem` | +| Privacy | `requestUserData`, `deleteUserData` | +| Events | `eventsSchema`, `queryEvents`, `groupEvents` | +| Webhooks | `verifyWebhookSignature` | +| Secure mode | `secureUserID` | +| Generic | `get`, `post`, `put`, `delete` | # Usage -See the [documentation](https://docs.castle.io) for how to use this SDK with the Castle APIs +See the [documentation](https://docs.castle.io) for how to use this SDK with the Castle APIs. # Quickstart @@ -16,17 +31,17 @@ When using Maven, add the following dependency to your `pom.xml` file: io.castle castle-java - 2.6.1 + 3.0.0 ``` ## Initialize the SDK -Go to the settings page of your Castle account and find your **API Secret** +Go to the settings page of your Castle account and find your **API Secret**. **Alt 1. Initialize using ENV variables** -On initialization the Castle SDK will look for the secret in the `CASTLE_API_SECRET` environment variable. If it is set, no options needs to be passed to the initializer. +On initialization the Castle SDK will look for the secret in the `CASTLE_API_SECRET` environment variable. If it is set, no options need to be passed to the initializer. ```java Castle castle = Castle.initialize(); @@ -38,10 +53,9 @@ Castle castle = Castle.initialize(); Castle castle = Castle.initialize("abcd"); ``` - **Alt 3. Initialize using configuration builder** -If you don't use ENV variables, you can set the secret programatically together +If you don't use ENV variables, you can set the secret programmatically together with other options by using `CastleConfigurationBuilder`. `Castle.configurationBuilder()` returns a configuration builder initialized with default settings. @@ -49,19 +63,82 @@ returns a configuration builder initialized with default settings. Castle castle = Castle.initialize( Castle.configurationBuilder() .apiSecret("abcd") - .enableHttpLogging(true) // Log all outgoing requests sent to Castle + .enableHttpLogging(true) + .withTimeout(java.time.Duration.ofMillis(1000)) .build() ); ``` All other settings will be set to their default values. -We can also maintain a global instance wich can be set the following way +`Castle` implements `AutoCloseable`. Call `close()` when the process shuts down, or use try-with-resources: + +```java +try (Castle castle = Castle.initialize("abcd")) { + castle.client().filter(...); +} +``` + +A Spring bean can release the HTTP client from a destroy callback: + +```java +@PreDestroy +public void shutdown() { + castle.close(); +} +``` + +A global instance can be registered with `Castle.setSingletonInstance(castle)` and read with `Castle.instance()`. + +# Scoring + +`risk`, `filter`, and `log` send the payload you pass. Put `context` (IP and headers) on that payload. `onRequest` and `mergeContext` do not attach fields to these requests. ```java -Castle.setSingletonInstance(castle); +CastleResponse response = castle.client().risk(ImmutableMap.builder() + .put("type", "$login") + .put("status", "$succeeded") + .put("request_token", requestToken) + .put("user", ImmutableMap.of("id", userId)) + .put("context", ImmutableMap.of( + "ip", ipAddress, + "headers", headers + )) + .build()); +``` + +Typed payloads work the same way: -// Use the singleton -Castle.instance().client().filter(...); +```java +Risk payload = new Risk() + .type(Risk.TypeEnum.LOGIN) + .status(Risk.StatusEnum.SUCCEEDED) + .requestToken(requestToken) + .user(new RiskUser().id(userId)) + .context(new Context() + .ip(ipAddress) + .addHeadersItem("User-Agent", userAgent)); + +FilterAndRiskResponse result = castle.client().risk(payload); +``` + +`CastleContextBuilder` can extract IP and headers from an `HttpServletRequest` for you to place on the payload: + +```java +CastleContext extracted = castle.contextBuilder() + .fromHttpServletRequest(request) + .build(); +``` + +Allowlist and denylist settings apply to that extraction. The default denylist is `Cookie` and `Authorization`. + +`doNotTrack(true)`, `client(true)`, and `onRequest(request, true)` store a flag on the client. Scoring and other API methods still send the HTTP request. + +# Webhooks + +Castle signs every webhook with HMAC-SHA256 of the raw request body. Verify the `X-Castle-Signature` header against those bytes: + +```java +boolean valid = castle.verifyWebhookSignature(request, rawBody); ``` # Configuring the SDK @@ -75,21 +152,21 @@ there is one that must be configured: If the API Secret is not provided, the client's initialization process will fail. It can be found in the settings page of the Castle dashboard. -Besides the aforementioned settings, the following are other application-level setting +Besides the aforementioned settings, the following are other application-level settings that can be optionally configured: * **Denylisted Headers**: a comma-separated list of strings representing HTTP headers that will - never get passed to the context object. See [The Context Object](#the-context-object). + never get passed to the context object extracted from an `HttpServletRequest`. * **Allowlisted Headers**: this is a comma-separated list of strings representing HTTP headers - that will get passed to the context object with each call to the Castle API, - unless they are denylisted. If not set or empty all headers will be sent. See [The Context Object](#the-context-object). - * **Authenticate Failover Strategy**: it can be set to `ALLOW`, `DENY`, `CHALLENGE` or `THROW`. - See also [Authenticate](#authenticate) - * **Timeout**: an integer that represents the time in milliseconds after which a request fails. + that will get passed to the extracted context object, + unless they are denylisted. If not set or empty all headers will be sent. + * **Timeout**: an integer that represents the time in milliseconds applied to connect, read, and write. + `CastleConfigurationBuilder#withTimeout(Duration)` accepts the same value as a `java.time.Duration`. * **Backend Provider**: The HTTP layer that will be used to make requests to the Castle API. Currently there is only one available and it uses [OkHttp](https://square.github.io/okhttp/). * **Base URL**: The base endpoint of the Castle API without any relative path. * **IP Headers**: The headers checked (in order) to use for the context IP. + * **Log HTTP**: when true, OkHttp logs request and response bodies. The `Authorization` header is redacted. Allowlist and Denylist are case-insensitive. @@ -99,7 +176,7 @@ See *[Where to Configure Settings](#where-to-configure-settings)* for a list of ## Where to Configure Settings Settings can be provided as a Java Properties file in the classpath, through -environmental variables or through methods calls on `CastleConfigurationBuilder` +environmental variables or through methods calls on `CastleConfigurationBuilder`. When two of these options are used, environmental variables take precedence over the Java Properties file. @@ -111,9 +188,8 @@ Setting | Default values, when they exist | Properties file key | Environment va --- | --- |---------------------| --- | API Secret | | `api_secret` | `CASTLE_API_SECRET` | Allowlisted Headers | | `allow_list` | `CASTLE_SDK_ALLOWLIST_HEADERS` | -Denylisted Headers | `Cookie` | `deny_list` | `CASTLE_SDK_DENYLIST_HEADERS` | -Timeout | `500` | `timeout` | `CASTLE_SDK_TIMEOUT` | -Authenticate Failover Strategy | `ALLOW` | `failover_strategy` | `CASTLE_SDK_AUTHENTICATE_FAILOVER_STRATEGY` | +Denylisted Headers | `Cookie`, `Authorization` | `deny_list` | `CASTLE_SDK_DENYLIST_HEADERS` | +Timeout | `1000` | `timeout` | `CASTLE_SDK_TIMEOUT` | Backend Provider | `OKHTTP` | `backend_provider` | `CASTLE_SDK_BACKEND_PROVIDER` | Base URL | `https://api.castle.io/` | `base_url` | `CASTLE_SDK_BASE_URL` | Log HTTP | false | `log_http` | `CASTLE_SDK_LOG_HTTP` | @@ -128,10 +204,9 @@ modified: ```properties api_secret= allow_list=User-Agent,Accept-Language,Accept-Encoding,Accept-Charset,Accept,Accept-Datetime,X-Forwarded-For,Forwarded,X-Forwarded,X-Real-IP,REMOTE_ADDR -deny_list=Cookie -timeout=500 +deny_list=Cookie,Authorization +timeout=1000 backend_provider=OKHTTP -failover_strategy=ALLOW base_url=https://api.castle.io/ log_http=false ip_headers= @@ -139,38 +214,15 @@ ip_headers= To configure using the `CastleConfigurationBuilder` use the corresponding method to set the values -```builder +```java Castle castle = Castle.initialize(Castle.configurationBuilder() .apiSecret("abcd") .withAllowListHeaders("User-Agent", "Accept-Language", "Accept-Encoding") - .withDenyListHeaders("Cookie") - .withTimeout(500) + .withDenyListHeaders("Cookie", "Authorization") + .withTimeout(1000) .withBackendProvider(CastleBackendProvider.OKHTTP) - .withAuthenticateFailoverStrategy(new AuthenticateFailoverStrategy(AuthenticateAction.ALLOW)) .withApiBaseUrl("https://api.castle.io/") .withLogHttpRequests(true) .ipHeaders(Arrays.asList("X-Forwarded-For", "CF-Connecting-IP")) .build()); ``` - -### The Authenticate Failover Strategy - -It is the strategy that will be used when a request to the `/v1/authenticate` endpoint -of the Castle API fails. -Also, see [`doNotTrack` boolean](the-donottrack-boolean) for another use case of a failover strategy. - -It can be one of the following options: -* return a specific *authenticate action* inside an instance of `Verdict`; -* throw an `io.castle.client.model.CastleRuntimeException`. - -See [configuration](#configuring-the-sdk) to find out how to enable a failover strategy and to -learn about its default value. - - -# Development branch - -Branch for development process. The castle-java-example application have a parallel dev branch for test proposes. - -To use on example application dev branch, first install locally: - - mvn clean install diff --git a/RELEASING.md b/RELEASING.md index eb4b8471..1b6c5d2a 100644 --- a/RELEASING.md +++ b/RELEASING.md @@ -1,11 +1,10 @@ +# Releasing -Releasing -========= - -1. Create branch `release-X.Y.Z`. +1. Create branch `X.Y.Z` from `main`. 2. Update the package version to the new version in `pom.xml` and `README.md` files. 3. Update `CHANGELOG.md` for the impending release. 4. `git commit -am "release X.Y.Z."` (where X.Y.Z is the new version). -5. Push to Github, make PR, and when ok, merge. -6. Make a release on Github, specify tag as `vX.Y.Z` to create a tag. Copy the Changelog entry to the release description. -7. Run `mvn -P castle-java-sdk,deploy clean deploy`. +5. Push to Github, make a PR to the `main` branch, and when approved, merge. +6. Make a release on Github from the `main` branch, specify tag as `vX.Y.Z` to create a tag. Copy the Changelog entry to the release description. +7. `git checkout main && git pull` +8. Run `mvn -P castle-java-sdk,deploy clean deploy`. diff --git a/pom.xml b/pom.xml index a3561f0c..72ef2469 100644 --- a/pom.xml +++ b/pom.xml @@ -6,7 +6,7 @@ io.castle castle-java jar - 2.6.1 + 3.0.0 ${project.groupId}:${project.artifactId}:${project.version} Castle adds real-time monitoring of your authentication stack, instantly notifying you and your users @@ -51,7 +51,7 @@ UTF-8 17 17 - 0.8.10 + 0.8.15 @@ -62,7 +62,7 @@ org.sonatype.central central-publishing-maven-plugin - 0.8.0 + 0.10.0 true ossrh @@ -71,7 +71,7 @@ org.apache.maven.plugins maven-gpg-plugin - 1.6 + 3.2.8 sign-artifacts @@ -91,22 +91,27 @@ org.openapitools jackson-databind-nullable - 0.2.6 + 0.2.10 javax.annotation javax.annotation-api 1.3.2 + + com.google.code.findbugs + jsr305 + 3.0.2 + org.threeten threetenbp - 1.6.8 + 1.7.3 io.swagger swagger-annotations - 1.6.11 + 1.6.16 jakarta.servlet @@ -117,61 +122,62 @@ com.squareup.okhttp3 okhttp - 4.11.0 + 4.12.0 com.squareup.okhttp3 logging-interceptor - 4.11.0 + 4.12.0 org.slf4j slf4j-api - 2.0.7 + 2.0.18 ch.qos.logback logback-classic - 1.4.12 + 1.5.34 test com.google.guava guava - 32.1.2-jre + 33.6.0-jre com.google.code.gson gson - 2.10.1 + 2.14.0 org.junit.jupiter junit-jupiter-api - 5.6.2 + 5.14.4 test org.junit.vintage junit-vintage-engine - 5.6.2 + 5.14.4 test uk.org.webcompere system-stubs-jupiter - 2.0.2 + 2.1.8 + test org.mockito mockito-core - 5.4.0 + 5.23.0 test org.assertj assertj-core - 3.24.2 + 3.27.7 test @@ -183,25 +189,19 @@ org.springframework spring-test - 6.0.11 + 6.1.21 test org.skyscreamer jsonassert - 1.5.1 - test - - - com.github.stefanbirkner - system-rules - 1.19.0 + 1.5.3 test com.squareup.okhttp3 mockwebserver - 4.11.0 + 4.12.0 test @@ -218,7 +218,7 @@ org.apache.maven.plugins maven-compiler-plugin - 3.8.0 + 3.14.0 17 17 @@ -229,7 +229,7 @@ org.apache.maven.plugins maven-javadoc-plugin - 3.0.1 + 3.12.0 private true @@ -246,7 +246,7 @@ org.apache.maven.plugins maven-source-plugin - 3.0.1 + 3.4.0 attach-sources @@ -259,9 +259,10 @@ org.apache.maven.plugins maven-surefire-plugin - 3.1.2 + 3.5.6 false + @{argLine} --add-opens java.base/java.util=ALL-UNNAMED --add-opens java.base/java.lang=ALL-UNNAMED @@ -297,7 +298,7 @@ org.codehaus.mojo versions-maven-plugin - 2.7 + 2.18.0 diff --git a/src/main/java/io/castle/client/Castle.java b/src/main/java/io/castle/client/Castle.java index 61cc4ee9..27618328 100644 --- a/src/main/java/io/castle/client/Castle.java +++ b/src/main/java/io/castle/client/Castle.java @@ -9,6 +9,7 @@ import io.castle.client.internal.config.CastleSdkInternalConfiguration; import io.castle.client.internal.json.CastleGsonModel; import io.castle.client.internal.utils.CastleContextBuilder; +import io.castle.client.internal.utils.Webhook; import io.castle.client.model.CastleResponse; import io.castle.client.model.CastleSdkConfigurationException; import org.slf4j.Logger; @@ -17,26 +18,26 @@ import jakarta.servlet.http.HttpServletRequest; /** - * Creates an instance of the Castle SDK - * - * This also provides methods for initialization of the SDK. - * {@code this#initialize()} must be called once per instance of the SDK - * - * Static method {@code this#setSingletonInstance()} can be called to set a global instance of the SDK. - * Once set the {@code this#instance()} method will return that instance + * Creates an instance of the Castle SDK. + *

+ * Initialize once with {@link #initialize()} or {@link #initialize(CastleConfiguration)}, + * then call {@link #client()} or {@link #onRequest(HttpServletRequest)} to send requests. + * {@code Castle} implements {@link AutoCloseable}; call {@link #close()} to release the HTTP client. + *

+ * Static method {@link #setSingletonInstance(Castle)} can be called to set a global instance of the SDK. + * Once set, {@link #instance()} returns that instance. */ -public class Castle { - public static final String URL_TRACK = "/v1/track"; - public static final String URL_AUTHENTICATE = "/v1/authenticate"; - public static final String URL_DEVICES = "/v1/devices/"; - public static final String URL_USERS = "/v1/users/"; - public static final String URL_IMPERSONATE = "/v1/impersonate"; +public class Castle implements AutoCloseable { public static final String URL_PRIVACY = "/v1/privacy/"; public static final String URL_RISK = "/v1/risk"; public static final String URL_FILTER = "/v1/filter"; public static final String URL_LOG = "/v1/log"; + public static final String URL_EVENTS = "/v1/events"; - public static final String URL_RECOVER = "/v1/users/%s/recover"; + /** + * Header used by Castle to sign webhook payloads. + */ + public static final String WEBHOOK_SIGNATURE_HEADER = "X-Castle-Signature"; public static final String URL_LISTS = "/v1/lists"; public static final String URL_LISTS_ID = "/v1/lists/%s"; @@ -123,8 +124,11 @@ public CastleApi client() throws IllegalStateException { } /** - * Creates a API client instance for sending a request - * @param doNotTrack when true, the API calls will be not realized and default values will be provided + * Creates an API client instance for sending a request. + * The {@code doNotTrack} flag is stored on the returned client and is not read by + * {@code risk}, {@code filter}, {@code log}, or other API methods. + * + * @param doNotTrack tracking flag stored on the returned client * @return A new instance of the API client {@code CastleApiImpl} * @throws IllegalStateException when the SDK has not been properly initialized */ @@ -213,8 +217,8 @@ public CastleApi buildApiClient(boolean doNotTrack) { } /** - * Create a API context for the given request. - * Tracking is ON by default. + * Creates an API client and extracts servlet headers and IP into an internal context object. + * {@code risk}, {@code filter}, and {@code log} send the payload as given; set {@code context} on the payload. * * @param request The request for data extraction * @return a API reference to make backend calls to the castle.io rest api. @@ -224,10 +228,12 @@ public CastleApi onRequest(HttpServletRequest request) { } /** - * Create a API context for the given request. + * Creates an API client and extracts servlet headers and IP into an internal context object. + * {@code risk}, {@code filter}, and {@code log} send the payload as given; set {@code context} on the payload. + * The {@code doNotTrack} flag is stored on the returned client and is not read by those methods. * * @param request The request for data extraction - * @param doNotTrack when true, the API calls will be not realized and default values will be provided + * @param doNotTrack tracking flag stored on the returned client * @return a API reference to make backend calls to the castle.io rest api. */ public CastleApi onRequest(HttpServletRequest request, boolean doNotTrack) { @@ -271,7 +277,39 @@ public String secureUserID(String userId) { } /** - * Make a GET request to a Castle API endpoint such as /v1/{userId}/devices + * Verifies a Castle webhook signature against the raw request body. + *

+ * Castle signs every webhook with an HMAC-SHA256 of the raw request body using + * the account API secret, base64 encoded and sent in the + * {@code X-Castle-Signature} header. + * + * @param signature the value of the {@code X-Castle-Signature} header + * @param body the raw request body bytes + * @return {@code true} when the signature matches the computed signature + */ + public boolean verifyWebhookSignature(String signature, byte[] body) { + return Webhook.verifySignature(internalConfiguration.getConfiguration().getApiSecret(), body, signature); + } + + /** + * Verifies a Castle webhook signature for the given servlet request. + *

+ * The signature is read from the {@code X-Castle-Signature} header and verified + * against the supplied raw request body bytes. + * + * @param request the incoming webhook request + * @param body the raw request body bytes + * @return {@code true} when the signature matches the computed signature + */ + public boolean verifyWebhookSignature(HttpServletRequest request, byte[] body) { + if (request == null) { + return false; + } + return verifyWebhookSignature(request.getHeader(WEBHOOK_SIGNATURE_HEADER), body); + } + + /** + * Make a GET request to a Castle API endpoint such as /v1/lists * * @param path api path * @return a decoded json response @@ -281,7 +319,7 @@ public CastleResponse get(String path) { } /** - * Make a POST request to a Castle API endpoint such as /v1/track + * Make a POST request to a Castle API endpoint such as /v1/risk * * @param path api path * @param payload request payload @@ -291,7 +329,7 @@ public CastleResponse post(String path, ImmutableMap payload) { return client().post(path, payload); } /** - * Make a PUT request to a Castle API endpoint such as /v1/devices/{deviceToken}/report + * Make a PUT request to a Castle API endpoint such as /v1/lists/{id} * * @param path api path * @return a decoded json response @@ -301,7 +339,7 @@ public CastleResponse put(String path) { } /** - * Make a PUT request to a Castle API endpoint such as /v1/devices/{deviceToken}/report + * Make a PUT request to a Castle API endpoint such as /v1/lists/{id} * * @param path api path * @param payload request payload @@ -312,7 +350,7 @@ public CastleResponse put(String path, ImmutableMap payload) { } /** - * Make a DELETE request to a Castle API endpoint such as /v1/impersonate + * Make a DELETE request to a Castle API endpoint such as /v1/lists/{id} * * @param path api path * @return a decoded json response @@ -322,7 +360,7 @@ public CastleResponse delete(String path) { } /** - * Make a DELETE request to a Castle API endpoint such as /v1/impersonate + * Make a DELETE request to a Castle API endpoint such as /v1/lists/{id} * * @param path api path * @param payload request payload @@ -331,4 +369,16 @@ public CastleResponse delete(String path) { public CastleResponse delete(String path, ImmutableMap payload) { return client().delete(path, payload); } + + /** + * Releases the HTTP dispatcher and connection pool used by this instance. + * When this instance is the SDK singleton, {@link #instance()} is cleared. + */ + @Override + public void close() { + internalConfiguration.close(); + if (instance == this) { + instance = null; + } + } } diff --git a/src/main/java/io/castle/client/api/CastleApi.java b/src/main/java/io/castle/client/api/CastleApi.java index ae782d8d..7a7bd43d 100644 --- a/src/main/java/io/castle/client/api/CastleApi.java +++ b/src/main/java/io/castle/client/api/CastleApi.java @@ -1,47 +1,20 @@ package io.castle.client.api; import com.google.common.collect.ImmutableMap; -import com.google.gson.JsonElement; import io.castle.client.model.*; import io.castle.client.model.generated.*; -import javax.annotation.Nullable; import java.util.List; /** - * Contains methods for calling the Castle API and the settings needed to properly make such a request. - *

- * Methods of this interface can be used to make calls to Castle's API - * {@code /v1/authenticate} and {@code /v1/track} endpoints. - *

- * A {@code castleApi} contains all necessary configurations to correctly call the Castle API. - * In particular, it contains: - *

    - *
  • a configuration object containing; - *
  • an HTTP layer for handling HTTP requests and responses; - *
  • a context object with metadata on the request made by the user to the server containing this client. - *

- * The context object could have one of the following origins: - *

    - *
  • the default setting for a context object; - *
  • T - *
  • A context object with metadata on the request made by the user to the server containing this client. - *

- * An instance of {@code CastleApi} contains a boolean named doNotTrack in a private field. - * When doNotTrack is set to false, the CastleApi instance created is configured to make requests when any - * of its methods is called. - * When set to true, authenticate and track methods return immediately without making any request. - * The {@code this#authenticate} method will resort to the {@link io.castle.client.model.AuthenticateFailoverStrategy} - * with authenticate action set to {@link io.castle.client.model.AuthenticateAction#ALLOW}. - * When doNotTrack is set to true, this will resort to the {@link io.castle.client.model.AuthenticateFailoverStrategy} - * with authenticate action set to {@link io.castle.client.model.AuthenticateAction#ALLOW}. + * Client for the Castle REST API. */ public interface CastleApi { /** * Merges an additional context object with the context object associated with this {@code CastleApi} instance. - *

- * When the additional context is null, then the returned context is an empty JSON object. + * The merged object is stored on the returned client and is not attached to {@code risk}, {@code filter}, or {@code log} payloads. + * When the additional context is null, then the stored context is an empty JSON object. * * @param additionalContext client defined model, takes null * @return an API reference with the merged context values @@ -49,260 +22,68 @@ public interface CastleApi { CastleApi mergeContext(Object additionalContext); /** - * Makes a sync POST request to the authenticate endpoint containing all required parameters. - *

- * Optional parameters (that is, traits and properties) are set to null. + * Returns a client that stores the do-not-track flag. + * The flag is not read by {@code risk}, {@code filter}, {@code log}, or other API methods. * - * @param event a String representing an event understood by the Castle API - * @param userId a String representing a user ID associated to an authentication attempt - * @return a verdict that might result from a successful call to the Castle API or from the client's - * {@link io.castle.client.model.AuthenticateFailoverStrategy}, in case of a failed call - * @see The docs - */ - Verdict authenticate(String event, String userId); - - /** - * Makes a sync POST request to the authenticate endpoint containing required and optional parameters. - * - * @param event a String representing an event understood by the Castle API - * @param userId a String representing a user ID associated to an authentication attempt - * @param properties object for recording additional information connected to the event, takes null - * @param traits object for recording additional information connected to the user, takes null - * @return a verdict that might result from a successful call to the Castle API or from the client's - * {@link io.castle.client.model.AuthenticateFailoverStrategy}, in case of a failed call - * @see The docs - */ - Verdict authenticate(String event, String userId, @Nullable Object properties, @Nullable Object traits); - - /** - * Makes a sync POST request to the authenticate endpoint containing required and optional parameters. - * @param message Event parameters - * @return a verdict that might result from a successful call to the Castle API or from the client's - */ - Verdict authenticate(CastleMessage message); - - JsonElement buildAuthenticateRequest(CastleMessage request); - - Verdict sendAuthenticateRequest(JsonElement request); - - void sendAuthenticateRequest(JsonElement request, AsyncCallbackHandler asyncCallbackHandler); - - /** - * Makes an async POST request to the authenticate endpoint containing required and optional parameters. - * - * @param event a String representing an event understood by the Castle API - * @param userId a String representing a user ID associated to an authentication attempt - * @param properties object for recording additional information connected to the event, takes null - * @param traits object for recording additional information connected to the user, takes null - * @param asyncCallbackHandler a user-implemented instance of {@code AsyncCallbackHandler} which specifies - * how to handle success of failure of authenticate API calls - * @see The docs - */ - void authenticateAsync(String event, String userId, @Nullable Object properties, @Nullable Object traits, AsyncCallbackHandler asyncCallbackHandler); - - /** - * Makes an async POST request to the authenticate endpoint containing all required parameters. - *

- * Optional parameters (that is, traits and properties) are set to null. - * - * @param event a String representing an event understood by the Castle API - * @param userId a String representing a user ID associated to an authentication attempt - * @param asyncCallbackHandler a user-implemented instance of {@code AsyncCallbackHandler} which specifies - * how to handle success of failure of authenticate API calls - * @see The docs - */ - void authenticateAsync(String event, String userId, AsyncCallbackHandler asyncCallbackHandler); - - /** - * Makes an async POST request to the authenticate endpoint containing required and optional parameters. - * @param message Event parameters - * @param asyncCallbackHandler a user-implemented instance of {@code AsyncCallbackHandler} which specifies - * how to handle success of failure of authenticate API calls - */ - void authenticateAsync(CastleMessage message, AsyncCallbackHandler asyncCallbackHandler); - - /** - * Sets the doNotTrack boolean of a new instance of {@code CastleApi} - * - * @param doNotTrack boolean representing the value that the doNotTrack private field of the new instance of - * {@code CastleApi} - * @return a {@code castleApi} reference whose doNotTrack private field is set to the doNotTrack parameter + * @param doNotTrack boolean stored on the returned client + * @return a {@code CastleApi} reference that stores the given flag */ CastleApi doNotTrack(boolean doNotTrack); - /** - * Makes an async POST request to the track endpoint containing all required parameters. - * - * @param event a String representing an event understood by the Castle API - * @see The docs - */ - void track(String event); - - /** - * Makes an async POST request to the track endpoint containing all required parameters and a user ID. - * - * @param event a String representing an event understood by the Castle API - * @param userId a String representing a user ID - * @see The docs - */ - void track(String event, @Nullable String userId); - - /** - * Makes an async POST request to the track endpoint containing all required parameters and a user ID. - * - * @param event a String representing an event understood by the Castle API - * @param userId a String representing a user ID - * @param reviewId a String representing a reference to review ID - * @see The docs - */ - void track(String event, @Nullable String userId, @Nullable String reviewId); - - /** - * Makes an async POST request to the track endpoint containing required and optional parameters. - * - * @param event a String representing an event understood by the Castle API - * @param userId a String representing a user ID - * @param reviewId a String representing a reference to review ID - * @param properties object for recording additional information connected to the event, takes null - * @see The docs - */ - void track(String event, @Nullable String userId, @Nullable String reviewId, @Nullable Object properties); - - /** - * Makes an async POST request to the track endpoint containing required and optional parameters. - * - * @param event a String representing an event understood by the Castle API - * @param userId a String representing a user ID - * @param reviewId a String representing a reference to review ID - * @param properties object for recording additional information connected to the event, takes null - * @param traits object for recording additional information about the user like email or name, takes null - * @see The docs - */ - void track(String event, @Nullable String userId, @Nullable String reviewId, @Nullable Object properties, @Nullable Object traits); - - /** - * Makes an async POST request to the track endpoint containing required and optional parameters and a custom handler - * for the async call's success and failure cases. - * - * @param event a String representing an event understood by the Castle API - * @param userId a String representing a user ID - * @param reviewId a String representing a reference to review ID - * @param properties object for recording additional information connected to the event, takes null - * @param traits object for recording additional information about the user like email or name, takes null - * @param asyncCallbackHandler a user-implemented instance of {@code AsyncCallbackHandler} which specifies - * how to handle success of failure of authenticate API calls - * @see The docs - */ - void track(String event, @Nullable String userId, @Nullable String reviewId, @Nullable Object properties, @Nullable Object traits, AsyncCallbackHandler asyncCallbackHandler); - - /** - * Makes an async POST request to the track endpoint containing required and optional parameters. - * @param message Event parameters - */ - void track(CastleMessage message); - - JsonElement buildTrackRequest(CastleMessage request); - - void sendTrackRequest(JsonElement request); - - void sendTrackRequest(JsonElement request, AsyncCallbackHandler asyncCallbackHandler); + CastleResponse get(String path); - /** - * Makes an async POST request to the track endpoint containing required and optional parameters. - * @param message Event parameters - * @param asyncCallbackHandler a user-implemented instance of {@code AsyncCallbackHandler} which specifies - * how to handle success of failure of authenticate API calls - */ - void track(CastleMessage message, AsyncCallbackHandler asyncCallbackHandler); + CastleResponse post(String path, Object payload); - /** - * Makes a DELETE request to the privacy endpoint. - * - * @param userId String representing a user id - * @see The docs - */ - Boolean removeUser(String userId); + CastleResponse put(String path); - /** - * Makes a sync POST request to the approve device endpoint. - * - * @param deviceToken string representing the device to approve - * @return device model object - */ - CastleUserDevice approve(String deviceToken); + CastleResponse put(String path, Object payload); - /** - * Makes a sync POST request to the report device endpoint. - * - * @param deviceToken string representing the device to report - * @return device model object - */ - CastleUserDevice report(String deviceToken); + CastleResponse delete(String path); - /** - * Makes a sync GET request to the user devices endpoint. - * - * @param userId user unique ID - * @return devices model object - */ - CastleUserDevices userDevices(String userId); + CastleResponse delete(String path, Object payload); /** - * Makes a sync GET request to the device endpoint. + * Makes a sync POST request to the privacy endpoint to request a user's data. * - * @param deviceToken string representing the device to report - * @return device model object + * @param payload request parameters + * @return a decoded json response */ - CastleUserDevice device(String deviceToken); + CastleResponse requestUserData(ImmutableMap payload); /** - * Makes a sync POST request to the impersonate endpoint. + * Makes a sync DELETE request to the privacy endpoint to delete a user's data. * - * @param userId user unique ID - * @return + * @param payload request parameters + * @return a decoded json response */ - CastleSuccess impersonateStart(String userId); + CastleResponse deleteUserData(ImmutableMap payload); /** - * Makes a sync POST request to the impersonate endpoint. + * Makes a sync GET request to the events schema endpoint. * - * @param userId user unique ID - * @param impersonator description of impersonator, e.g., email - * @return + * @return a decoded json response */ - CastleSuccess impersonateStart(String userId, String impersonator); + CastleResponse eventsSchema(); /** - * Makes a sync DELETE request to the impersonate endpoint. + * Makes a sync POST request to the events query endpoint. * - * @param userId user unique ID - * @return + * @param payload query parameters + * @return a decoded json response */ - CastleSuccess impersonateEnd(String userId); + CastleResponse queryEvents(ImmutableMap payload); /** - * Makes a sync DELETE request to the impersonate endpoint. + * Makes a sync POST request to the events group endpoint. * - * @param userId user unique ID - * @param impersonator description of impersonator, e.g., email - * @return + * @param payload group parameters + * @return a decoded json response */ - CastleSuccess impersonateEnd(String userId, String impersonator); - - CastleResponse get(String path); - - CastleResponse post(String path, Object payload); - - CastleResponse put(String path); - - CastleResponse put(String path, Object payload); - - CastleResponse delete(String path); - - CastleResponse delete(String path, Object payload); + CastleResponse groupEvents(ImmutableMap payload); /** - * Makes a sync POST request to the risk endpoint. + * Sends a POST request to {@code /v1/risk}. + * The payload is sent as given. Set {@code context} on the payload for IP and headers. * * @param payload Event parameters * @return @@ -310,7 +91,8 @@ public interface CastleApi { CastleResponse risk(ImmutableMap payload); /** - * Makes a sync POST request to the risk endpoint. + * Sends a POST request to {@code /v1/risk}. + * The payload is sent as given. Set {@code context} on the payload for IP and headers. * * @param payload Event parameters * @return @@ -421,7 +203,8 @@ public interface CastleApi { CastleResponse unarchiveListItem(String listId, String itemid); /** - * Makes a sync POST request to the filter endpoint. + * Sends a POST request to {@code /v1/filter}. + * The payload is sent as given. Set {@code context} on the payload for IP and headers. * * @param payload Event parameters * @return @@ -429,7 +212,8 @@ public interface CastleApi { CastleResponse filter(ImmutableMap payload); /** - * Makes a sync POST request to the filter endpoint. + * Sends a POST request to {@code /v1/filter}. + * The payload is sent as given. Set {@code context} on the payload for IP and headers. * * @param payload Event parameters * @return @@ -437,7 +221,8 @@ public interface CastleApi { FilterAndRiskResponse filter(Filter payload); /** - * Makes a sync POST request to the log endpoint. + * Sends a POST request to {@code /v1/log}. + * The payload is sent as given. Set {@code context} on the payload for IP and headers. * * @param payload Event parameters * @return @@ -445,18 +230,11 @@ public interface CastleApi { CastleResponse log(ImmutableMap payload); /** - * Makes a sync POST request to the log endpoint. + * Sends a POST request to {@code /v1/log}. + * The payload is sent as given. Set {@code context} on the payload for IP and headers. * * @param payload Event parameters * @return */ CastleResponse log(Log payload); - - /** - * Makes a sync PUT request to the recover endpoint. - * - * @param userId User ID - * @return - */ - CastleResponse recover(String userId); } diff --git a/src/main/java/io/castle/client/internal/CastleApiImpl.java b/src/main/java/io/castle/client/internal/CastleApiImpl.java index aaa96527..5e5cdb1f 100644 --- a/src/main/java/io/castle/client/internal/CastleApiImpl.java +++ b/src/main/java/io/castle/client/internal/CastleApiImpl.java @@ -2,7 +2,6 @@ import com.google.common.base.Preconditions; import com.google.common.collect.ImmutableMap; -import com.google.gson.JsonElement; import com.google.gson.JsonObject; import com.google.gson.reflect.TypeToken; import io.castle.client.Castle; @@ -11,13 +10,10 @@ import io.castle.client.internal.config.CastleSdkInternalConfiguration; import io.castle.client.internal.utils.CastleContextBuilder; import io.castle.client.internal.utils.ContextMerge; -import io.castle.client.internal.utils.Timestamp; -import io.castle.client.internal.utils.VerdictBuilder; import io.castle.client.model.*; import io.castle.client.model.generated.*; import jakarta.servlet.http.HttpServletRequest; -import javax.annotation.Nullable; import java.lang.reflect.Type; import java.util.List; @@ -70,258 +66,79 @@ public CastleApi doNotTrack(boolean doNotTrack) { } @Override - public Verdict authenticate(String event, String userId) { - return authenticate(event, userId, null, null); - } - - @Override - public Verdict authenticate(String event, String userId, @Nullable Object properties, @Nullable Object traits) { - return authenticate(buildMessage(event, userId, properties, traits)); - } - - @Override - public Verdict authenticate(CastleMessage message) { - JsonElement request = buildAuthenticateRequest(message); - return sendAuthenticateRequest(request); - } - - @Override - public JsonElement buildAuthenticateRequest(CastleMessage message) { - return buildJson(message); - } - - @Override - public Verdict sendAuthenticateRequest(JsonElement request) { - Preconditions.checkNotNull(request, "Request json can not be null"); - - if (doNotTrack) { - return buildVerdictForDoNotTrack(request.getAsJsonObject().get("user_id").getAsString()); - } - - RestApi restApi = configuration.getRestApiFactory().buildBackend(); - return restApi.sendAuthenticateSync(request); - } - - @Override - public void sendAuthenticateRequest(JsonElement request, AsyncCallbackHandler asyncCallbackHandler) { - Preconditions.checkNotNull(request, "Request json can not be null"); - - if (doNotTrack) { - asyncCallbackHandler.onResponse(buildVerdictForDoNotTrack(request.getAsJsonObject().get("user_id").getAsString())); - } else { - Preconditions.checkNotNull(asyncCallbackHandler, "The async handler can not be null"); - RestApi restApi = configuration.getRestApiFactory().buildBackend(); - restApi.sendAuthenticateAsync(request, asyncCallbackHandler); - } - } - - private Verdict buildVerdictForDoNotTrack(String userId) { - return VerdictBuilder.failover("Castle set to do not track.") - .withAction(AuthenticateAction.ALLOW) - .withUserId(userId) - .build(); - } - - @Override - public void authenticateAsync(String event, String userId, @Nullable Object properties, @Nullable Object traits, AsyncCallbackHandler asyncCallbackHandler) { - authenticateAsync( - buildMessage(event, userId, properties, traits), - asyncCallbackHandler - ); - } - - @Override - public void authenticateAsync(String event, String userId, AsyncCallbackHandler asyncCallbackHandler) { - authenticateAsync( - CastleMessage.builder(event).userId(userId).build(), - asyncCallbackHandler - ); - } - - @Override - public void authenticateAsync(CastleMessage message, AsyncCallbackHandler asyncCallbackHandler) { - JsonElement request = buildAuthenticateRequest(message); - sendAuthenticateRequest(request, asyncCallbackHandler); - } - - @Override - public void track(String event) { - track(event, null, null, null, null); - } - - @Override - public void track(String event, String userId) { - track(event, userId, null, null, null); - } - - @Override - public void track(String event, @Nullable String userId, @Nullable String reviewId) { - track(event, userId, reviewId, null, null, null); - } - - @Override - public void track(String event, String userId, String reviewId, Object properties) { - track(event, userId, reviewId, properties, null, null); - } - - @Override - public void track(String event, @Nullable String userId, @Nullable String reviewId, @Nullable Object properties, @Nullable Object traits) { - track(event, userId, reviewId, properties, traits, null); - } - - @Override - public void track(String event, @Nullable String userId, @Nullable String reviewId, @Nullable Object properties, @Nullable Object traits, AsyncCallbackHandler asyncCallbackHandler) { - - CastleMessage message = buildMessage(event, userId, properties, traits); - - if (reviewId != null) { - message.setReviewId(reviewId); - } - - track(message, asyncCallbackHandler); - } - - @Override - public void track(CastleMessage message) { - track(message, null); - } - - @Override - public JsonElement buildTrackRequest(CastleMessage message) { - Preconditions.checkNotNull(message.getEvent()); - return buildJson(message); - } - - @Override - public void sendTrackRequest(JsonElement request) { - sendTrackRequest(request, null); - } - - @Override - public void sendTrackRequest(JsonElement request, AsyncCallbackHandler asyncCallbackHandler) { - Preconditions.checkNotNull(request, "Request json can not be null"); - - if (doNotTrack) { - if (asyncCallbackHandler != null) { - asyncCallbackHandler.onResponse(true); - } - return; - } - - RestApi restApi = configuration.getRestApiFactory().buildBackend(); - restApi.sendTrackRequest(request, asyncCallbackHandler); - } - - @Override - public void track(CastleMessage message, @Nullable AsyncCallbackHandler asyncCallbackHandler) { - JsonElement messageJson = buildTrackRequest(message); - - sendTrackRequest(messageJson, asyncCallbackHandler); - } - - @Override - public Boolean removeUser(String userId) { - Preconditions.checkNotNull(userId); - RestApi restApi = configuration.getRestApiFactory().buildBackend(); - return restApi.sendPrivacyRemoveUser(userId); - } - - @Override - public CastleUserDevice approve(String deviceToken) { - Preconditions.checkNotNull(deviceToken); - RestApi restApi = configuration.getRestApiFactory().buildBackend(); - return restApi.sendApproveDeviceRequestSync(deviceToken); - } - - @Override - public CastleUserDevice report(String deviceToken) { - Preconditions.checkNotNull(deviceToken); + public CastleResponse get(String path) { RestApi restApi = configuration.getRestApiFactory().buildBackend(); - return restApi.sendReportDeviceRequestSync(deviceToken); + return restApi.get(path); } @Override - public CastleUserDevices userDevices(String userId) { - Preconditions.checkNotNull(userId); + public CastleResponse post(String path, Object payload) { RestApi restApi = configuration.getRestApiFactory().buildBackend(); - return restApi.sendGetUserDevicesRequestSync(userId); + return restApi.post(path, payload); } @Override - public CastleUserDevice device(String deviceToken) { - Preconditions.checkNotNull(deviceToken); + public CastleResponse put(String path) { RestApi restApi = configuration.getRestApiFactory().buildBackend(); - return restApi.sendGetUserDeviceRequestSync(deviceToken); + return restApi.put(path); } @Override - public CastleSuccess impersonateStart(String userId) { - Preconditions.checkNotNull(userId); + public CastleResponse put(String path, Object payload) { RestApi restApi = configuration.getRestApiFactory().buildBackend(); - return restApi.sendImpersonateStartRequestSync(userId, null, contextJson); + return restApi.put(path, payload); } @Override - public CastleSuccess impersonateStart(String userId, String impersonator) { - Preconditions.checkNotNull(userId); + public CastleResponse delete(String path) { RestApi restApi = configuration.getRestApiFactory().buildBackend(); - return restApi.sendImpersonateStartRequestSync(userId, impersonator, contextJson); + return restApi.delete(path); } @Override - public CastleSuccess impersonateEnd(String userId) { - Preconditions.checkNotNull(userId); + public CastleResponse delete(String path, Object payload) { RestApi restApi = configuration.getRestApiFactory().buildBackend(); - return restApi.sendImpersonateEndRequestSync(userId, "", contextJson); + return restApi.delete(path, payload); } - @Override - public CastleSuccess impersonateEnd(String userId, String impersonator) { - Preconditions.checkNotNull(userId); + public CastleResponse risk(ImmutableMap payload) { + Preconditions.checkNotNull(payload); RestApi restApi = configuration.getRestApiFactory().buildBackend(); - return restApi.sendImpersonateEndRequestSync(userId, impersonator, contextJson); + return restApi.post(Castle.URL_RISK, payload); } @Override - public CastleResponse get(String path) { + public CastleResponse requestUserData(ImmutableMap payload) { + Preconditions.checkNotNull(payload); RestApi restApi = configuration.getRestApiFactory().buildBackend(); - return restApi.get(path); + return restApi.post(Castle.URL_PRIVACY + "users", payload); } @Override - public CastleResponse post(String path, Object payload) { - RestApi restApi = configuration.getRestApiFactory().buildBackend(); - return restApi.post(path, payload); - } - - @Override - public CastleResponse put(String path) { + public CastleResponse deleteUserData(ImmutableMap payload) { + Preconditions.checkNotNull(payload); RestApi restApi = configuration.getRestApiFactory().buildBackend(); - return restApi.put(path); + return restApi.delete(Castle.URL_PRIVACY + "users", payload); } @Override - public CastleResponse put(String path, Object payload) { + public CastleResponse eventsSchema() { RestApi restApi = configuration.getRestApiFactory().buildBackend(); - return restApi.put(path, payload); + return restApi.get(Castle.URL_EVENTS + "/schema"); } @Override - public CastleResponse delete(String path) { + public CastleResponse queryEvents(ImmutableMap payload) { + Preconditions.checkNotNull(payload); RestApi restApi = configuration.getRestApiFactory().buildBackend(); - return restApi.delete(path); + return restApi.post(Castle.URL_EVENTS + "/query", payload); } @Override - public CastleResponse delete(String path, Object payload) { - RestApi restApi = configuration.getRestApiFactory().buildBackend(); - return restApi.delete(path, payload); - } - - public CastleResponse risk(ImmutableMap payload) { + public CastleResponse groupEvents(ImmutableMap payload) { Preconditions.checkNotNull(payload); RestApi restApi = configuration.getRestApiFactory().buildBackend(); - return restApi.post(Castle.URL_RISK, payload); + return restApi.post(Castle.URL_EVENTS + "/group", payload); } @Override @@ -482,55 +299,4 @@ public CastleResponse log(Log payload) { return restApi.post(Castle.URL_LOG, payload); } - @Override - public CastleResponse recover(String userId) { - Preconditions.checkNotNull(userId, "UserId can not be null"); - Preconditions.checkArgument(!userId.isEmpty()); - - RestApi restApi = configuration.getRestApiFactory().buildBackend(); - return restApi.put(String.format(Castle.URL_RECOVER, userId)); - } - - private CastleMessage buildMessage(String event, String userId, @Nullable Object properties, @Nullable Object traits) { - CastleMessage message = new CastleMessage(event); - - message.setUserId(userId); - - return setTraitsAndProperties(message, properties, traits); - } - - private CastleMessage setTraitsAndProperties(CastleMessage message, @Nullable Object properties, @Nullable Object traits) { - if (properties != null) { - JsonElement propertiesJson = configuration.getModel().getGson().toJsonTree(properties); - message.setProperties(propertiesJson); - } - - if (traits != null) { - JsonElement traitsJson = configuration.getModel().getGson().toJsonTree(traits); - message.setUserTraits(traitsJson); - } - - return message; - } - - private JsonElement buildJson(CastleMessage message) throws CastleRuntimeException { - JsonObject contextJson; - // Context can be either from the message or from the instance of this - // class. Make sure we have one - CastleContext context = message.getContext(); - if (context == null) { - contextJson = this.contextJson; - } else { - contextJson = configuration.getModel().getGson().toJsonTree(context).getAsJsonObject(); - } - - JsonElement messageJson = configuration.getModel().getGson().toJsonTree(message); - JsonObject messageObj = messageJson.getAsJsonObject(); - messageObj.add("context", contextJson); - - // Add sent_at to json - messageObj.addProperty("sent_at", Timestamp.timestamp()); - - return messageObj; - } } diff --git a/src/main/java/io/castle/client/internal/backend/OkHttpFactory.java b/src/main/java/io/castle/client/internal/backend/OkHttpFactory.java index 4b32028f..b63b7d7c 100644 --- a/src/main/java/io/castle/client/internal/backend/OkHttpFactory.java +++ b/src/main/java/io/castle/client/internal/backend/OkHttpFactory.java @@ -36,8 +36,8 @@ private OkHttpClient createOkHttpClient() { .writeTimeout(configuration.getTimeout(), TimeUnit.MILLISECONDS); if (configuration.isLogHttpRequests()) { HttpLoggingInterceptor logging = new HttpLoggingInterceptor(); - // TODO provide more configurable logging features. logging.setLevel(HttpLoggingInterceptor.Level.BODY); + logging.redactHeader("Authorization"); builder = builder.addInterceptor(logging); } @@ -61,4 +61,10 @@ public Response intercept(Chain chain) throws IOException { public RestApi buildBackend() { return new OkRestApiBackend(client, modelInstance, configuration); } + + @Override + public void close() { + client.dispatcher().executorService().shutdown(); + client.connectionPool().evictAll(); + } } diff --git a/src/main/java/io/castle/client/internal/backend/OkRestApiBackend.java b/src/main/java/io/castle/client/internal/backend/OkRestApiBackend.java index 46c77f6c..ad551005 100644 --- a/src/main/java/io/castle/client/internal/backend/OkRestApiBackend.java +++ b/src/main/java/io/castle/client/internal/backend/OkRestApiBackend.java @@ -1,12 +1,9 @@ package io.castle.client.internal.backend; import com.google.gson.*; -import io.castle.client.Castle; import io.castle.client.internal.config.CastleConfiguration; import io.castle.client.internal.json.CastleGsonModel; import io.castle.client.internal.utils.OkHttpExceptionUtil; -import io.castle.client.internal.utils.VerdictBuilder; -import io.castle.client.internal.utils.VerdictTransportModel; import io.castle.client.model.*; import okhttp3.*; @@ -25,224 +22,15 @@ public class OkRestApiBackend implements RestApi { private final CastleConfiguration configuration; private final HttpUrl baseUrl; - private final HttpUrl track; - private final HttpUrl authenticate; - private final HttpUrl deviceBase; - private final HttpUrl userBase; - private final HttpUrl impersonateBase; - private final HttpUrl privacyBase; public OkRestApiBackend(OkHttpClient client, CastleGsonModel model, CastleConfiguration configuration) { this.baseUrl = HttpUrl.parse(configuration.getApiBaseUrl()); this.client = client; this.model = model; this.configuration = configuration; - this.track = baseUrl.resolve(Castle.URL_TRACK); - this.authenticate = baseUrl.resolve(Castle.URL_AUTHENTICATE); - this.deviceBase = baseUrl.resolve(Castle.URL_DEVICES); - this.userBase = baseUrl.resolve(Castle.URL_USERS); - this.impersonateBase = baseUrl.resolve(Castle.URL_IMPERSONATE); - this.privacyBase = baseUrl.resolve(Castle.URL_PRIVACY); } @Override - public void sendTrackRequest(JsonElement payload, final AsyncCallbackHandler asyncCallbackHandler) { - RequestBody body = buildRequestBody(payload); - Request request = new Request.Builder() - .url(track) - .post(body) - .build(); - client.newCall(request).enqueue(new Callback() { - @Override - public void onFailure(Call call, IOException e) { - Castle.logger.error("HTTP layer. Error sending track request.", e); - if (asyncCallbackHandler != null) { - asyncCallbackHandler.onException(e); - } - } - - @Override - public void onResponse(Call call, Response response) throws IOException { - try (ResponseBody responseBody = response.body()) { - if (asyncCallbackHandler != null) { - asyncCallbackHandler.onResponse(response.isSuccessful()); - } - } - } - }); - } - - @Override - public Verdict sendAuthenticateSync(JsonElement payloadJson) { - final String userId = getUserIdFromPayload(payloadJson); - - RequestBody body = buildRequestBody(payloadJson); - Request request = new Request.Builder() - .url(authenticate) - .post(body) - .build(); - try (Response response = client.newCall(request).execute()) { - return extractAuthenticationAction(response, userId); - } catch (IOException e) { - Castle.logger.error("HTTP layer. Error sending request.", e); - if (configuration.getAuthenticateFailoverStrategy().isThrowTimeoutException()) { - throw OkHttpExceptionUtil.handle(e); - } else { - return VerdictBuilder.failover(e.getMessage()) - .withAction(configuration.getAuthenticateFailoverStrategy().getDefaultAction()) - .withUserId(userId) - .build(); - } - } - } - - @Override - public void sendAuthenticateAsync(JsonElement payloadJson, final AsyncCallbackHandler asyncCallbackHandler) { - final String userId = getUserIdFromPayload(payloadJson); - - RequestBody body = buildRequestBody(payloadJson); - Request request = new Request.Builder() - .url(authenticate) - .post(body) - .build(); - client.newCall(request).enqueue(new Callback() { - @Override - public void onFailure(Call call, IOException e) { - if (configuration.getAuthenticateFailoverStrategy().isThrowTimeoutException()) { - asyncCallbackHandler.onException(OkHttpExceptionUtil.handle(e)); - } else { - asyncCallbackHandler.onResponse( - VerdictBuilder.failover(e.getMessage()) - .withAction(configuration.getAuthenticateFailoverStrategy().getDefaultAction()) - .withUserId(userId) - .build() - ); - } - } - - @Override - public void onResponse(Call call, Response response) throws IOException { - try (ResponseBody responseBody = response.body()) { - asyncCallbackHandler.onResponse(extractAuthenticationAction(response, userId)); - } - } - }); - } - - private String getUserIdFromPayload(JsonElement payloadJson) { - final String userId = ((JsonObject) payloadJson).has("user_id") ? ((JsonObject) payloadJson).get("user_id").getAsString() : null; - if (userId == null) { - Castle.logger.warn("Authenticate called with user_id null. Is this correct?"); - } - return userId; - } - - private RequestBody buildRequestBody(JsonElement payloadJson) { - JsonObject json = payloadJson.getAsJsonObject(); - return RequestBody.create(json.toString(), JSON); - } - - private Verdict extractAuthenticationAction(Response response, String userId) throws IOException { - String errorReason = response.message(); - String jsonResponse = response.body().string(); - - if (response.isSuccessful()) { - Gson gson = model.getGson(); - VerdictTransportModel transport = gson.fromJson(jsonResponse, VerdictTransportModel.class); - if (transport != null && transport.getAction() != null) { - return VerdictBuilder.fromTransport(transport, JsonParser.parseString(jsonResponse)); - } else { - errorReason = "Invalid JSON in response"; - } - } - - if (response.code() >= 500) { - //Use failover for error backends calls. - if (!configuration.getAuthenticateFailoverStrategy().isThrowTimeoutException()) { - Verdict verdict = VerdictBuilder.failover(errorReason) - .withAction(configuration.getAuthenticateFailoverStrategy().getDefaultAction()) - .withUserId(userId) - .build(); - return verdict; - } else { - throw new CastleApiInternalServerErrorException(response); - } - } - - // Could not extract Verdict, so fail for client logic space. - throw new CastleRuntimeException(response); - } - - @Override - public Boolean sendPrivacyRemoveUser(String userId) { - Request request = createPrivacyRemoveRequest(userId); - try (Response response = client.newCall(request).execute()) { - return extractResponse(response); - } catch (IOException e) { - throw OkHttpExceptionUtil.handle(e); - } - } - - @Override - public CastleUserDevice sendApproveDeviceRequestSync(String deviceToken) { - Request request = createApproveDeviceRequest(deviceToken); - try (Response response = client.newCall(request).execute()) { - return extractDevice(response); - } catch (IOException e) { - throw OkHttpExceptionUtil.handle(e); - } - } - - @Override - public CastleUserDevice sendReportDeviceRequestSync(String deviceToken) { - Request request = createReportDeviceRequest(deviceToken); - try (Response response = client.newCall(request).execute()) { - return extractDevice(response); - } catch (IOException e) { - throw OkHttpExceptionUtil.handle(e); - } - } - - @Override - public CastleUserDevices sendGetUserDevicesRequestSync(String userId) { - Request request = createGetUserDevicesRequest(userId); - try (Response response = client.newCall(request).execute()) { - return extractDevices(response); - } catch (IOException e) { - throw OkHttpExceptionUtil.handle(e); - } - } - - @Override - public CastleUserDevice sendGetUserDeviceRequestSync(String deviceToken) { - Request request = createGetUserDeviceRequest(deviceToken); - try (Response response = client.newCall(request).execute()) { - return extractDevice(response); - } catch (IOException e) { - throw OkHttpExceptionUtil.handle(e); - } - } - - @Override - public CastleSuccess sendImpersonateStartRequestSync(String userId, String impersonator, JsonObject contextJson) { - Request request = createImpersonateStartRequest(userId, impersonator, contextJson); - try (Response response = client.newCall(request).execute()) { - return extractSuccess(response); - } catch (IOException e) { - throw OkHttpExceptionUtil.handle(e); - } - } - - @Override - public CastleSuccess sendImpersonateEndRequestSync(String userId, String impersonator, JsonObject contextJson) { - Request request = createImpersonateEndRequest(userId, impersonator, contextJson); - try (Response response = client.newCall(request).execute()) { - return extractSuccess(response); - } catch (IOException e) { - throw OkHttpExceptionUtil.handle(e); - } - } - public CastleResponse get(String path) { return makeRequest(path, null, METHOD_GET); } @@ -302,118 +90,6 @@ private CastleResponse makeRequest(String path, JsonElement payload, String meth } } - private CastleUserDevice extractDevice(Response response) throws IOException { - return (CastleUserDevice) extract(response, CastleUserDevice.class); - } - - private CastleUserDevices extractDevices(Response response) throws IOException { - return (CastleUserDevices) extract(response, CastleUserDevices.class); - } - - private Object extract(Response response, Class clazz) throws IOException { - if (response.isSuccessful()) { - String jsonResponse = response.body().string(); - Gson gson = model.getGson(); - return gson.fromJson(jsonResponse, clazz); - } else if (response.code() == 404) { - return null; - } - OkHttpExceptionUtil.handle(response); - return null; - } - - private CastleSuccess extractSuccess(Response response) throws IOException { - if (response.isSuccessful()) { - if (response.body() != null) { - String jsonResponse = response.body().string(); - Gson gson = model.getGson(); - return gson.fromJson(jsonResponse, CastleSuccess.class); - } - } - OkHttpExceptionUtil.handle(response); - return null; - } - - private Boolean extractResponse(Response response) { - if (response.isSuccessful()) { - return true; - } else if (response.code() == 404) { - return null; - } - OkHttpExceptionUtil.handle(response); - return false; - } - - private CastleUser extractUser(Response response) throws IOException { - return (CastleUser) extract(response, CastleUser.class); - } - - private Request createApproveDeviceRequest(String deviceToken) { - HttpUrl approveDeviceUrl = deviceBase.resolve(deviceToken + "/approve"); - return new Request.Builder() - .url(approveDeviceUrl) - .put(createEmptyRequestBody()) - .build(); - } - - private Request createReportDeviceRequest(String deviceToken) { - HttpUrl reportDeviceUrl = deviceBase.resolve(deviceToken + "/report"); - return new Request.Builder() - .url(reportDeviceUrl) - .put(createEmptyRequestBody()) - .build(); - } - - private Request createGetUserDevicesRequest(String userId) { - HttpUrl getUserDevicesUrl = userBase.resolve(userId + "/devices"); - return new Request.Builder() - .url(getUserDevicesUrl) - .get() - .build(); - } - - private Request createGetUserDeviceRequest(String deviceToken) { - HttpUrl getUserDeviceUrl = deviceBase.resolve(deviceToken); - return new Request.Builder() - .url(getUserDeviceUrl) - .get() - .build(); - } - - private Request createImpersonateStartRequest(String userId, String impersonator, JsonObject contextJson) { - HttpUrl impersonateUrl = impersonateBase; - - ImpersonatePayload payload = new ImpersonatePayload(userId, impersonator, contextJson); - - RequestBody body = RequestBody.create(model.getGson().toJson(payload), JSON); - - return new Request.Builder() - .url(impersonateUrl) - .post(body) - .build(); - } - - private Request createImpersonateEndRequest(String userId, String impersonator, JsonObject contextJson) { - HttpUrl impersonateUrl = impersonateBase; - - ImpersonatePayload payload = new ImpersonatePayload(userId, impersonator, contextJson); - - RequestBody body = RequestBody.create(model.getGson().toJson(payload), JSON); - - return new Request.Builder() - .url(impersonateUrl) - .delete(body) - .build(); - } - - private Request createPrivacyRemoveRequest(String userId) { - HttpUrl privacyRemoveUrl = privacyBase.resolve("users/" + userId); - return new Request.Builder() - .url(privacyRemoveUrl) - .delete() - .build(); - } - private RequestBody createEmptyRequestBody() { return RequestBody.create(new byte[0], null); } diff --git a/src/main/java/io/castle/client/internal/backend/RestApi.java b/src/main/java/io/castle/client/internal/backend/RestApi.java index 1c19d081..5663aca7 100644 --- a/src/main/java/io/castle/client/internal/backend/RestApi.java +++ b/src/main/java/io/castle/client/internal/backend/RestApi.java @@ -1,93 +1,11 @@ package io.castle.client.internal.backend; -import com.google.gson.JsonElement; -import com.google.gson.JsonObject; import io.castle.client.model.*; public interface RestApi { /** - * - * @param payloadJson JSON object containing the event properties - * @param asyncCallbackHandler callback to inform if request was correctly sent - */ - void sendTrackRequest(JsonElement payloadJson, AsyncCallbackHandler asyncCallbackHandler); - - /** - * - * @param payloadJson JSON object containing the event properties - * @return Verdict to be used in login logic - */ - Verdict sendAuthenticateSync(JsonElement payloadJson); - - /** - * - * @param payloadJson JSON object containing the event properties - * @param asyncCallbackHandler callback to inform if request was correctly sent - */ - void sendAuthenticateAsync(JsonElement payloadJson, AsyncCallbackHandler asyncCallbackHandler); - - /** - * Remove user from Castle (GDPR reasons) - * @see The docs - * @param userId user id to be removed - */ - Boolean sendPrivacyRemoveUser(String userId); - - /** - * Sync call to the approve device endpoint. - * - * @param deviceToken string representing the token for the device to get - * @return a {@code device} with metadata contained in the body of the response - */ - CastleUserDevice sendApproveDeviceRequestSync(String deviceToken); - - /** - * Sync call to the report device endpoint. - * - * @param deviceToken string representing the token for the device to get - * @return a {@code device} with metadata contained in the body of the response - */ - CastleUserDevice sendReportDeviceRequestSync(String deviceToken); - - /** - * Sync call to the devices endpoint. - * - * @param userId string representing the user to get devices for - * @return a {@code devices} with metadata contained in the body of the response - */ - CastleUserDevices sendGetUserDevicesRequestSync(String userId); - - /** - * Sync call to the device endpoint. - * - * @param deviceToken string representing the token for the device to get - * @return a {@code device} with metadata contained in the body of the response - */ - CastleUserDevice sendGetUserDeviceRequestSync(String deviceToken); - - /** - * Sync call to the impersonate endpoint. - * - * @param userId id of the user to impersonate - * @param impersonator id of the user doing the impersonation - * @param contextJson context json - * @return a success message - */ - CastleSuccess sendImpersonateStartRequestSync(String userId, String impersonator, JsonObject contextJson); - - /** - * Sync call to the impersonate endpoint. - * - * @param userId id of the user to stop impersonating - * @param impersonator id of the user doing the impersonation - * @param contextJson context json - * @return a success message - */ - CastleSuccess sendImpersonateEndRequestSync(String userId, String impersonator, JsonObject contextJson); - - /** - * Make a GET request to a Castle API endpoint such as /v1/{userId}/devices + * Make a GET request to a Castle API endpoint such as /v1/lists * * @param path api path * @return a decoded json response @@ -95,7 +13,7 @@ public interface RestApi { CastleResponse get(String path); /** - * Make a POST request to a Castle API endpoint such as /v1/track + * Make a POST request to a Castle API endpoint such as /v1/risk * * @param path api path * @param payload request payload @@ -104,7 +22,7 @@ public interface RestApi { CastleResponse post(String path, Object payload); /** - * Make a PUT request to a Castle API endpoint such as /v1/devices/{deviceToken}/report + * Make a PUT request to a Castle API endpoint such as /v1/lists/{id} * * @param path api path * @return a decoded json response @@ -112,7 +30,7 @@ public interface RestApi { CastleResponse put(String path); /** - * Make a PUT request to a Castle API endpoint such as /v1/devices/{deviceToken}/report + * Make a PUT request to a Castle API endpoint such as /v1/lists/{id} * * @param path api path * @param payload request payload @@ -121,7 +39,7 @@ public interface RestApi { CastleResponse put(String path, Object payload); /** - * Make a DELETE request to a Castle API endpoint such as /v1/impersonate + * Make a DELETE request to a Castle API endpoint such as /v1/lists/{id} * * @param path api path * @return a decoded json response @@ -129,7 +47,7 @@ public interface RestApi { CastleResponse delete(String path); /** - * Make a DELETE request to a Castle API endpoint such as /v1/impersonate + * Make a DELETE request to a Castle API endpoint such as /v1/lists/{id} * * @param path api path * @param payload request payload diff --git a/src/main/java/io/castle/client/internal/backend/RestApiFactory.java b/src/main/java/io/castle/client/internal/backend/RestApiFactory.java index a758b005..e01b297b 100644 --- a/src/main/java/io/castle/client/internal/backend/RestApiFactory.java +++ b/src/main/java/io/castle/client/internal/backend/RestApiFactory.java @@ -3,4 +3,6 @@ public interface RestApiFactory { RestApi buildBackend(); + + void close(); } diff --git a/src/main/java/io/castle/client/internal/config/CastleConfiguration.java b/src/main/java/io/castle/client/internal/config/CastleConfiguration.java index d4b5f73d..cd3952ea 100644 --- a/src/main/java/io/castle/client/internal/config/CastleConfiguration.java +++ b/src/main/java/io/castle/client/internal/config/CastleConfiguration.java @@ -1,7 +1,6 @@ package io.castle.client.internal.config; import io.castle.client.internal.backend.CastleBackendProvider; -import io.castle.client.model.AuthenticateFailoverStrategy; import io.castle.client.model.CastleRuntimeException; import java.util.List; @@ -20,15 +19,10 @@ public class CastleConfiguration { private final String apiBaseUrl; /** - * Timeout after which a request fails. + * Timeout in milliseconds applied to connect, read, and write. */ private final int timeout; - /** - * Strategy for returning a {@code verdict} when an authenticate call fails. - */ - private final AuthenticateFailoverStrategy authenticateFailoverStrategy; - /** * List of headers that will get passed to the {@code CastleContext} unless they are denyListed. */ @@ -66,10 +60,9 @@ public class CastleConfiguration { */ private final int maxRequests; - public CastleConfiguration(String apiBaseUrl, int timeout, AuthenticateFailoverStrategy authenticateFailoverStrategy, List allowListHeaders, List denyListHeaders, String apiSecret, String castleAppId, CastleBackendProvider backendProvider, boolean logHttpRequests, List ipHeaders, Integer maxRequests) { + public CastleConfiguration(String apiBaseUrl, int timeout, List allowListHeaders, List denyListHeaders, String apiSecret, String castleAppId, CastleBackendProvider backendProvider, boolean logHttpRequests, List ipHeaders, Integer maxRequests) { this.apiBaseUrl = apiBaseUrl; this.timeout = timeout; - this.authenticateFailoverStrategy = authenticateFailoverStrategy; this.allowListHeaders = allowListHeaders; this.denyListHeaders = denyListHeaders; this.apiSecret = apiSecret; @@ -88,10 +81,6 @@ public int getTimeout() { return timeout; } - public AuthenticateFailoverStrategy getAuthenticateFailoverStrategy() { - return authenticateFailoverStrategy; - } - public List getAllowListHeaders() { return allowListHeaders; } diff --git a/src/main/java/io/castle/client/internal/config/CastleConfigurationBuilder.java b/src/main/java/io/castle/client/internal/config/CastleConfigurationBuilder.java index ab177de1..c738f9fd 100644 --- a/src/main/java/io/castle/client/internal/config/CastleConfigurationBuilder.java +++ b/src/main/java/io/castle/client/internal/config/CastleConfigurationBuilder.java @@ -4,10 +4,9 @@ import com.google.common.collect.ImmutableList; import io.castle.client.internal.backend.CastleBackendProvider; import io.castle.client.internal.utils.HeaderNormalizer; -import io.castle.client.model.AuthenticateAction; -import io.castle.client.model.AuthenticateFailoverStrategy; import io.castle.client.model.CastleSdkConfigurationException; +import java.time.Duration; import java.util.LinkedList; import java.util.List; @@ -19,7 +18,6 @@ * The fields that can be set in a CastleConfiguration: *

    *
  • timeout - *
  • failoverStrategy *
  • allowListHeaders *
  • denyListHeaders *
  • apiSecret @@ -38,14 +36,9 @@ */ public class CastleConfigurationBuilder { /** - * Represents the milliseconds after which a request fails. + * Milliseconds applied to connect, read, and write for each request. */ - private int timeout = 500; - - /** - * Strategy used when an authenticate call to the Castle API fails. - */ - private AuthenticateFailoverStrategy failoverStrategy; + private int timeout = 1000; /** * Strings representing headers that should be passed to the context object unless they are also denyListed. @@ -110,8 +103,7 @@ public static CastleConfigurationBuilder defaultConfigBuilder() { .withDefaultAllowList() .withDefaultDenyList() .withDefaultApiBaseUrl() - .withTimeout(500) - .withDefaultAuthenticateFailoverStrategy() + .withTimeout(1000) .withDefaultBackendProvider() .withMaxRequests(5); return builder; @@ -120,7 +112,7 @@ public static CastleConfigurationBuilder defaultConfigBuilder() { /** * Provides a fresh castleConfigurationBuilder. *

    - * The only default value provided is the timeout, which is set to 500 milliseconds. + * The only default value provided is the timeout, which is set to 1000 milliseconds. * * @return a castleConfigurationBuilder with all values set to null, except timeout */ @@ -141,7 +133,7 @@ public CastleConfigurationBuilder withDefaultAllowList() { /** * Sets the default list of denyListed headers. *

    - * The default value is a list whose single element is the {@code Cookie} header. + * The default value is {@code Cookie} and {@code Authorization}. * * @return a castleConfigurationBuilder instance with the default list of denyListed headers */ @@ -153,7 +145,7 @@ public CastleConfigurationBuilder withDefaultDenyList() { } /** - * Sets the timeout in milliseconds for a request. + * Sets the connect, read, and write timeout in milliseconds for a request. * * @param timeout milliseconds after which a request times out * @return a castleConfigurationBuilder with a timeout set to a new value @@ -164,26 +156,13 @@ public CastleConfigurationBuilder withTimeout(int timeout) { } /** - * Establishes the authentication strategy that will be used in case of a timeout when performing a - * {@code CastleApi#authenticate} call. - * - * @param failoverStrategy strategy to use for failed authenticate API calls; not null. - * @return a castleConfigurationBuilder with the chosen AuthenticationStrategy set - */ - public CastleConfigurationBuilder withAuthenticateFailoverStrategy(AuthenticateFailoverStrategy failoverStrategy) { - this.failoverStrategy = failoverStrategy; - return this; - } - - /** - * Sets the failover strategy for the authenticate Castle API call to allow. - *

    - * The authenticate failover strategy for the default configuration is to return {@link AuthenticateAction#ALLOW}. + * Sets the connect, read, and write timeout for a request. * - * @return a castleConfigurationBuilder with allow as the authenticate failover strategy + * @param timeout duration after which a request times out + * @return a castleConfigurationBuilder with a timeout set to a new value */ - public CastleConfigurationBuilder withDefaultAuthenticateFailoverStrategy() { - return this.withAuthenticateFailoverStrategy(new AuthenticateFailoverStrategy(AuthenticateAction.ALLOW)); + public CastleConfigurationBuilder withTimeout(Duration timeout) { + return withTimeout(Math.toIntExact(timeout.toMillis())); } /** @@ -292,7 +271,7 @@ public CastleConfigurationBuilder apiSecret(String apiSecret) { * * @return a castleConfiguration with all fields set to some meaningful value * @throws CastleSdkConfigurationException if at least one of castleAppId, apiSecret, allowListHeaders, - * denyListHeaders, failoverStrategy, backendProvider is not provided + * denyListHeaders, backendProvider is not provided * during the building stage of the * CastleConfiguration instance. */ @@ -310,10 +289,6 @@ public CastleConfiguration build() throws CastleSdkConfigurationException { builder.add("A denyList of headers must be provided. If not sure, then use the default values provided " + "by method withDefaultDenyList. Read documentation for further details."); } - if (failoverStrategy == null) { - builder.add("A failover strategy must be provided. If not sure, then use the default values provided " + - "by method withDefaultAuthenticateFailoverStrategy. Read documentation for further details."); - } if (backendProvider == null) { builder.add("A backend provider must be selected. If not sure, then use the default values provided " + "by method withDefaultBackendProvider. Read documentation for further details."); @@ -328,7 +303,6 @@ public CastleConfiguration build() throws CastleSdkConfigurationException { HeaderNormalizer normalizer = new HeaderNormalizer(); return new CastleConfiguration(apiBaseUrl, timeout, - failoverStrategy, normalizer.normalizeList(allowListHeaders), normalizer.normalizeList(denyListHeaders), apiSecret, @@ -378,6 +352,7 @@ public CastleConfigurationBuilder withApiBaseUrl(String apiBaseUrl) { /** * Flag to enable logging on backend HTTP requests. + * When enabled, the {@code Authorization} header is redacted from the log. * * @param logHttpRequests boolean to switch logging on or off. * @return a castleConfigurationBuilder with logging setup set diff --git a/src/main/java/io/castle/client/internal/config/CastleSdkInternalConfiguration.java b/src/main/java/io/castle/client/internal/config/CastleSdkInternalConfiguration.java index 4756fd7c..58e01899 100644 --- a/src/main/java/io/castle/client/internal/config/CastleSdkInternalConfiguration.java +++ b/src/main/java/io/castle/client/internal/config/CastleSdkInternalConfiguration.java @@ -70,4 +70,8 @@ public CastleConfiguration getConfiguration() { public HashFunction getSecureHashFunction() { return Hashing.hmacSha256(sha256Key); } + + public void close() { + restApiFactory.close(); + } } diff --git a/src/main/java/io/castle/client/internal/config/ConfigurationLoader.java b/src/main/java/io/castle/client/internal/config/ConfigurationLoader.java index 30513ab0..598e769f 100644 --- a/src/main/java/io/castle/client/internal/config/ConfigurationLoader.java +++ b/src/main/java/io/castle/client/internal/config/ConfigurationLoader.java @@ -3,8 +3,6 @@ import com.google.common.base.Splitter; import io.castle.client.Castle; import io.castle.client.internal.backend.CastleBackendProvider; -import io.castle.client.model.AuthenticateAction; -import io.castle.client.model.AuthenticateFailoverStrategy; import io.castle.client.model.CastleSdkConfigurationException; import java.io.InputStream; @@ -78,8 +76,6 @@ private static String getPropertiesFilePath() { * Loads the application level configuration for the Castle SDK with values taken from environmental variables, * a properties file or default values in that order of precedence. *

    - * When wrong failover strategy or backend, null is returned instead. - * * @return a CastleConfiguration instance * @throws CastleSdkConfigurationException if at least one of apiSecret or castleAppId is not provided in either the * environment or the properties file in the classpath @@ -130,11 +126,6 @@ public CastleConfigurationBuilder loadConfigurationBuilder() { "backend_provider", "CASTLE_SDK_BACKEND_PROVIDER" ); - String authenticateFailoverStrategyValue = loadConfigurationValue( - castleConfigurationProperties, - "failover_strategy", - "CASTLE_SDK_AUTHENTICATE_FAILOVER_STRATEGY" - ); String apiBaseUrl = loadConfigurationValue( castleConfigurationProperties, "base_url", @@ -170,19 +161,6 @@ public CastleConfigurationBuilder loadConfigurationBuilder() { int timeout = Integer.parseInt(timeoutValue); builder.withTimeout(timeout); } - if (authenticateFailoverStrategyValue != null) { - if (authenticateFailoverStrategyValue.compareTo("throw") == 0) { - builder.withAuthenticateFailoverStrategy(new AuthenticateFailoverStrategy()); - } else { - builder.withAuthenticateFailoverStrategy( - new AuthenticateFailoverStrategy( - AuthenticateAction.fromAction(authenticateFailoverStrategyValue) - ) - ); - } - } else { - builder.withDefaultAuthenticateFailoverStrategy(); - } if (backendProviderValue != null) { builder.withBackendProvider( CastleBackendProvider.valueOf(backendProviderValue) diff --git a/src/main/java/io/castle/client/internal/json/AuthenticateActionDeserializer.java b/src/main/java/io/castle/client/internal/json/AuthenticateActionDeserializer.java deleted file mode 100644 index 05837ac4..00000000 --- a/src/main/java/io/castle/client/internal/json/AuthenticateActionDeserializer.java +++ /dev/null @@ -1,16 +0,0 @@ -package io.castle.client.internal.json; - -import com.google.gson.JsonDeserializationContext; -import com.google.gson.JsonDeserializer; -import com.google.gson.JsonElement; -import com.google.gson.JsonParseException; -import io.castle.client.model.AuthenticateAction; - -import java.lang.reflect.Type; - -public class AuthenticateActionDeserializer implements JsonDeserializer { - @Override - public AuthenticateAction deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException { - return AuthenticateAction.fromAction(json.getAsString()); - } -} diff --git a/src/main/java/io/castle/client/internal/json/CastleGsonModel.java b/src/main/java/io/castle/client/internal/json/CastleGsonModel.java index ff0b65a0..b25a5967 100644 --- a/src/main/java/io/castle/client/internal/json/CastleGsonModel.java +++ b/src/main/java/io/castle/client/internal/json/CastleGsonModel.java @@ -4,10 +4,8 @@ import com.google.gson.internal.bind.util.ISO8601Utils; import com.google.gson.stream.JsonReader; import com.google.gson.stream.JsonWriter; -import io.castle.client.model.AuthenticateAction; import io.castle.client.model.RiskPolicyType; import io.castle.client.model.CastleHeaders; -import io.castle.client.model.CastleMessage; import io.castle.client.model.generated.BaseChangesetEntry; import okio.ByteString; import org.threeten.bp.LocalDate; @@ -27,9 +25,7 @@ public class CastleGsonModel { public CastleGsonModel() { GsonBuilder builder = createGsonBuilder(); builder.registerTypeAdapter(CastleHeaders.class, new CastleHeadersSerializer()); - builder.registerTypeAdapter(CastleMessage.class, new CastleMessageSerializer()); builder.registerTypeAdapter(CastleHeaders.class, new CastleHeadersDeserializer()); - builder.registerTypeAdapter(AuthenticateAction.class, new AuthenticateActionDeserializer()); builder.registerTypeAdapter(RiskPolicyType.class, new RiskPolicyTypeDeserializer()); builder.registerTypeAdapterFactory(ChangesetEntryTypeAdapter.FACTORY); builder.registerTypeAdapter(BaseChangesetEntry.class, new BaseChangesetEntryDeserializer()); diff --git a/src/main/java/io/castle/client/internal/json/CastleMessageSerializer.java b/src/main/java/io/castle/client/internal/json/CastleMessageSerializer.java deleted file mode 100644 index 51d819b3..00000000 --- a/src/main/java/io/castle/client/internal/json/CastleMessageSerializer.java +++ /dev/null @@ -1,29 +0,0 @@ -package io.castle.client.internal.json; - -import com.google.gson.JsonElement; -import com.google.gson.JsonSerializationContext; -import com.google.gson.JsonSerializer; -import com.google.gson.Gson; - -import io.castle.client.internal.utils.ContextMerge; -import io.castle.client.model.CastleMessage; - -import java.lang.reflect.Type; -import java.util.HashMap; - -public class CastleMessageSerializer implements JsonSerializer { - - private final Gson gson = CastleGsonModel.createGsonBuilder().create(); - - @Override - public JsonElement serialize(CastleMessage message, Type typeOfSrc, JsonSerializationContext context) { - JsonElement root = gson.toJsonTree(message); - HashMap other = message.getOther(); - if (other == null) { - return root; - } - JsonElement otherJson = context.serialize(message.getOther()); - ContextMerge merger = new ContextMerge(); - return merger.merge(root.getAsJsonObject(), otherJson.getAsJsonObject()); - } -} diff --git a/src/main/java/io/castle/client/internal/utils/OkHttpExceptionUtil.java b/src/main/java/io/castle/client/internal/utils/OkHttpExceptionUtil.java index 730002c8..d72a0ee9 100644 --- a/src/main/java/io/castle/client/internal/utils/OkHttpExceptionUtil.java +++ b/src/main/java/io/castle/client/internal/utils/OkHttpExceptionUtil.java @@ -34,6 +34,8 @@ public static void handle(Response response) throws CastleServerErrorException { throw new CastleApiInvalidParametersException(response); case 404: throw new CastleApiNotFoundException(response); + case 402: + throw new CastleApiPaymentRequiredException(response); } throw new CastleServerErrorException(response); } diff --git a/src/main/java/io/castle/client/internal/utils/VerdictBuilder.java b/src/main/java/io/castle/client/internal/utils/VerdictBuilder.java deleted file mode 100644 index df775030..00000000 --- a/src/main/java/io/castle/client/internal/utils/VerdictBuilder.java +++ /dev/null @@ -1,97 +0,0 @@ -package io.castle.client.internal.utils; - -import com.google.gson.JsonElement; -import io.castle.client.model.AuthenticateAction; -import io.castle.client.model.RiskPolicyResult; -import io.castle.client.model.Verdict; - -public class VerdictBuilder { - private AuthenticateAction action; - private RiskPolicyResult riskPolicy; - private String userId; - private boolean failover; - private String failoverReason; - private String deviceToken; - private JsonElement internal; - private float risk; - - private VerdictBuilder() { - } - - public static VerdictBuilder success() { - return new VerdictBuilder() - .withFailover(false); - } - - public static VerdictBuilder failover(String failoverReason) { - return new VerdictBuilder() - .withFailover(true) - .withFailoverReason(failoverReason); - } - - public VerdictBuilder withAction(AuthenticateAction action) { - this.action = action; - return this; - } - - public VerdictBuilder withUserId(String userId) { - this.userId = userId; - return this; - } - - - public VerdictBuilder withRiskPolicy(RiskPolicyResult riskPolicy) { - this.riskPolicy = riskPolicy; - return this; - } - - public VerdictBuilder withDeviceToken(final String deviceToken) { - this.deviceToken = deviceToken; - return this; - } - - public VerdictBuilder withRisk(final float risk) { - this.risk = risk; - return this; - } - - public Verdict build() { - Verdict verdict = new Verdict(); - verdict.setAction(action); - verdict.setUserId(userId); - verdict.setFailover(failover); - verdict.setFailoverReason(failoverReason); - verdict.setDeviceToken(deviceToken); - verdict.setRiskPolicy(riskPolicy); - verdict.setInternal(internal); - verdict.setRisk(risk); - return verdict; - } - - public VerdictBuilder withFailover(boolean failover) { - this.failover = failover; - return this; - } - - public VerdictBuilder withFailoverReason(String failoverReason) { - this.failoverReason = failoverReason; - return this; - } - - public VerdictBuilder withInternal(JsonElement internal) { - this.internal = internal; - return this; - } - - public static Verdict fromTransport(VerdictTransportModel transport, JsonElement internal) { - internal.getAsJsonObject().get("action").getAsString(); - return success() - .withAction(transport.getAction()) - .withUserId(transport.getUserId()) - .withDeviceToken(transport.getDeviceToken()) - .withRiskPolicy(transport.getRiskPolicy()) - .withRisk(transport.getRisk()) - .withInternal(internal) - .build(); - } -} diff --git a/src/main/java/io/castle/client/internal/utils/VerdictTransportModel.java b/src/main/java/io/castle/client/internal/utils/VerdictTransportModel.java deleted file mode 100644 index 07dab519..00000000 --- a/src/main/java/io/castle/client/internal/utils/VerdictTransportModel.java +++ /dev/null @@ -1,50 +0,0 @@ -package io.castle.client.internal.utils; - -import io.castle.client.model.AuthenticateAction; -import io.castle.client.model.RiskPolicyResult; - -public class VerdictTransportModel { - - private AuthenticateAction action; - private RiskPolicyResult riskPolicy; - private String userId; - private String deviceToken; - private float risk; - - public AuthenticateAction getAction() { - return action; - } - - public void setAction(AuthenticateAction action) { - this.action = action; - } - - public RiskPolicyResult getRiskPolicy() { - return riskPolicy; - } - - public void setRiskPolicy(RiskPolicyResult riskPolicy) { - this.riskPolicy = riskPolicy; - } - - public String getUserId() { - return userId; - } - - public void setUserId(String userId) { - this.userId = userId; - } - - public String getDeviceToken() { - return deviceToken; - } - - public float getRisk() { - return risk; - } - - public void setRisk(float risk) { - this.risk = risk; - } - -} diff --git a/src/main/java/io/castle/client/internal/utils/Webhook.java b/src/main/java/io/castle/client/internal/utils/Webhook.java new file mode 100644 index 00000000..9c6ebf3c --- /dev/null +++ b/src/main/java/io/castle/client/internal/utils/Webhook.java @@ -0,0 +1,56 @@ +package io.castle.client.internal.utils; + +import com.google.common.base.Charsets; +import com.google.common.hash.Hashing; +import com.google.common.io.BaseEncoding; + +import java.security.MessageDigest; + +/** + * Helper for verifying Castle webhook signatures. + *

    + * Castle signs every webhook with an HMAC-SHA256 of the raw request body using the + * account API secret. The signature is base64 encoded and delivered in the + * {@code X-Castle-Signature} header. + */ +public final class Webhook { + + private Webhook() { + } + + /** + * Computes the base64 encoded HMAC-SHA256 signature for the given body. + * + * @param secret the account API secret + * @param body the raw request body bytes + * @return the base64 encoded signature + */ + public static String computeSignature(String secret, byte[] body) { + byte[] safeBody = body != null ? body : new byte[0]; + byte[] hmac = Hashing.hmacSha256(secret.getBytes(Charsets.UTF_8)) + .hashBytes(safeBody) + .asBytes(); + return BaseEncoding.base64().encode(hmac); + } + + /** + * Verifies that the supplied signature matches the computed signature for the body. + *

    + * The comparison is done in constant time to avoid timing attacks. + * + * @param secret the account API secret + * @param body the raw request body bytes + * @param signature the signature received in the {@code X-Castle-Signature} header + * @return {@code true} when the signature matches + */ + public static boolean verifySignature(String secret, byte[] body, String signature) { + if (secret == null || signature == null) { + return false; + } + String expected = computeSignature(secret, body); + return MessageDigest.isEqual( + expected.getBytes(Charsets.UTF_8), + signature.getBytes(Charsets.UTF_8) + ); + } +} diff --git a/src/main/java/io/castle/client/model/AsyncCallbackHandler.java b/src/main/java/io/castle/client/model/AsyncCallbackHandler.java deleted file mode 100644 index 0286db15..00000000 --- a/src/main/java/io/castle/client/model/AsyncCallbackHandler.java +++ /dev/null @@ -1,14 +0,0 @@ -package io.castle.client.model; - -/** - * Callback interface used to handle async requests responses. - *

    - * An implementation of this interface allows the user to handle async request success and failure cases in a custom manner. - * - * @param The type of the internal response after execution. - */ -public interface AsyncCallbackHandler { - void onResponse(T response); - - void onException(Exception exception); -} diff --git a/src/main/java/io/castle/client/model/AuthenticateAction.java b/src/main/java/io/castle/client/model/AuthenticateAction.java deleted file mode 100644 index 7839c701..00000000 --- a/src/main/java/io/castle/client/model/AuthenticateAction.java +++ /dev/null @@ -1,28 +0,0 @@ -package io.castle.client.model; - -/** - * Action that needs to be taken after a login attempt. - *

    - * See the documentation for the semantics of each case. - * It can be null. - * - * @see Adaptive authentication - */ -public enum AuthenticateAction { - ALLOW, DENY, CHALLENGE; - - /** - * Returns an AuthenticateAction from a string representing its name. - * - * @param action string representing the name of the AuthenticateAction, case-insensitive - * @return the enum value matching the name, or null if it does not match any enum - */ - public static AuthenticateAction fromAction(String action) { - for (AuthenticateAction kind : AuthenticateAction.class.getEnumConstants()) { - if (kind.name().equalsIgnoreCase(action)) { - return kind; - } - } - return null; - } -} diff --git a/src/main/java/io/castle/client/model/AuthenticateFailoverStrategy.java b/src/main/java/io/castle/client/model/AuthenticateFailoverStrategy.java deleted file mode 100644 index 294b4600..00000000 --- a/src/main/java/io/castle/client/model/AuthenticateFailoverStrategy.java +++ /dev/null @@ -1,57 +0,0 @@ -package io.castle.client.model; - -import io.castle.client.api.CastleApi; - -/** - * Stores settings for either using an AuthenticationAction or throwing a TimeoutException, - * if a Castle API authenticate call fails. - *

    - * If a {@link CastleApi#authenticate} call fails during an authentication attempt, the failover strategy specifies - * which {@link AuthenticateAction} should be used. - * Alternatively, a TimeoutException instance could be configured. - */ -public class AuthenticateFailoverStrategy { - - private final AuthenticateAction defaultAction; - private final boolean throwTimeoutException; - - private AuthenticateFailoverStrategy(AuthenticateAction defaultAction, boolean throwTimeoutException) { - this.defaultAction = defaultAction; - this.throwTimeoutException = throwTimeoutException; - } - - /** - * Sets the default authenticationAction for failed requests. - * - * @param action the authenticateAction that will be used as a default for failed requests - */ - public AuthenticateFailoverStrategy(AuthenticateAction action) { - this(action, false); - } - - /** - * Creates an authenticationStrategy whose policy is to throw a timeoutException. - */ - public AuthenticateFailoverStrategy() { - this(null, true); - } - - - /** - * Gets the authenticateAction that this strategy uses for failover. - * - * @return the authenticateAction configured as default; null if there is no default AuthenticateAction - */ - public AuthenticateAction getDefaultAction() { - return defaultAction; - } - - /** - * Checks whether the failover strategy is configured to throw a timeoutException. - * - * @return true if the failover strategy is set to throw a timeoutException, false otherwise - */ - public boolean isThrowTimeoutException() { - return throwTimeoutException; - } -} diff --git a/src/main/java/io/castle/client/model/CastleApiPaymentRequiredException.java b/src/main/java/io/castle/client/model/CastleApiPaymentRequiredException.java new file mode 100644 index 00000000..9ef6258d --- /dev/null +++ b/src/main/java/io/castle/client/model/CastleApiPaymentRequiredException.java @@ -0,0 +1,9 @@ +package io.castle.client.model; + +import okhttp3.Response; + +public class CastleApiPaymentRequiredException extends CastleServerErrorException { + public CastleApiPaymentRequiredException(Response response) { + super(response); + } +} diff --git a/src/main/java/io/castle/client/model/CastleMessage.java b/src/main/java/io/castle/client/model/CastleMessage.java deleted file mode 100644 index ec7ba32d..00000000 --- a/src/main/java/io/castle/client/model/CastleMessage.java +++ /dev/null @@ -1,198 +0,0 @@ -package io.castle.client.model; - -import java.util.HashMap; - -import javax.annotation.Nonnull; - -public class CastleMessage { - private transient CastleContext context; - private String createdAt; - private String timestamp; - private String deviceToken; - @Nonnull private String event; - /** - * Collect other attributes that haven't not explicit setters to accommodate for - * future parameters - */ - private transient HashMap other; - private Object properties; - private String reviewId; - private String userId; - private Object userTraits; - - /** - * Initialize a new request payload message - * @param event Name of the event to send - */ - public CastleMessage(String event) { - setEvent(event); - } - - public CastleContext getContext() { - return context; - } - - public void setContext(CastleContext context) { - this.context = context; - } - - /** - * @deprecated use {@link #getTimestamp()} instead. - */ - @Deprecated - public String getCreatedAt() { - return createdAt; - } - - /** - * @deprecated use {@link #setTimestamp(String)} instead. - */ - @Deprecated - public void setCreatedAt(String createdAt) { - this.createdAt = createdAt; - } - - public String getTimestamp() { - return timestamp; - } - - public void setTimestamp(String timestamp) { - this.timestamp = timestamp; - } - - public String getDeviceToken() { - return deviceToken; - } - - public void setDeviceToken(String deviceToken) { - this.deviceToken = deviceToken; - } - - @Nonnull - public String getEvent() { - return event; - } - - public void setEvent(@Nonnull String event) { - this.event = event; - } - - public HashMap getOther() { - if (other == null) { - this.other = new HashMap(); - } - return other; - } - - public void setOther(HashMap other) { - this.other = other; - } - - public Object getProperties() { - return properties; - } - - public void setProperties(Object properties) { - this.properties = properties; - } - - public String getReviewId() { - return reviewId; - } - - public void setReviewId(String reviewId) { - this.reviewId = reviewId; - } - - public String getUserId() { - return userId; - } - - public void setUserId(String userId) { - this.userId = userId; - } - - public Object getUserTraits() { - return userTraits; - } - - public void setUserTraits(Object userTraits) { - this.userTraits = userTraits; - } - - public static Builder builder(String event) { - return new Builder(new CastleMessage(event)); - } - - public static class Builder { - private CastleMessage payload; - - public Builder(CastleMessage payload) { - this.payload = payload; - } - - public CastleMessage build() { - return payload; - } - - public Builder context(CastleContext context) { - payload.setContext(context); - return this; - } - - /** - * @deprecated use {@link #timestamp(String)} instead. - */ - @Deprecated - public Builder createdAt(String createdAt) { - payload.setCreatedAt(createdAt); - return this; - } - - public Builder timestamp(String timestamp) { - payload.setTimestamp(timestamp); - return this; - } - - public Builder deviceToken(String deviceToken) { - payload.setDeviceToken(deviceToken); - return this; - } - - public Builder reviewId(String reviewId) { - payload.setReviewId(reviewId); - return this; - } - - public Builder properties(Object properties) { - if (properties == null) { - throw new NullPointerException("Null properties"); - } - payload.setProperties(properties); - return this; - } - - public Builder other(HashMap other) { - payload.setOther(other); - return this; - } - - public Builder put(String key, Object value) { - payload.getOther().put(key, value); - return this; - } - - public Builder userId(String userId) { - payload.setUserId(userId); - return this; - } - - public Builder userTraits(Object userTraits) { - if (userTraits == null) { - throw new NullPointerException("Null userTraits"); - } - payload.setUserTraits(userTraits); - return this; - } - } -} diff --git a/src/main/java/io/castle/client/model/CastleResponse.java b/src/main/java/io/castle/client/model/CastleResponse.java index 2d2b3794..150bdc17 100644 --- a/src/main/java/io/castle/client/model/CastleResponse.java +++ b/src/main/java/io/castle/client/model/CastleResponse.java @@ -1,12 +1,18 @@ package io.castle.client.model; import com.google.gson.JsonElement; +import com.google.gson.JsonNull; import com.google.gson.JsonParser; import io.castle.client.internal.utils.OkHttpExceptionUtil; import okhttp3.Response; +import okhttp3.ResponseBody; import java.io.IOException; +/** + * Parsed HTTP response from the Castle API. + * An empty or missing body is represented as JSON null. + */ public class CastleResponse { private final int code; private final JsonElement json; @@ -15,8 +21,9 @@ public CastleResponse(Response response) throws IOException { OkHttpExceptionUtil.handle(response); code = response.code(); - String body = response.body().string(); - json = JsonParser.parseString(body); + ResponseBody responseBody = response.body(); + String body = responseBody != null ? responseBody.string() : ""; + json = body.isBlank() ? JsonNull.INSTANCE : JsonParser.parseString(body); } public boolean isSuccessful() { diff --git a/src/main/java/io/castle/client/model/CastleRuntimeException.java b/src/main/java/io/castle/client/model/CastleRuntimeException.java index 226fc588..9330dd56 100644 --- a/src/main/java/io/castle/client/model/CastleRuntimeException.java +++ b/src/main/java/io/castle/client/model/CastleRuntimeException.java @@ -3,8 +3,7 @@ import okhttp3.Response; /** - * Exception thrown if the client is configured to use a THROW - * {@code AuthenticateFailoverStrategy} and if a call to the authenticate endpoint of the Castle API fails. + * Unchecked exception thrown when a Castle API call fails. */ public class CastleRuntimeException extends RuntimeException { diff --git a/src/main/java/io/castle/client/model/CastleSuccess.java b/src/main/java/io/castle/client/model/CastleSuccess.java deleted file mode 100644 index 060999d0..00000000 --- a/src/main/java/io/castle/client/model/CastleSuccess.java +++ /dev/null @@ -1,13 +0,0 @@ -package io.castle.client.model; - -public class CastleSuccess { - boolean success; - - public boolean isSuccess() { - return success; - } - - public void setSuccess(boolean success) { - this.success = success; - } -} diff --git a/src/main/java/io/castle/client/model/CastleUserDevice.java b/src/main/java/io/castle/client/model/CastleUserDevice.java deleted file mode 100644 index 6e751272..00000000 --- a/src/main/java/io/castle/client/model/CastleUserDevice.java +++ /dev/null @@ -1,89 +0,0 @@ -package io.castle.client.model; - -/** - * Model of the device object returned in the body of the response of a device call to the Castle Api. - */ -public class CastleUserDevice { - private String token; - private double risk; - private String createdAt; - private String lastSeenAt; - private String approvedAt; - private String escalatedAt; - private String mitigatedAt; - private CastleUserDeviceContext context; - - boolean isCurrentDevice; - - public String getToken() { - return token; - } - - public void setToken(String token) { - this.token = token; - } - - public double getRisk() { - return risk; - } - - public void setRisk(double risk) { - this.risk = risk; - } - - public String getCreatedAt() { - return createdAt; - } - - public void setCreatedAt(String createdAt) { - this.createdAt = createdAt; - } - - public String getLastSeenAt() { - return lastSeenAt; - } - - public void setLastSeenAt(String lastSeenAt) { - this.lastSeenAt = lastSeenAt; - } - - public String getApprovedAt() { - return approvedAt; - } - - public void setApprovedAt(String approvedAt) { - this.approvedAt = approvedAt; - } - - public String getEscalatedAt() { - return escalatedAt; - } - - public void setEscalatedAt(String escalatedAt) { - this.escalatedAt = escalatedAt; - } - - public String getMitigatedAt() { - return mitigatedAt; - } - - public void setMitigatedAt(String mitigatedAt) { - this.mitigatedAt = mitigatedAt; - } - - public CastleUserDeviceContext getContext() { - return context; - } - - public void setContext(CastleUserDeviceContext context) { - this.context = context; - } - - public boolean isCurrentDevice() { - return isCurrentDevice; - } - - public void setCurrentDevice(boolean currentDevice) { - isCurrentDevice = currentDevice; - } -} diff --git a/src/main/java/io/castle/client/model/CastleUserDeviceContext.java b/src/main/java/io/castle/client/model/CastleUserDeviceContext.java deleted file mode 100644 index 91c806e7..00000000 --- a/src/main/java/io/castle/client/model/CastleUserDeviceContext.java +++ /dev/null @@ -1,31 +0,0 @@ -package io.castle.client.model; - -public class CastleUserDeviceContext { - private String ip; - private DeviceUserAgent userAgent; - private String type; - - public String getIp() { - return ip; - } - - public void setIp(String ip) { - this.ip = ip; - } - - public DeviceUserAgent getUserAgent() { - return userAgent; - } - - public void setUserAgent(DeviceUserAgent userAgent) { - this.userAgent = userAgent; - } - - public String getType() { - return type; - } - - public void setType(String type) { - this.type = type; - } -} diff --git a/src/main/java/io/castle/client/model/CastleUserDevices.java b/src/main/java/io/castle/client/model/CastleUserDevices.java deleted file mode 100644 index fbc9399d..00000000 --- a/src/main/java/io/castle/client/model/CastleUserDevices.java +++ /dev/null @@ -1,24 +0,0 @@ -package io.castle.client.model; - -import com.google.gson.annotations.SerializedName; - -import java.util.List; - -public class CastleUserDevices { - private int totalCount; - @SerializedName("data") - private List devices; - - public int getTotalCount() { - return totalCount; - } - - public List getDevices() { - return devices; - } - - public void setDevices(List devices) { - this.devices = devices; - this.totalCount = devices != null ? devices.size() : 0; - } -} diff --git a/src/main/java/io/castle/client/model/DeviceUserAgent.java b/src/main/java/io/castle/client/model/DeviceUserAgent.java deleted file mode 100644 index b92c6573..00000000 --- a/src/main/java/io/castle/client/model/DeviceUserAgent.java +++ /dev/null @@ -1,76 +0,0 @@ -package io.castle.client.model; - -public class DeviceUserAgent { - private String raw; - private String browser; - private String version; - private String os; - private boolean mobile; - private String platform; - private String device; - private String family; - - public String getRaw() { - return raw; - } - - public void setRaw(String raw) { - this.raw = raw; - } - - public String getBrowser() { - return browser; - } - - public void setBrowser(String browser) { - this.browser = browser; - } - - public String getVersion() { - return version; - } - - public void setVersion(String version) { - this.version = version; - } - - public String getOs() { - return os; - } - - public void setOs(String os) { - this.os = os; - } - - public boolean isMobile() { - return mobile; - } - - public void setMobile(boolean mobile) { - this.mobile = mobile; - } - - public String getPlatform() { - return platform; - } - - public void setPlatform(String platform) { - this.platform = platform; - } - - public String getDevice() { - return device; - } - - public void setDevice(String device) { - this.device = device; - } - - public String getFamily() { - return family; - } - - public void setFamily(String family) { - this.family = family; - } -} diff --git a/src/main/java/io/castle/client/model/ImpersonatePayload.java b/src/main/java/io/castle/client/model/ImpersonatePayload.java deleted file mode 100644 index 161fe525..00000000 --- a/src/main/java/io/castle/client/model/ImpersonatePayload.java +++ /dev/null @@ -1,22 +0,0 @@ -package io.castle.client.model; - -import com.google.gson.JsonObject; - -public class ImpersonatePayload { - private final String userId; - private final JsonObject context; - private final JsonObject properties; - - public ImpersonatePayload(String userId, String impersonator, JsonObject contextJson) { - this.userId = userId; - this.properties = new JsonObject(); - this.properties.addProperty("impersonator", impersonator); - this.context = contextJson; - } - - public ImpersonatePayload(String userId, JsonObject contextJson) { - this.userId = userId; - this.properties = new JsonObject(); - this.context = contextJson; - } -} diff --git a/src/main/java/io/castle/client/model/Verdict.java b/src/main/java/io/castle/client/model/Verdict.java deleted file mode 100644 index 8e29681e..00000000 --- a/src/main/java/io/castle/client/model/Verdict.java +++ /dev/null @@ -1,113 +0,0 @@ -package io.castle.client.model; - -import com.google.gson.JsonElement; - -/** - * Model of the outcome of an authenticate call to the Castle API. - */ -public class Verdict { - - /** - * AuthenticateAction returned by a call to the CastleAPI, or configured by a failover strategy. - */ - private AuthenticateAction action; - - /** - * String representing a user ID associated with an authenticate call. - */ - private String userId; - - /** - * True if the SDK resorted the {@code AuthenticateFailoverStrategy} configured. - */ - private boolean failover; - - /** - * Explains the reason why the {@code AuthenticateFailoverStrategy} was used. - */ - private String failoverReason; - - /** - * String representing a device ID associated with an authenticate call. - */ - private String deviceToken; - - /** - * Float representing risk value associated with an authenticate call. - */ - private float risk; - - /** - * RiskPolicyResult representing risk policy used for generating this verdict. - */ - private RiskPolicyResult riskPolicy; - - /** - * JsonElement representing the full response of the server request - */ - private JsonElement internal; - - public AuthenticateAction getAction() { - return action; - } - - public void setAction(AuthenticateAction action) { - this.action = action; - } - - public String getUserId() { - return userId; - } - - public void setUserId(String userId) { - this.userId = userId; - } - - public boolean isFailover() { - return failover; - } - - public void setFailover(boolean failover) { - this.failover = failover; - } - - public String getFailoverReason() { - return failoverReason; - } - - public void setFailoverReason(String failoverReason) { - this.failoverReason = failoverReason; - } - - public String getDeviceToken() { - return deviceToken; - } - - public void setRisk(float risk) { - this.risk = risk; - } - - public float getRisk() { - return risk; - } - - public void setDeviceToken(String deviceToken) { - this.deviceToken = deviceToken; - } - - public void setInternal(JsonElement internal) { - this.internal = internal; - } - - public RiskPolicyResult getRiskPolicy() { - return riskPolicy; - } - - public void setRiskPolicy(RiskPolicyResult riskPolicy) { - this.riskPolicy = riskPolicy; - } - - public JsonElement getInternal() { - return internal; - } -} diff --git a/src/main/java/io/castle/client/model/generated/BaseList.java b/src/main/java/io/castle/client/model/generated/BaseList.java index d6dc4aa8..b512229b 100755 --- a/src/main/java/io/castle/client/model/generated/BaseList.java +++ b/src/main/java/io/castle/client/model/generated/BaseList.java @@ -103,7 +103,7 @@ public BaseList defaultItemArchivationTime(Integer defaultItemArchivationTime) { * @return defaultItemArchivationTime **/ @ApiModelProperty(example = "2592000", value = "Default time, in seconds, after which Items in this List automatically gets archived after they are created. It is possible to override this value when creating the individual List Items") - public int getDefaultItemArchivationTime() { + public Integer getDefaultItemArchivationTime() { return defaultItemArchivationTime; } diff --git a/src/main/java/io/castle/client/model/generated/DeviceScreen.java b/src/main/java/io/castle/client/model/generated/DeviceScreen.java index f25da37e..f1d14138 100755 --- a/src/main/java/io/castle/client/model/generated/DeviceScreen.java +++ b/src/main/java/io/castle/client/model/generated/DeviceScreen.java @@ -39,7 +39,7 @@ public DeviceScreen screen(Integer screen) { * @return screen **/ @ApiModelProperty(example = "2", value = "") - public int getScreen() { + public Integer getScreen() { return screen; } diff --git a/src/main/java/io/castle/client/model/generated/IP.java b/src/main/java/io/castle/client/model/generated/IP.java index 2202660a..ce650978 100755 --- a/src/main/java/io/castle/client/model/generated/IP.java +++ b/src/main/java/io/castle/client/model/generated/IP.java @@ -51,7 +51,7 @@ public IP asn(Integer asn) { * @return asn **/ @ApiModelProperty(example = "14618", required = true, value = "") - public int getAsn() { + public Integer getAsn() { return asn; } diff --git a/src/main/java/io/castle/client/model/generated/ListItemsBatchResponse.java b/src/main/java/io/castle/client/model/generated/ListItemsBatchResponse.java index c1b3d1ad..32e0e972 100755 --- a/src/main/java/io/castle/client/model/generated/ListItemsBatchResponse.java +++ b/src/main/java/io/castle/client/model/generated/ListItemsBatchResponse.java @@ -52,7 +52,7 @@ public ListItemsBatchResponse totalReceived(Integer totalReceived) { * @return totalReceived **/ @ApiModelProperty(required = true, value = "Total number of items received") - public int getTotalReceived() { + public Integer getTotalReceived() { return totalReceived; } @@ -70,7 +70,7 @@ public ListItemsBatchResponse totalProcessed(int totalProcessed) { * @return totalProcessed **/ @ApiModelProperty(required = true, value = "Total number of items processed without any errors") - public int getTotalProcessed() { + public Integer getTotalProcessed() { return totalProcessed; } @@ -88,7 +88,7 @@ public ListItemsBatchResponse created(int created) { * @return created **/ @ApiModelProperty(value = "Number of items created") - public int getCreated() { + public Integer getCreated() { return created; } @@ -106,7 +106,7 @@ public ListItemsBatchResponse updated(int updated) { * @return updated **/ @ApiModelProperty(value = "Number of items updated") - public int getUpdated() { + public Integer getUpdated() { return updated; } @@ -124,7 +124,7 @@ public ListItemsBatchResponse replaced(int replaced) { * @return replaced **/ @ApiModelProperty(value = "Total number of items replaced") - public int getReplaced() { + public Integer getReplaced() { return replaced; } @@ -142,7 +142,7 @@ public ListItemsBatchResponse errored(int errored) { * @return errored **/ @ApiModelProperty(value = "Total number of items that errored") - public int getErrored() { + public Integer getErrored() { return errored; } diff --git a/src/test/java/io/castle/client/AbstractCastleHttpLayerTest.java b/src/test/java/io/castle/client/AbstractCastleHttpLayerTest.java index 23303633..4a7f4a5e 100644 --- a/src/test/java/io/castle/client/AbstractCastleHttpLayerTest.java +++ b/src/test/java/io/castle/client/AbstractCastleHttpLayerTest.java @@ -1,41 +1,31 @@ package io.castle.client; -import io.castle.client.internal.backend.OkHttpFactory; import io.castle.client.internal.config.CastleConfiguration; import io.castle.client.internal.config.CastleConfigurationBuilder; import io.castle.client.internal.config.CastleSdkInternalConfiguration; -import io.castle.client.model.AuthenticateFailoverStrategy; import io.castle.client.model.CastleSdkConfigurationException; import okhttp3.HttpUrl; import okhttp3.mockwebserver.MockWebServer; -import org.assertj.core.api.Assertions; import org.junit.After; import org.junit.Before; import java.io.IOException; import java.net.InetAddress; -import java.util.concurrent.atomic.AtomicReference; public abstract class AbstractCastleHttpLayerTest { - private final AuthenticateFailoverStrategy testAuthenticateFailoverStrategy; - Castle sdk; MockWebServer server; HttpUrl testServerBaseUrl; - protected AbstractCastleHttpLayerTest(AuthenticateFailoverStrategy testAuthenticateFailoverStrategy) { - this.testAuthenticateFailoverStrategy = testAuthenticateFailoverStrategy; - } - @Before public void prepare() throws NoSuchFieldException, IllegalAccessException, CastleSdkConfigurationException, IOException { //Given a mocked API server server = new MockWebServer(); server.start(InetAddress.getByName("127.0.0.1"),0); //Given a SDK instance - sdk = new Castle(CastleSdkInternalConfiguration.getInternalConfiguration()); - CastleConfiguration configuration = sdk.getInternalConfiguration().getConfiguration(); + Castle loaded = new Castle(CastleSdkInternalConfiguration.getInternalConfiguration()); + CastleConfiguration configuration = loaded.getInternalConfiguration().getConfiguration(); testServerBaseUrl = server.url("/"); CastleConfiguration mockedApiConfiguration = CastleConfigurationBuilder.aConfigBuilder() .withApiSecret(configuration.getApiSecret()) @@ -45,36 +35,18 @@ public void prepare() throws NoSuchFieldException, IllegalAccessException, Castl .withAllowListHeaders(configuration.getAllowListHeaders()) .withCastleAppId(configuration.getCastleAppId()) .withBackendProvider(configuration.getBackendProvider()) - .withAuthenticateFailoverStrategy(testAuthenticateFailoverStrategy) .withTimeout(configuration.getTimeout()) .build(); + loaded.close(); sdk = Castle.initialize(mockedApiConfiguration); } @After public void tearDown() throws Exception { - server.shutdown(); - } - - - protected void waitForValueAndVerify(AtomicReference result, T expected) { - T extracted = waitForValue(result); - Assertions.assertThat(extracted).usingRecursiveComparison().isEqualTo(expected); - } - - protected T waitForValue(AtomicReference result) { - T value = result.get(); - int maxNrOfSleeps = 10; - while (value == null && maxNrOfSleeps > 0) { - maxNrOfSleeps = maxNrOfSleeps - 1; - try { - Thread.sleep(50); - } catch (InterruptedException e) { - } - value = result.get(); + if (sdk != null) { + sdk.close(); } - Assertions.assertThat(value).isNotNull(); - return value; + server.shutdown(); } } diff --git a/src/test/java/io/castle/client/CastleAuthenticateHttpTest.java b/src/test/java/io/castle/client/CastleAuthenticateHttpTest.java deleted file mode 100644 index 4e57ce44..00000000 --- a/src/test/java/io/castle/client/CastleAuthenticateHttpTest.java +++ /dev/null @@ -1,541 +0,0 @@ -package io.castle.client; - -import com.google.gson.JsonParser; -import io.castle.client.internal.utils.VerdictBuilder; -import io.castle.client.model.*; -import io.castle.client.internal.json.CastleGsonModel; -import io.castle.client.utils.SDKVersion; -import okhttp3.mockwebserver.MockResponse; -import okhttp3.mockwebserver.RecordedRequest; -import org.assertj.core.api.Assertions; -import org.json.JSONException; -import org.json.JSONObject; -import org.junit.Assert; -import org.junit.Test; -import org.skyscreamer.jsonassert.JSONAssert; -import org.springframework.mock.web.MockHttpServletRequest; - -import com.google.common.collect.ImmutableMap; - -import jakarta.servlet.http.HttpServletRequest; -import java.util.concurrent.atomic.AtomicReference; - -import static okhttp3.mockwebserver.SocketPolicy.NO_RESPONSE; - -public class CastleAuthenticateHttpTest extends AbstractCastleHttpLayerTest { - - private static final String DENY_RESPONSE = "{\n" + - " \"action\": \"deny\",\n" + - " \"user_id\": \"12345\",\n" + - " \"device_token\": \"abcdefg1234\",\n" + - " \"risk_policy\": {\n" + - " \"id\": \"q-rbeMzBTdW2Fd09sbz55A\",\n" + - " \"revision_id\": \"pke4zqO2TnqVr-NHJOAHEg\",\n" + - " \"name\": \"Block Users from X\",\n" + - " \"type\": \"bot\"\n" + - " }\n" + - "}"; - - private static final String DENY_RESPONSE_NO_POLICY = "{\n" + - " \"action\": \"deny\",\n" + - " \"user_id\": \"12345\",\n" + - " \"device_token\": \"abcdefg1234\"\n" + - "}"; - - public CastleAuthenticateHttpTest() { - super(new AuthenticateFailoverStrategy(AuthenticateAction.CHALLENGE)); - } - - @Test - public void authenticateAsyncEndpointWithNullUserIdPayload() throws InterruptedException, JSONException { - // Given - server.enqueue(new MockResponse().setBody(DENY_RESPONSE)); - - // And a mock Request - HttpServletRequest request = new MockHttpServletRequest(); - final AtomicReference result = new AtomicReference<>(); - AsyncCallbackHandler handler = new AsyncCallbackHandler() { - @Override - public void onResponse(Verdict response) { - result.set(response); - } - - @Override - public void onException(Exception exception) { - Assertions.fail("error on request", exception); - } - }; - // and an authenticate request is made - sdk.onRequest(request).authenticateAsync( - CastleMessage.builder("$login.succeeded").build(), - handler - ); - - // then - RecordedRequest recordedRequest = server.takeRequest(); - String body = recordedRequest.getBody().readUtf8(); - JSONAssert.assertEquals("{\"event\":\"$login.succeeded\",\"context\":{\"active\":true,\"ip\":\"127.0.0.1\",\"headers\":{\"REMOTE_ADDR\":\"127.0.0.1\"}," + SDKVersion.getLibraryString() +"}}", - body, false); - - Assert.assertTrue(new JSONObject(body).has("sent_at")); - } - - public void authenticateAsyncEndpontWithPayload() throws InterruptedException, JSONException { - // Given - server.enqueue(new MockResponse().setBody(DENY_RESPONSE)); - - // And a mock Request - HttpServletRequest request = new MockHttpServletRequest(); - final AtomicReference result = new AtomicReference<>(); - AsyncCallbackHandler handler = new AsyncCallbackHandler() { - @Override - public void onResponse(Verdict response) { - result.set(response); - } - - @Override - public void onException(Exception exception) { - Assertions.fail("error on request", exception); - } - }; - // and an authenticate request is made - sdk.onRequest(request).authenticateAsync( - CastleMessage.builder("$login.succeeded").userId("12345").build(), - handler - ); - - // then - RecordedRequest recordedRequest = server.takeRequest(); - String body = recordedRequest.getBody().readUtf8(); - JSONAssert.assertEquals("{\"event\":\"$login.succeeded\",\"user_id\":\"12345\",\"context\":{\"active\":true,\"ip\":\"127.0.0.1\",\"headers\":{\"REMOTE_ADDR\":\"127.0.0.1\"}," + SDKVersion.getLibraryString() +"}}", - body, false); - - Assert.assertTrue(new JSONObject(body).has("sent_at")); - } - - @Test - public void authenticationAsyncEndpointTest() throws InterruptedException, JSONException { - //given - server.enqueue(new MockResponse().setBody(DENY_RESPONSE)); - String id = "12345"; - String event = "$login.succeeded"; - String deviceToken = "abcdefg1234"; - - // And a mock Request - HttpServletRequest request = new MockHttpServletRequest(); - final AtomicReference result = new AtomicReference<>(); - AsyncCallbackHandler handler = new AsyncCallbackHandler() { - @Override - public void onResponse(Verdict response) { - result.set(response); - } - - @Override - public void onException(Exception exception) { - Assertions.fail("error on request", exception); - } - }; - // and an authenticate request is made - sdk.onRequest(request).authenticateAsync(event, id, handler); - - // then - RecordedRequest recordedRequest = server.takeRequest(); - String body = recordedRequest.getBody().readUtf8(); - String json = "{\"event\":\"$login.succeeded\",\"user_id\":\"12345\",\"context\":{\"active\":true,\"ip\":\"127.0.0.1\",\"headers\":{\"REMOTE_ADDR\":\"127.0.0.1\"}," + SDKVersion.getLibraryString() +"}}"; - - JSONAssert.assertEquals(json, body, false); - - Assert.assertTrue(new JSONObject(body).has("sent_at")); - - RiskPolicyResult riskPolicyResult = new CastleGsonModel().getGson().fromJson("{\"id\": \"q-rbeMzBTdW2Fd09sbz55A\", \"revision_id\": \"pke4zqO2TnqVr-NHJOAHEg\",\"name\": \"Block Users from X\",\"type\": \"bot\"}", RiskPolicyResult.class); - - // and - Verdict expected = VerdictBuilder.success() - .withAction(AuthenticateAction.DENY) - .withUserId("12345") - .withDeviceToken(deviceToken) - .withRiskPolicy(riskPolicyResult) - .withInternal(JsonParser.parseString("{\"action\":\"deny\",\"user_id\":\"12345\",\"device_token\":\"abcdefg1234\", \"risk_policy\": {\"id\": \"q-rbeMzBTdW2Fd09sbz55A\", \"revision_id\": \"pke4zqO2TnqVr-NHJOAHEg\",\"name\": \"Block Users from X\",\"type\": \"bot\"}}")) - .build(); - waitForValueAndVerify(result, expected); - } - - @Test - public void authenticationAsyncEndpointNoRiskPolicyTest() throws InterruptedException, JSONException { - //given - server.enqueue(new MockResponse().setBody(DENY_RESPONSE_NO_POLICY)); - String id = "12345"; - String event = "$login.succeeded"; - String deviceToken = "abcdefg1234"; - - // And a mock Request - HttpServletRequest request = new MockHttpServletRequest(); - final AtomicReference result = new AtomicReference<>(); - AsyncCallbackHandler handler = new AsyncCallbackHandler() { - @Override - public void onResponse(Verdict response) { - result.set(response); - } - - @Override - public void onException(Exception exception) { - Assertions.fail("error on request", exception); - } - }; - // and an authenticate request is made - sdk.onRequest(request).authenticateAsync(event, id, handler); - - // then - RecordedRequest recordedRequest = server.takeRequest(); - String body = recordedRequest.getBody().readUtf8(); - String json = "{\"event\":\"$login.succeeded\",\"user_id\":\"12345\",\"context\":{\"active\":true,\"ip\":\"127.0.0.1\",\"headers\":{\"REMOTE_ADDR\":\"127.0.0.1\"}," + SDKVersion.getLibraryString() +"}}"; - - JSONAssert.assertEquals(json, body, false); - - Assert.assertTrue(new JSONObject(body).has("sent_at")); - - // and - Verdict expected = VerdictBuilder.success() - .withAction(AuthenticateAction.DENY) - .withUserId("12345") - .withDeviceToken(deviceToken) - .withInternal(JsonParser.parseString("{\"action\":\"deny\",\"user_id\":\"12345\",\"device_token\":\"abcdefg1234\"}")) - .build(); - waitForValueAndVerify(result, expected); - } - - @Test - public void authenticationAsyncEndpointWithPropertiesAndTraitsTest() throws InterruptedException, JSONException { - //given - server.enqueue(new MockResponse().setBody(DENY_RESPONSE)); - String id = "12345"; - String event = "$login.succeeded"; - String deviceToken = "abcdefg1234"; - - // And a mock Request - HttpServletRequest request = new MockHttpServletRequest(); - final AtomicReference result = new AtomicReference<>(); - AsyncCallbackHandler handler = new AsyncCallbackHandler() { - @Override - public void onResponse(Verdict response) { - result.set(response); - } - - @Override - public void onException(Exception exception) { - Assertions.fail("error on request", exception); - } - }; - // and an authenticate request is made - sdk.onRequest(request).authenticateAsync(event, id, ImmutableMap.builder() - .put("b",0) - .build(), ImmutableMap.builder() - .put("y",0) - .build(), handler); - - // then - RecordedRequest recordedRequest = server.takeRequest(); - String body = recordedRequest.getBody().readUtf8(); - JSONAssert.assertEquals("{\"event\":\"$login.succeeded\",\"properties\":{\"b\":0},\"user_id\":\"12345\",\"user_traits\":{\"y\":0},\"context\":{\"active\":true,\"ip\":\"127.0.0.1\",\"headers\":{\"REMOTE_ADDR\":\"127.0.0.1\"}," + SDKVersion.getLibraryString() +"}}", - body, false); - - Assert.assertTrue(new JSONObject(body).has("sent_at")); - - RiskPolicyResult riskPolicyResult = new CastleGsonModel().getGson().fromJson("{\"id\": \"q-rbeMzBTdW2Fd09sbz55A\", \"revision_id\": \"pke4zqO2TnqVr-NHJOAHEg\",\"name\": \"Block Users from X\",\"type\": \"bot\"}", RiskPolicyResult.class); - - // and - Verdict expected = VerdictBuilder.success() - .withAction(AuthenticateAction.DENY) - .withUserId("12345") - .withDeviceToken(deviceToken) - .withRiskPolicy(riskPolicyResult) - .withInternal(JsonParser.parseString("{\"action\":\"deny\",\"user_id\":\"12345\",\"device_token\":\"abcdefg1234\", \"risk_policy\": {\"id\": \"q-rbeMzBTdW2Fd09sbz55A\", \"revision_id\": \"pke4zqO2TnqVr-NHJOAHEg\",\"name\": \"Block Users from X\",\"type\": \"bot\"}}")) - .build(); - waitForValueAndVerify(result, expected); - } - - - @Test - public void authenticationAsyncEndpointProvideDefaultValueOnTimeoutErrorTest() throws InterruptedException, JSONException { - //given - server.enqueue(new MockResponse().setSocketPolicy(NO_RESPONSE)); - String id = "12345"; - String event = "$login.succeeded"; - - // And a mock Request - HttpServletRequest request = new MockHttpServletRequest(); - final AtomicReference result = new AtomicReference<>(); - AsyncCallbackHandler handler = new AsyncCallbackHandler() { - @Override - public void onResponse(Verdict response) { - result.set(response); - } - - @Override - public void onException(Exception exception) { - Assertions.fail("error on request", exception); - } - }; - // and an authenticate request is made - sdk.onRequest(request).authenticateAsync(event, id, handler); - - // then - RecordedRequest recordedRequest = server.takeRequest(); - String body = recordedRequest.getBody().readUtf8(); - JSONAssert.assertEquals("{\"event\":\"$login.succeeded\",\"user_id\":\"12345\",\"context\":{\"active\":true,\"ip\":\"127.0.0.1\",\"headers\":{\"REMOTE_ADDR\":\"127.0.0.1\"}," + SDKVersion.getLibraryString() +"}}", - body, false); - - Assert.assertTrue(new JSONObject(body).has("sent_at")); - - // and - Verdict expected = VerdictBuilder.failover("timeout") - .withAction(AuthenticateAction.CHALLENGE) - .withUserId(id) - .build(); - - verifyFailoverResponse(result, expected, false); - } - - - @Test - public void authenticationEndpointDefaultValueOnTimeoutErrorTest() throws InterruptedException, JSONException { - //given the backed will timeout - server.enqueue(new MockResponse().setSocketPolicy(NO_RESPONSE)); - String id = "12345"; - String event = "$login.succeeded"; - - // And a mock Request - HttpServletRequest request = new MockHttpServletRequest(); - - // and an authenticate request is made - Verdict verdict = sdk.onRequest(request).authenticate(event, id); - - // then - Verdict expected = VerdictBuilder.failover("timeout") - .withAction(AuthenticateAction.CHALLENGE) - .withUserId(id) - .build(); - verifyFailoverResponse(verdict, expected, false); - - // and - RecordedRequest recordedRequest = server.takeRequest(); - String body = recordedRequest.getBody().readUtf8(); - JSONAssert.assertEquals("{\"event\":\"$login.succeeded\",\"user_id\":\"12345\",\"context\":{\"active\":true,\"ip\":\"127.0.0.1\",\"headers\":{\"REMOTE_ADDR\":\"127.0.0.1\"}," + SDKVersion.getLibraryString() +"}}", - body, false); - - Assert.assertTrue(new JSONObject(body).has("sent_at")); - } - - @Test - public void authenticationEndpointWithPayload() throws InterruptedException, JSONException { - // Given - server.enqueue(new MockResponse().setBody(DENY_RESPONSE)); - - // And a mock Request - HttpServletRequest request = new MockHttpServletRequest(); - - // And an authenticate request is made - Verdict verdict = sdk.onRequest(request).authenticate( - CastleMessage.builder("$login.succeeded").userId("12345").build() - ); - - // Then - RecordedRequest recordedRequest = server.takeRequest(); - String body = recordedRequest.getBody().readUtf8(); - JSONAssert.assertEquals("{\"event\":\"$login.succeeded\",\"user_id\":\"12345\",\"context\":{\"active\":true,\"ip\":\"127.0.0.1\",\"headers\":{\"REMOTE_ADDR\":\"127.0.0.1\"}," + SDKVersion.getLibraryString() +"}}", - body, false); - - Assert.assertTrue(new JSONObject(body).has("sent_at")); - } - - @Test - public void authenticationEndpointTest() throws InterruptedException, JSONException { - //given - server.enqueue(new MockResponse().setBody(DENY_RESPONSE)); - String id = "12345"; - String event = "$login.succeeded"; - String deviceToken = "abcdefg1234"; - - // And a mock Request - HttpServletRequest request = new MockHttpServletRequest(); - - // and an authenticate request is made - Verdict verdict = sdk.onRequest(request).authenticate(event, id); - - RiskPolicyResult riskPolicyResult = new CastleGsonModel().getGson().fromJson("{\"id\": \"q-rbeMzBTdW2Fd09sbz55A\", \"revision_id\": \"pke4zqO2TnqVr-NHJOAHEg\",\"name\": \"Block Users from X\",\"type\": \"bot\"}", RiskPolicyResult.class); - - // then - Verdict expected = VerdictBuilder.success() - .withAction(AuthenticateAction.DENY) - .withUserId(id) - .withDeviceToken(deviceToken) - .withRiskPolicy(riskPolicyResult) - .withInternal(JsonParser.parseString("{\"action\":\"deny\",\"user_id\":\"12345\",\"device_token\":\"abcdefg1234\", \"risk_policy\": {\"id\": \"q-rbeMzBTdW2Fd09sbz55A\", \"revision_id\": \"pke4zqO2TnqVr-NHJOAHEg\",\"name\": \"Block Users from X\",\"type\": \"bot\"}}")) - .build(); - Assertions.assertThat(verdict).usingRecursiveComparison().isEqualTo(expected); - - // and - RecordedRequest recordedRequest = server.takeRequest(); - String body = recordedRequest.getBody().readUtf8(); - JSONAssert.assertEquals("{\"event\":\"$login.succeeded\",\"user_id\":\"12345\",\"context\":{\"active\":true,\"ip\":\"127.0.0.1\",\"headers\":{\"REMOTE_ADDR\":\"127.0.0.1\"}," + SDKVersion.getLibraryString() +"}}", - body, false); - - Assert.assertTrue(new JSONObject(body).has("sent_at")); - } - - @Test - public void authenticationEndpointWithPropertiesAndTraitsTest() throws InterruptedException, JSONException { - //given - server.enqueue(new MockResponse().setBody(DENY_RESPONSE)); - String id = "12345"; - String event = "$login.succeeded"; - String deviceToken = "abcdefg1234"; - - // And a mock Request - HttpServletRequest request = new MockHttpServletRequest(); - - // and an authenticate request is made - Verdict verdict = sdk.onRequest(request).authenticate(event, id, ImmutableMap.builder() - .put("a","valueA") - .put("b",123456) - .build(), ImmutableMap.builder() - .put("x","valueX") - .put("y",654321) - .build()); - - RiskPolicyResult riskPolicyResult = new CastleGsonModel().getGson().fromJson("{\"id\": \"q-rbeMzBTdW2Fd09sbz55A\", \"revision_id\": \"pke4zqO2TnqVr-NHJOAHEg\",\"name\": \"Block Users from X\",\"type\": \"bot\"}", RiskPolicyResult.class); - - // then - Verdict expected = VerdictBuilder.success() - .withAction(AuthenticateAction.DENY) - .withUserId(id) - .withDeviceToken(deviceToken) - .withRiskPolicy(riskPolicyResult) - .withInternal(JsonParser.parseString("{\"action\":\"deny\",\"user_id\":\"12345\",\"device_token\":\"abcdefg1234\", \"risk_policy\": {\"id\": \"q-rbeMzBTdW2Fd09sbz55A\", \"revision_id\": \"pke4zqO2TnqVr-NHJOAHEg\",\"name\": \"Block Users from X\",\"type\": \"bot\"}}")) - .build(); - Assertions.assertThat(verdict).usingRecursiveComparison().isEqualTo(expected); - - // and - RecordedRequest recordedRequest = server.takeRequest(); - String body = recordedRequest.getBody().readUtf8(); - JSONAssert.assertEquals("{\"event\":\"$login.succeeded\",\"properties\":{\"a\":\"valueA\",\"b\":123456},\"user_id\":\"12345\",\"user_traits\":{\"x\":\"valueX\",\"y\":654321},\"context\":{\"active\":true,\"ip\":\"127.0.0.1\",\"headers\":{\"REMOTE_ADDR\":\"127.0.0.1\"}," + SDKVersion.getLibraryString() +"}}", - body, false); - - Assert.assertTrue(new JSONObject(body).has("sent_at")); - } - - @Test(expected = CastleRuntimeException.class) - public void authenticationThrowExceptionWhenBackendReturnErrors() throws InterruptedException { - //given a 403 response from backend - server.enqueue(new MockResponse().setResponseCode(403)); - String id = "12345"; - String event = "$login.succeeded"; - - // And a mock Request - HttpServletRequest request = new MockHttpServletRequest(); - - // and an authenticate request is made - Verdict verdict = sdk.onRequest(request).authenticate(event, id, ImmutableMap.builder() - .put("a","valueA") - .put("b",123456) - .build(), null); - - // then a exception is send to the client code because of bad response from the castle backend - } - - @Test - public void authenticationUseDefaultOnBackendErrorTest() throws InterruptedException, JSONException { - //given a 403 response from backend - server.enqueue(new MockResponse().setResponseCode(500)); - String id = "12345"; - String event = "$login.succeeded"; - - // And a mock Request - HttpServletRequest request = new MockHttpServletRequest(); - - // and an authenticate request is made - Verdict verdict = sdk.onRequest(request).authenticate(event, id, ImmutableMap.builder() - .put("a","valueA") - .put("b",123456) - .build(), null); - - // then - Verdict expected = VerdictBuilder.failover("Client Error") - .withAction(AuthenticateAction.CHALLENGE) - .withUserId(id) - .build(); - verifyFailoverResponse(verdict, expected, false); - - // and - RecordedRequest recordedRequest = server.takeRequest(); - String body = recordedRequest.getBody().readUtf8(); - JSONAssert.assertEquals("{\"event\":\"$login.succeeded\",\"properties\":{\"a\":\"valueA\",\"b\":123456},\"user_id\":\"12345\",\"context\":{\"active\":true,\"ip\":\"127.0.0.1\",\"headers\":{\"REMOTE_ADDR\":\"127.0.0.1\"}," + SDKVersion.getLibraryString() +"}}", - body, false); - - Assert.assertTrue(new JSONObject(body).has("sent_at")); - } - - @Test(expected = CastleRuntimeException.class) - public void authenticationUseDefaultOnBadResponseFormatTestEmptyCase() throws InterruptedException { - //given a response do not match the transport contract - testIlegalJsonForAuthenticate("{}"); - - } - @Test(expected = CastleRuntimeException.class) - public void authenticationUseDefaultOnBadResponseFormatTestInvalidActionUseCase() throws InterruptedException { - //given a response do not match the transport contract - testIlegalJsonForAuthenticate("{\"action\":\"action\"}"); - - } - - private void testIlegalJsonForAuthenticate(String illegalJsonBody) { - server.enqueue(new MockResponse().setBody(illegalJsonBody)); - String id = "12345"; - String event = "$login.succeeded"; - - // And a mock Request - HttpServletRequest request = new MockHttpServletRequest(); - - // and an authenticate request is made - Verdict verdict = sdk.onRequest(request).authenticate(event, id, ImmutableMap.builder() - .put("a","valueA") - .put("b",123456) - .build(), null); - - // then a illegal json failover is provided - Verdict expected = VerdictBuilder.failover("Illegal json format") - .withAction(AuthenticateAction.CHALLENGE) - .withUserId(id) - .build(); - verifyFailoverResponse(verdict, expected, true); - } - - /** - * Verify that the async Verdict value match the expected values. - * The Failover reason depend from the JVM implementation, so we only check that is not null. - * - * @param result - * @param expected - */ - private void verifyFailoverResponse(AtomicReference result, Verdict expected, boolean expectedExactReasonMatch) { - Verdict extractedVerdict = waitForValue(result); - verifyFailoverResponse(extractedVerdict, expected, expectedExactReasonMatch); - } - - /** - * Sync version of the verification for Verdict - * - * @param extractedVerdict - * @param expected - */ - private void verifyFailoverResponse(Verdict extractedVerdict, Verdict expected, boolean expectedExactReasonMatch) { - Assertions.assertThat(extractedVerdict.getAction()).isEqualTo(expected.getAction()); - Assertions.assertThat(extractedVerdict.getUserId()).isEqualTo(expected.getUserId()); - Assertions.assertThat(extractedVerdict.isFailover()).isTrue(); - if (expectedExactReasonMatch) { - Assertions.assertThat(extractedVerdict.getFailoverReason()).isEqualTo(expected.getFailoverReason()); - } else { - Assertions.assertThat(extractedVerdict.getFailoverReason()).isNotEmpty(); - } - } -} diff --git a/src/test/java/io/castle/client/CastleDeviceApproveHttpTest.java b/src/test/java/io/castle/client/CastleDeviceApproveHttpTest.java deleted file mode 100644 index 6809d281..00000000 --- a/src/test/java/io/castle/client/CastleDeviceApproveHttpTest.java +++ /dev/null @@ -1,69 +0,0 @@ -package io.castle.client; - -import io.castle.client.model.*; -import io.castle.client.utils.DeviceUtils; -import okhttp3.mockwebserver.MockResponse; -import okhttp3.mockwebserver.RecordedRequest; -import org.assertj.core.api.Assertions; -import org.junit.Assert; -import org.junit.Test; -import org.springframework.mock.web.MockHttpServletRequest; - -import jakarta.servlet.http.HttpServletRequest; - -import static okhttp3.mockwebserver.SocketPolicy.NO_RESPONSE; - -public class CastleDeviceApproveHttpTest extends AbstractCastleHttpLayerTest { - - public CastleDeviceApproveHttpTest() { - super(new AuthenticateFailoverStrategy(AuthenticateAction.CHALLENGE)); - } - - @Test public void approveDevice() throws InterruptedException { - MockResponse mockResponse = new MockResponse(); - mockResponse.setBody(DeviceUtils.getDefaultDeviceJSON()); - server.enqueue(mockResponse); - - // And a mock Request - HttpServletRequest request = new MockHttpServletRequest(); - - // And an report device request is made - CastleUserDevice device = sdk.onRequest(request).approve("deviceToken"); - - // Then - RecordedRequest recordedRequest = server.takeRequest(); - Assert.assertEquals(testServerBaseUrl.resolve("v1/devices/deviceToken/approve"), recordedRequest.getRequestUrl()); - Assert.assertEquals("PUT", recordedRequest.getMethod()); - - // and the correct CastleUserDevice model object is extracted - CastleUserDevice expected = DeviceUtils.createExpectedDevice(); - - Assertions.assertThat(device).usingRecursiveComparison().isEqualTo(expected); - } - - @Test(expected = CastleApiTimeoutException.class) - public void approveDeviceTimeoutTest() { - - // given backend request timeouts - server.enqueue(new MockResponse().setSocketPolicy(NO_RESPONSE)); - HttpServletRequest request = new MockHttpServletRequest(); - String deviceToken = "deviceToken"; - - //when a v1/devices/:device_token/approve request is made - sdk.onRequest(request).approve(deviceToken); - } - - @Test(expected = CastleRuntimeException.class) - public void testExceptionWithServerError () { - - // given a server failure - server.enqueue(new MockResponse().setResponseCode(500)); - // And a request - HttpServletRequest request = new MockHttpServletRequest(); - String deviceToken = "deviceToken"; - - //when a v1/devices/:device_token/approve request is made - sdk.onRequest(request).approve(deviceToken); - - } -} diff --git a/src/test/java/io/castle/client/CastleDeviceHttpTest.java b/src/test/java/io/castle/client/CastleDeviceHttpTest.java deleted file mode 100644 index 8ac91f25..00000000 --- a/src/test/java/io/castle/client/CastleDeviceHttpTest.java +++ /dev/null @@ -1,84 +0,0 @@ -package io.castle.client; - -import io.castle.client.internal.json.CastleGsonModel; -import io.castle.client.model.*; -import io.castle.client.utils.DeviceUtils; -import okhttp3.mockwebserver.MockResponse; -import okhttp3.mockwebserver.RecordedRequest; -import org.assertj.core.api.Assertions; -import org.junit.Assert; -import org.junit.Test; -import org.springframework.mock.web.MockHttpServletRequest; - -import jakarta.servlet.http.HttpServletRequest; - -import static okhttp3.mockwebserver.SocketPolicy.NO_RESPONSE; - -public class CastleDeviceHttpTest extends AbstractCastleHttpLayerTest { - - public CastleDeviceHttpTest() { - super(new AuthenticateFailoverStrategy(AuthenticateAction.CHALLENGE)); - } - - @Test public void getDevice() throws Exception { - MockResponse mockResponse = new MockResponse(); - mockResponse.setBody(DeviceUtils.getDefaultDeviceJSON()); - server.enqueue(mockResponse); - - // And a mock Request - HttpServletRequest request = new MockHttpServletRequest(); - - // And an devices request is made - CastleUserDevice device = sdk.onRequest(request).device("deviceToken"); - - // Then - RecordedRequest recordedRequest = server.takeRequest(); - Assert.assertEquals(testServerBaseUrl.resolve("v1/devices/deviceToken"), recordedRequest.getRequestUrl()); - - // and the correct CastleUserDevice model object is extracted - CastleUserDevice expected = DeviceUtils.createExpectedDevice(); - - Assertions.assertThat(device).usingRecursiveComparison().isEqualTo(expected); - } - - @Test(expected = CastleApiTimeoutException.class) - public void getDeviceTimeoutTest() { - - // given backend request timeouts - server.enqueue(new MockResponse().setSocketPolicy(NO_RESPONSE)); - HttpServletRequest request = new MockHttpServletRequest(); - String deviceToken = "deviceToken"; - - //when a v1/devices/:device_token request is made - sdk.onRequest(request).device(deviceToken); - } - - @Test(expected = CastleRuntimeException.class) - public void testExceptionWithServerError () { - - // given a server failure - server.enqueue(new MockResponse().setResponseCode(500)); - // And a request - HttpServletRequest request = new MockHttpServletRequest(); - String deviceToken = "deviceToken"; - - //when a v1/devices/:device_token request is made - sdk.onRequest(request).device(deviceToken); - - } - - @Test - public void getDeviceNotFoundTest () { - - // given a server failure - server.enqueue(new MockResponse().setResponseCode(404)); - // And a request - HttpServletRequest request = new MockHttpServletRequest(); - String deviceToken = "deviceToken"; - - //when a v1/devices/:device_token request is made - CastleUserDevice userDevice = sdk.onRequest(request).device(deviceToken); - - Assert.assertNull(userDevice); - } -} diff --git a/src/test/java/io/castle/client/CastleDeviceReportHttpTest.java b/src/test/java/io/castle/client/CastleDeviceReportHttpTest.java deleted file mode 100644 index d927a4cb..00000000 --- a/src/test/java/io/castle/client/CastleDeviceReportHttpTest.java +++ /dev/null @@ -1,69 +0,0 @@ -package io.castle.client; - -import io.castle.client.model.*; -import io.castle.client.utils.DeviceUtils; -import okhttp3.mockwebserver.MockResponse; -import okhttp3.mockwebserver.RecordedRequest; -import org.assertj.core.api.Assertions; -import org.junit.Assert; -import org.junit.Test; -import org.springframework.mock.web.MockHttpServletRequest; - -import jakarta.servlet.http.HttpServletRequest; - -import static okhttp3.mockwebserver.SocketPolicy.NO_RESPONSE; - -public class CastleDeviceReportHttpTest extends AbstractCastleHttpLayerTest { - - public CastleDeviceReportHttpTest() { - super(new AuthenticateFailoverStrategy(AuthenticateAction.CHALLENGE)); - } - - @Test public void reportDevice() throws InterruptedException { - MockResponse mockResponse = new MockResponse(); - mockResponse.setBody(DeviceUtils.getDefaultDeviceJSON()); - server.enqueue(mockResponse); - - // And a mock Request - HttpServletRequest request = new MockHttpServletRequest(); - - // And an report device request is made - CastleUserDevice device = sdk.onRequest(request).report("deviceToken"); - - // Then - RecordedRequest recordedRequest = server.takeRequest(); - Assert.assertEquals(testServerBaseUrl.resolve("v1/devices/deviceToken/report"), recordedRequest.getRequestUrl()); - Assert.assertEquals("PUT", recordedRequest.getMethod()); - - // and the correct CastleUserDevice model object is extracted - CastleUserDevice expected = DeviceUtils.createExpectedDevice(); - - Assertions.assertThat(device).usingRecursiveComparison().isEqualTo(expected); - } - - @Test(expected = CastleApiTimeoutException.class) - public void reportDeviceTimeoutTest() { - - // given backend request timeouts - server.enqueue(new MockResponse().setSocketPolicy(NO_RESPONSE)); - HttpServletRequest request = new MockHttpServletRequest(); - String deviceToken = "deviceToken"; - - //when a v1/devices/:device_token/report request is made - sdk.onRequest(request).report(deviceToken); - } - - @Test(expected = CastleRuntimeException.class) - public void testExceptionWithServerError () { - - // given a server failure - server.enqueue(new MockResponse().setResponseCode(500)); - // And a request - HttpServletRequest request = new MockHttpServletRequest(); - String deviceToken = "deviceToken"; - - //when a v1/devices/:device_token/report request is made - sdk.onRequest(request).report(deviceToken); - - } -} diff --git a/src/test/java/io/castle/client/CastleDevicesHttpTest.java b/src/test/java/io/castle/client/CastleDevicesHttpTest.java deleted file mode 100644 index 68be962d..00000000 --- a/src/test/java/io/castle/client/CastleDevicesHttpTest.java +++ /dev/null @@ -1,86 +0,0 @@ -package io.castle.client; - -import io.castle.client.internal.json.CastleGsonModel; -import io.castle.client.model.*; -import io.castle.client.utils.DeviceUtils; -import okhttp3.mockwebserver.MockResponse; -import okhttp3.mockwebserver.RecordedRequest; -import org.assertj.core.api.Assertions; -import org.junit.Assert; -import org.junit.Test; -import org.springframework.mock.web.MockHttpServletRequest; - -import jakarta.servlet.http.HttpServletRequest; - -import static okhttp3.mockwebserver.SocketPolicy.NO_RESPONSE; - -public class CastleDevicesHttpTest extends AbstractCastleHttpLayerTest { - - private CastleGsonModel model = new CastleGsonModel(); - - public CastleDevicesHttpTest() { - super(new AuthenticateFailoverStrategy(AuthenticateAction.CHALLENGE)); - } - - @Test public void getDevices() throws Exception { - MockResponse mockResponse = new MockResponse(); - mockResponse.setBody("{\"total_count\":1,\"data\":[" + DeviceUtils.getDefaultDeviceJSON() + "]}"); - server.enqueue(mockResponse); - - // And a mock Request - HttpServletRequest request = new MockHttpServletRequest(); - - // And an devices request is made - CastleUserDevices devices = sdk.onRequest(request).userDevices("userId"); - - // Then - RecordedRequest recordedRequest = server.takeRequest(); - Assert.assertEquals(testServerBaseUrl.resolve("v1/users/userId/devices"), recordedRequest.getRequestUrl()); - - // and the correct CastleUserDevice model object is extracted - CastleUserDevices expected = DeviceUtils.createExpectedDevices(); - - Assertions.assertThat(devices).usingRecursiveComparison().isEqualTo(expected); - } - - @Test(expected = CastleApiTimeoutException.class) - public void getDevicesTimeoutTest() { - - // given backend request timeouts - server.enqueue(new MockResponse().setSocketPolicy(NO_RESPONSE)); - HttpServletRequest request = new MockHttpServletRequest(); - String userId = "userId"; - - //when a v1/users/:user_id/devices request is made - sdk.onRequest(request).userDevices(userId); - } - - @Test(expected = CastleRuntimeException.class) - public void testExceptionWithServerError () { - - // given a server failure - server.enqueue(new MockResponse().setResponseCode(500)); - // And a request - HttpServletRequest request = new MockHttpServletRequest(); - String userId = "userId"; - - //when a v1/users/:user_id/devices request is made - sdk.onRequest(request).userDevices(userId); - - } - - @Test - public void getDevicesNotFoundTest () { - - // given a server failure - server.enqueue(new MockResponse().setResponseCode(404)); - // And a request - HttpServletRequest request = new MockHttpServletRequest(); - String userId = "userId"; - - //when a v1/users/:user_id/devices request is made - CastleUserDevices userDevices = sdk.onRequest(request).userDevices(userId); - - Assert.assertNull(userDevices); - } -} diff --git a/src/test/java/io/castle/client/CastleEventsHttpTest.java b/src/test/java/io/castle/client/CastleEventsHttpTest.java new file mode 100644 index 00000000..5c3894e0 --- /dev/null +++ b/src/test/java/io/castle/client/CastleEventsHttpTest.java @@ -0,0 +1,70 @@ +package io.castle.client; + +import com.google.common.collect.ImmutableMap; +import io.castle.client.model.CastleResponse; +import okhttp3.mockwebserver.MockResponse; +import okhttp3.mockwebserver.RecordedRequest; +import org.assertj.core.api.Assertions; +import org.json.JSONException; +import org.junit.Assert; +import org.junit.Test; +import org.skyscreamer.jsonassert.JSONAssert; +import org.springframework.mock.web.MockHttpServletRequest; + +import jakarta.servlet.http.HttpServletRequest; + +public class CastleEventsHttpTest extends AbstractCastleHttpLayerTest { + + @Test + public void eventsSchema() throws InterruptedException { + server.enqueue(new MockResponse().setBody("{}")); + HttpServletRequest request = new MockHttpServletRequest(); + + CastleResponse response = sdk.onRequest(request).eventsSchema(); + if (response == null) { + Assertions.fail("error on request"); + } + + RecordedRequest recordedRequest = server.takeRequest(); + Assert.assertEquals(testServerBaseUrl.resolve("v1/events/schema"), recordedRequest.getRequestUrl()); + Assert.assertEquals("GET", recordedRequest.getMethod()); + } + + @Test + public void queryEvents() throws InterruptedException, JSONException { + server.enqueue(new MockResponse().setBody("{}")); + HttpServletRequest request = new MockHttpServletRequest(); + + CastleResponse response = sdk.onRequest(request).queryEvents(ImmutableMap.builder() + .put("type", "$login") + .build()); + if (response == null) { + Assertions.fail("error on request"); + } + + RecordedRequest recordedRequest = server.takeRequest(); + String body = recordedRequest.getBody().readUtf8(); + Assert.assertEquals(testServerBaseUrl.resolve("v1/events/query"), recordedRequest.getRequestUrl()); + Assert.assertEquals("POST", recordedRequest.getMethod()); + JSONAssert.assertEquals("{\"type\":\"$login\"}", body, false); + } + + @Test + public void groupEvents() throws InterruptedException, JSONException { + server.enqueue(new MockResponse().setBody("{}")); + HttpServletRequest request = new MockHttpServletRequest(); + + CastleResponse response = sdk.onRequest(request).groupEvents(ImmutableMap.builder() + .put("field", "name") + .build()); + if (response == null) { + Assertions.fail("error on request"); + } + + RecordedRequest recordedRequest = server.takeRequest(); + String body = recordedRequest.getBody().readUtf8(); + Assert.assertEquals(testServerBaseUrl.resolve("v1/events/group"), recordedRequest.getRequestUrl()); + Assert.assertEquals("POST", recordedRequest.getMethod()); + JSONAssert.assertEquals("{\"field\":\"name\"}", body, false); + } +} diff --git a/src/test/java/io/castle/client/CastleExceptionTest.java b/src/test/java/io/castle/client/CastleExceptionTest.java index 04665ecb..a4fb1aa9 100644 --- a/src/test/java/io/castle/client/CastleExceptionTest.java +++ b/src/test/java/io/castle/client/CastleExceptionTest.java @@ -120,6 +120,51 @@ public void invalidJsonError() { OkHttpExceptionUtil.handle(response); } + @Test(expected = CastleApiPaymentRequiredException.class) + public void paymentRequiredError() { + //Given + Response response = new Response.Builder() + .code(402) + .request(new Request.Builder().url("http://localhost").build()) + .protocol(Protocol.HTTP_1_1) + .message("Payment Required") + .body(ResponseBody.create("{\"type\":\"credit_exhausted\",\"message\":\"Included usage credit for this billing period has been used up\"}", JsonMediaType)) + .build(); + + OkHttpExceptionUtil.handle(response); + } + + @Test + public void emptyBodyIsAccepted() throws IOException { + Response response = new Response.Builder() + .code(204) + .request(new Request.Builder().url("http://localhost").build()) + .protocol(Protocol.HTTP_1_1) + .message("No Content") + .body(ResponseBody.create("", JsonMediaType)) + .build(); + + CastleResponse castleResponse = new CastleResponse(response); + + Assert.assertTrue(castleResponse.isSuccessful()); + Assert.assertTrue(castleResponse.json().isJsonNull()); + } + + @Test + public void missingBodyIsAccepted() throws IOException { + Response response = new Response.Builder() + .code(204) + .request(new Request.Builder().url("http://localhost").build()) + .protocol(Protocol.HTTP_1_1) + .message("No Content") + .build(); + + CastleResponse castleResponse = new CastleResponse(response); + + Assert.assertTrue(castleResponse.isSuccessful()); + Assert.assertTrue(castleResponse.json().isJsonNull()); + } + @Test(expected = CastleApiNotFoundException.class) public void notFoundError() { //Given diff --git a/src/test/java/io/castle/client/CastleFilterHttpTest.java b/src/test/java/io/castle/client/CastleFilterHttpTest.java index 8c2fd85e..1950089c 100644 --- a/src/test/java/io/castle/client/CastleFilterHttpTest.java +++ b/src/test/java/io/castle/client/CastleFilterHttpTest.java @@ -2,8 +2,6 @@ import com.google.gson.JsonParser; import io.castle.client.internal.json.CastleGsonModel; -import io.castle.client.model.AuthenticateAction; -import io.castle.client.model.AuthenticateFailoverStrategy; import io.castle.client.model.generated.*; import jakarta.servlet.http.HttpServletRequest; import okhttp3.mockwebserver.MockResponse; @@ -18,10 +16,6 @@ public class CastleFilterHttpTest extends AbstractCastleHttpLayerTest { - public CastleFilterHttpTest() { - super(new AuthenticateFailoverStrategy(AuthenticateAction.CHALLENGE)); - } - @Test public void filter() throws InterruptedException { MockResponse mockResponse = new MockResponse(); mockResponse.setBody("{\n" + @@ -242,7 +236,7 @@ public CastleFilterHttpTest() { Assert.assertEquals("en", response.getDevice().getSoftware().getLanguages().get(1)); Assert.assertEquals("91.0.4472", response.getDevice().getSoftware().getVersion().getFull()); Assert.assertEquals("91", response.getDevice().getSoftware().getVersion().getMajor()); - Assert.assertEquals(2, response.getDevice().getScreen().getScreen()); + Assert.assertEquals(Integer.valueOf(2), response.getDevice().getScreen().getScreen()); Assert.assertEquals("landscape", response.getDevice().getScreen().getOrientation()); Assert.assertEquals("America/New_York", response.getDevice().getTimezone().getName()); Assert.assertEquals(-300, response.getDevice().getTimezone().getOffset()); @@ -270,7 +264,7 @@ public CastleFilterHttpTest() { Assert.assertFalse(response.getEmail().getDomainDetails().getDmarcRecord().isExists()); Assert.assertFalse(response.getEmail().getDomainDetails().getMxRecords().isNullMx()); Assert.assertEquals("/v1/risk", response.getEndpoint()); - Assert.assertEquals(14618, response.getIp().getAsn()); + Assert.assertEquals(Integer.valueOf(14618), response.getIp().getAsn()); Assert.assertEquals("Ashburn", response.getIp().getLocation().getCity()); Assert.assertEquals("NA", response.getIp().getLocation().getContinentCode()); Assert.assertEquals("US", response.getIp().getLocation().getCountryCode()); diff --git a/src/test/java/io/castle/client/CastleGenericAPIHttpTest.java b/src/test/java/io/castle/client/CastleGenericAPIHttpTest.java index a510c887..ebcb67ea 100644 --- a/src/test/java/io/castle/client/CastleGenericAPIHttpTest.java +++ b/src/test/java/io/castle/client/CastleGenericAPIHttpTest.java @@ -1,14 +1,8 @@ package io.castle.client; import com.google.common.collect.ImmutableMap; -import com.google.gson.JsonElement; -import com.google.gson.JsonParser; -import io.castle.client.internal.json.CastleGsonModel; -import io.castle.client.internal.utils.CastleContextBuilder; import io.castle.client.internal.utils.Timestamp; -import io.castle.client.internal.utils.VerdictBuilder; import io.castle.client.model.*; -import io.castle.client.utils.DeviceUtils; import io.castle.client.utils.SDKVersion; import okhttp3.mockwebserver.MockResponse; import okhttp3.mockwebserver.RecordedRequest; @@ -21,9 +15,6 @@ import org.springframework.mock.web.MockHttpServletRequest; import jakarta.servlet.http.HttpServletRequest; -import java.util.concurrent.atomic.AtomicReference; - -import static okhttp3.mockwebserver.SocketPolicy.NO_RESPONSE; public class CastleGenericAPIHttpTest extends AbstractCastleHttpLayerTest { @@ -39,10 +30,6 @@ public class CastleGenericAPIHttpTest extends AbstractCastleHttpLayerTest { " }\n" + "}"; - public CastleGenericAPIHttpTest() { - super(new AuthenticateFailoverStrategy(AuthenticateAction.CHALLENGE)); - } - @Test public void postRequest() throws InterruptedException, JSONException { // Given @@ -54,8 +41,7 @@ public void postRequest() throws InterruptedException, JSONException { CastleContext payload = sdk.contextBuilder() .build(); - // and an authenticate request is made - CastleResponse response = sdk.onRequest(request).post("/v1/authenticate", ImmutableMap.builder() + CastleResponse response = sdk.onRequest(request).post("/v1/risk", ImmutableMap.builder() .put("event", "$login.succeeded") .put("userId", "12345") .put("context", payload) diff --git a/src/test/java/io/castle/client/CastleImpersonateEndHttpTest.java b/src/test/java/io/castle/client/CastleImpersonateEndHttpTest.java deleted file mode 100644 index 8474d69d..00000000 --- a/src/test/java/io/castle/client/CastleImpersonateEndHttpTest.java +++ /dev/null @@ -1,56 +0,0 @@ -package io.castle.client; - -import io.castle.client.model.*; -import io.castle.client.utils.DeviceUtils; -import okhttp3.mockwebserver.MockResponse; -import okhttp3.mockwebserver.RecordedRequest; -import org.assertj.core.api.Assertions; -import org.junit.Assert; -import org.junit.Test; -import org.springframework.mock.web.MockHttpServletRequest; - -import jakarta.servlet.http.HttpServletRequest; - -import static okhttp3.mockwebserver.SocketPolicy.NO_RESPONSE; - -public class CastleImpersonateEndHttpTest extends AbstractCastleHttpLayerTest { - - public CastleImpersonateEndHttpTest() { - super(new AuthenticateFailoverStrategy(AuthenticateAction.CHALLENGE)); - } - - @Test public void impersonateEnd() throws Exception { - MockResponse mockResponse = new MockResponse(); - mockResponse.setBody("{\"success\":true}"); - server.enqueue(mockResponse); - - // And a mock Request - HttpServletRequest request = new MockHttpServletRequest(); - - // And an devices request is made - CastleSuccess result = sdk.onRequest(request).impersonateEnd("userId"); - - // Then - RecordedRequest recordedRequest = server.takeRequest(); - Assert.assertEquals(testServerBaseUrl.resolve("v1/impersonate"), recordedRequest.getRequestUrl()); - Assert.assertEquals("DELETE", recordedRequest.getMethod()); - - // and the correct Success model object is extracted - CastleSuccess expected = new CastleSuccess(); - expected.setSuccess(true); - - Assertions.assertThat(result).usingRecursiveComparison().isEqualTo(expected); - } - - @Test(expected = CastleApiTimeoutException.class) - public void impersonateEndTimeoutTest() { - - // given backend request timeouts - server.enqueue(new MockResponse().setSocketPolicy(NO_RESPONSE)); - HttpServletRequest request = new MockHttpServletRequest(); - String userId = "userId"; - - //when a v1/impersonate request is made - sdk.onRequest(request).impersonateStart(userId); - } -} diff --git a/src/test/java/io/castle/client/CastleImpersonateStartHttpTest.java b/src/test/java/io/castle/client/CastleImpersonateStartHttpTest.java deleted file mode 100644 index f2cb0eb6..00000000 --- a/src/test/java/io/castle/client/CastleImpersonateStartHttpTest.java +++ /dev/null @@ -1,56 +0,0 @@ -package io.castle.client; - -import io.castle.client.model.*; -import io.castle.client.utils.DeviceUtils; -import okhttp3.mockwebserver.MockResponse; -import okhttp3.mockwebserver.RecordedRequest; -import org.assertj.core.api.Assertions; -import org.junit.Assert; -import org.junit.Test; -import org.springframework.mock.web.MockHttpServletRequest; - -import jakarta.servlet.http.HttpServletRequest; - -import static okhttp3.mockwebserver.SocketPolicy.NO_RESPONSE; - -public class CastleImpersonateStartHttpTest extends AbstractCastleHttpLayerTest { - - public CastleImpersonateStartHttpTest() { - super(new AuthenticateFailoverStrategy(AuthenticateAction.CHALLENGE)); - } - - @Test public void impersonateStart() throws Exception { - MockResponse mockResponse = new MockResponse(); - mockResponse.setBody("{\"success\":true}"); - server.enqueue(mockResponse); - - // And a mock Request - HttpServletRequest request = new MockHttpServletRequest(); - - // And an devices request is made - CastleSuccess result = sdk.onRequest(request).impersonateStart("userId"); - - // Then - RecordedRequest recordedRequest = server.takeRequest(); - Assert.assertEquals(testServerBaseUrl.resolve("v1/impersonate"), recordedRequest.getRequestUrl()); - Assert.assertEquals("POST", recordedRequest.getMethod()); - - // and the correct Success model object is extracted - CastleSuccess expected = new CastleSuccess(); - expected.setSuccess(true); - - Assertions.assertThat(result).usingRecursiveComparison().isEqualTo(expected); - } - - @Test(expected = CastleApiTimeoutException.class) - public void impersonateStartTimeoutTest() { - - // given backend request timeouts - server.enqueue(new MockResponse().setSocketPolicy(NO_RESPONSE)); - HttpServletRequest request = new MockHttpServletRequest(); - String userId = "userId"; - - //when a review request is made - sdk.onRequest(request).impersonateStart(userId); - } -} \ No newline at end of file diff --git a/src/test/java/io/castle/client/CastleListItemsTest.java b/src/test/java/io/castle/client/CastleListItemsTest.java index a3c91e91..2a04118f 100644 --- a/src/test/java/io/castle/client/CastleListItemsTest.java +++ b/src/test/java/io/castle/client/CastleListItemsTest.java @@ -1,7 +1,5 @@ package io.castle.client; -import io.castle.client.model.AuthenticateAction; -import io.castle.client.model.AuthenticateFailoverStrategy; import io.castle.client.model.CastleResponse; import io.castle.client.model.generated.*; import jakarta.servlet.http.HttpServletRequest; @@ -18,10 +16,6 @@ public class CastleListItemsTest extends AbstractCastleHttpLayerTest { - public CastleListItemsTest() { - super(new AuthenticateFailoverStrategy(AuthenticateAction.CHALLENGE)); - } - @Test public void createListItem() throws InterruptedException { MockResponse mockResponse = new MockResponse(); @@ -89,12 +83,12 @@ public void createOrUpdateListItems() throws InterruptedException { ListItemsBatchResponse response = sdk.onRequest(request).createOrUpdateListItems("2ee938c8-24c2-4c26-9d25-19511dd75029", items); // Check response object - Assert.assertEquals(135, response.getTotalReceived()); - Assert.assertEquals(130, response.getTotalProcessed()); - Assert.assertEquals(5, response.getErrored()); - Assert.assertEquals(10, response.getReplaced()); - Assert.assertEquals(20, response.getUpdated()); - Assert.assertEquals(100, response.getCreated()); + Assert.assertEquals(Integer.valueOf(135), response.getTotalReceived()); + Assert.assertEquals(Integer.valueOf(130), response.getTotalProcessed()); + Assert.assertEquals(Integer.valueOf(5), response.getErrored()); + Assert.assertEquals(Integer.valueOf(10), response.getReplaced()); + Assert.assertEquals(Integer.valueOf(20), response.getUpdated()); + Assert.assertEquals(Integer.valueOf(100), response.getCreated()); // Then RecordedRequest recordedRequest = server.takeRequest(); diff --git a/src/test/java/io/castle/client/CastleListsHttpTest.java b/src/test/java/io/castle/client/CastleListsHttpTest.java index 548be6f8..bd9675b7 100644 --- a/src/test/java/io/castle/client/CastleListsHttpTest.java +++ b/src/test/java/io/castle/client/CastleListsHttpTest.java @@ -2,8 +2,6 @@ import com.google.gson.JsonParser; import io.castle.client.internal.json.CastleGsonModel; -import io.castle.client.model.AuthenticateAction; -import io.castle.client.model.AuthenticateFailoverStrategy; import io.castle.client.model.generated.*; import jakarta.servlet.http.HttpServletRequest; import okhttp3.mockwebserver.MockResponse; @@ -19,10 +17,6 @@ public class CastleListsHttpTest extends AbstractCastleHttpLayerTest { - public CastleListsHttpTest() { - super(new AuthenticateFailoverStrategy(AuthenticateAction.CHALLENGE)); - } - @Test public void updateList() throws InterruptedException { MockResponse mockResponse = new MockResponse(); @@ -51,7 +45,7 @@ public void updateList() throws InterruptedException { Assert.assertEquals("Malicious IPs", response.getName()); Assert.assertEquals("We block these IPs from withdrawing funds. Please be careful.", response.getDescription()); Assert.assertEquals(ListColor.RED, response.getColor()); - Assert.assertEquals(2592000, response.getDefaultItemArchivationTime()); + Assert.assertEquals(Integer.valueOf(2592000), response.getDefaultItemArchivationTime()); Assert.assertEquals("2ee938c8-24c2-4c26-9d25-19511dd75029", response.getId()); Assert.assertEquals("device.fingerprint", response.getPrimaryField()); Assert.assertEquals("ip.value", response.getSecondaryField()); @@ -94,7 +88,7 @@ public void listAllLists() throws InterruptedException { Assert.assertEquals("Malicious IPs", list.getName()); Assert.assertEquals("We block these IPs from withdrawing funds. Please be careful.", list.getDescription()); Assert.assertEquals(ListColor.RED, list.getColor()); - Assert.assertEquals(2592000, list.getDefaultItemArchivationTime()); + Assert.assertEquals(Integer.valueOf(2592000), list.getDefaultItemArchivationTime()); Assert.assertEquals("2ee938c8-24c2-4c26-9d25-19511dd75029", list.getId()); Assert.assertEquals("device.fingerprint", list.getPrimaryField()); Assert.assertEquals("ip.value", list.getSecondaryField()); @@ -134,7 +128,7 @@ public void listAllLists() throws InterruptedException { Assert.assertEquals("Malicious IPs", response.getName()); Assert.assertEquals("We block these IPs from withdrawing funds. Please be careful.", response.getDescription()); Assert.assertEquals(ListColor.RED, response.getColor()); - Assert.assertEquals(2592000, response.getDefaultItemArchivationTime()); + Assert.assertEquals(Integer.valueOf(2592000), response.getDefaultItemArchivationTime()); Assert.assertEquals("2ee938c8-24c2-4c26-9d25-19511dd75029", response.getId()); Assert.assertEquals("device.fingerprint", response.getPrimaryField()); Assert.assertEquals("ip.value", response.getSecondaryField()); @@ -197,7 +191,7 @@ public void searchList() throws InterruptedException { Assert.assertEquals("Malicious IPs", list.getName()); Assert.assertEquals("We block these IPs from withdrawing funds. Please be careful.", list.getDescription()); Assert.assertEquals(ListColor.RED, list.getColor()); - Assert.assertEquals(2592000, list.getDefaultItemArchivationTime()); + Assert.assertEquals(Integer.valueOf(2592000), list.getDefaultItemArchivationTime()); Assert.assertEquals("2ee938c8-24c2-4c26-9d25-19511dd75029", list.getId()); Assert.assertEquals("device.fingerprint", list.getPrimaryField()); Assert.assertEquals("ip.value", list.getSecondaryField()); @@ -239,7 +233,7 @@ public void createList() throws InterruptedException { Assert.assertEquals("Malicious IPs", response.getName()); Assert.assertEquals("We block these IPs from withdrawing funds. Please be careful.", response.getDescription()); Assert.assertEquals(ListColor.RED, response.getColor()); - Assert.assertEquals(2592000, response.getDefaultItemArchivationTime()); + Assert.assertEquals(Integer.valueOf(2592000), response.getDefaultItemArchivationTime()); Assert.assertEquals("2ee938c8-24c2-4c26-9d25-19511dd75029", response.getId()); Assert.assertEquals("device.fingerprint", response.getPrimaryField()); Assert.assertEquals("ip.value", response.getSecondaryField()); @@ -421,4 +415,13 @@ public void compareListResponseJson() { // Compare the JSON strings Assert.assertEquals(JsonParser.parseString(providedJson), JsonParser.parseString(listJson)); } + + @Test + public void listResponseAllowsMissingArchivationTime() { + ListResponse response = new CastleGsonModel().getGson().fromJson( + "{\"name\":\"demo\",\"id\":\"abc\",\"primary_field\":\"user.email\"}", + ListResponse.class); + + Assert.assertNull(response.getDefaultItemArchivationTime()); + } } diff --git a/src/test/java/io/castle/client/CastleLogHttpTest.java b/src/test/java/io/castle/client/CastleLogHttpTest.java index d3d1a579..534adadc 100644 --- a/src/test/java/io/castle/client/CastleLogHttpTest.java +++ b/src/test/java/io/castle/client/CastleLogHttpTest.java @@ -1,11 +1,8 @@ package io.castle.client; import com.google.gson.JsonParser; -import io.castle.client.model.AuthenticateAction; -import io.castle.client.model.AuthenticateFailoverStrategy; import io.castle.client.model.CastleResponse; import io.castle.client.model.generated.*; -import io.castle.client.utils.DeviceUtils; import okhttp3.mockwebserver.MockResponse; import okhttp3.mockwebserver.RecordedRequest; import org.assertj.core.api.Assertions; @@ -20,10 +17,6 @@ public class CastleLogHttpTest extends AbstractCastleHttpLayerTest { - public CastleLogHttpTest() { - super(new AuthenticateFailoverStrategy(AuthenticateAction.CHALLENGE)); - } - @Test public void log() throws InterruptedException { MockResponse mockResponse = new MockResponse(); mockResponse.setResponseCode(204); diff --git a/src/test/java/io/castle/client/CastleMergeHttpTest.java b/src/test/java/io/castle/client/CastleMergeHttpTest.java deleted file mode 100644 index 8323ad55..00000000 --- a/src/test/java/io/castle/client/CastleMergeHttpTest.java +++ /dev/null @@ -1,100 +0,0 @@ -package io.castle.client; - -import io.castle.client.model.AuthenticateAction; -import io.castle.client.model.AuthenticateFailoverStrategy; -import io.castle.client.utils.SDKVersion; -import okhttp3.mockwebserver.MockResponse; -import okhttp3.mockwebserver.RecordedRequest; -import org.json.JSONException; -import org.json.JSONObject; -import org.junit.Assert; -import org.junit.Test; -import org.skyscreamer.jsonassert.JSONAssert; -import org.springframework.mock.web.MockHttpServletRequest; - -import com.google.common.collect.ImmutableMap; - -import jakarta.servlet.http.HttpServletRequest; -import java.util.concurrent.atomic.AtomicReference; -import io.castle.client.model.AsyncCallbackHandler; -import org.assertj.core.api.Assertions; - -public class CastleMergeHttpTest extends AbstractCastleHttpLayerTest { - - public CastleMergeHttpTest() { - super(new AuthenticateFailoverStrategy(AuthenticateAction.CHALLENGE)); - } - - @Test - public void mergeContextIsSendOnHttp() throws InterruptedException, JSONException { - // Given - server.enqueue(new MockResponse().setResponseCode(200)); - String event = "any.valid.event"; - - // And a mock Request - HttpServletRequest request = new MockHttpServletRequest(); - - // And a extra context - CustomExtraContext customExtraContext = new CustomExtraContext(); - customExtraContext.setAddString("String value"); - customExtraContext.setCondition(true); - customExtraContext.setValue(10L); - sdk.onRequest(request).mergeContext(customExtraContext).track(event, null, null, null, ImmutableMap.builder() - .put("x", "valueX") - .put("y", 234567) - .build()); - - RecordedRequest recordedRequest = server.takeRequest(); - String body = recordedRequest.getBody().readUtf8(); - - JSONAssert.assertEquals("{\"event\":\"any.valid.event\",\"context\":{\"active\":true,\"ip\":\"127.0.0.1\",\"headers\":{\"REMOTE_ADDR\":\"127.0.0.1\"}," + SDKVersion.getLibraryString() +",\"add_string\":\"String value\",\"condition\":true,\"value\":10},\"user_traits\":{\"x\":\"valueX\",\"y\":234567}}", body, false); - } - - @Test - public void mergeContextIsNull() throws InterruptedException, JSONException { - // Given - server.enqueue(new MockResponse().setResponseCode(200)); - String event = "any.valid.event"; - - // And a mock Request - HttpServletRequest request = new MockHttpServletRequest(); - - // And null context - sdk.onRequest(request).mergeContext(null).track(event, null, null, null); - - RecordedRequest recordedRequest = server.takeRequest(); - String body = recordedRequest.getBody().readUtf8(); - - JSONAssert.assertEquals("{\"event\":\"any.valid.event\"}", body, false); - } - - private static class CustomExtraContext { - private String addString; - private Boolean condition; - private Long value; - - public String getAddString() { - return addString; - } - - public void setAddString(String addString) { - this.addString = addString; - } - - public Boolean getCondition() { - return condition; - } - - public void setCondition(Boolean condition) { - this.condition = condition; - } - - public Long getValue() { - return value; - } - - public void setValue(Long value) { - this.value = value; - } - } -} diff --git a/src/test/java/io/castle/client/CastleNoTrackOptionTest.java b/src/test/java/io/castle/client/CastleNoTrackOptionTest.java deleted file mode 100644 index 144a03f7..00000000 --- a/src/test/java/io/castle/client/CastleNoTrackOptionTest.java +++ /dev/null @@ -1,170 +0,0 @@ -package io.castle.client; - -import io.castle.client.api.CastleApi; -import io.castle.client.model.AsyncCallbackHandler; -import io.castle.client.model.AuthenticateAction; -import io.castle.client.model.AuthenticateFailoverStrategy; -import io.castle.client.model.Verdict; -import okhttp3.mockwebserver.MockResponse; -import org.assertj.core.api.Assertions; -import org.junit.Assert; -import org.junit.Test; -import org.springframework.mock.web.MockHttpServletRequest; - -import jakarta.servlet.http.HttpServletRequest; -import java.util.concurrent.atomic.AtomicReference; - -public class CastleNoTrackOptionTest extends AbstractCastleHttpLayerTest { - - public CastleNoTrackOptionTest() { - super(new AuthenticateFailoverStrategy(AuthenticateAction.CHALLENGE)); - } - - @Test - public void noTrackOptionDisableRequests() throws InterruptedException { - - //Given a mock HTTP request is provided - HttpServletRequest request = new MockHttpServletRequest(); - //And a CastleApi is created with doNotTrack option - CastleApi castleApi = sdk.onRequest(request).doNotTrack(true); - // And a custom handler for the async authenticate call - final AtomicReference authenticateAsyncResult = new AtomicReference<>(); - AsyncCallbackHandler handler = new AsyncCallbackHandler() { - @Override - public void onResponse(Verdict response) { - authenticateAsyncResult.set(response); - } - - @Override - public void onException(Exception exception) { - Assertions.fail("error on request", exception); - } - }; - - //When all API call are executed - castleApi.track("testEvent"); - Verdict verdict = castleApi.authenticate("testEvent", "userId"); - castleApi.authenticateAsync("testEvent", "userId", handler); - - //and we await the timeout period to avoid concurrency races - Thread.sleep(120); - - //Then no calls are made to the backend - Assertions.assertThat(server.getRequestCount()).isEqualTo(0); - //And the Verdict is the default ALLOW value - Assert.assertEquals(AuthenticateAction.ALLOW, verdict.getAction()); - Assert.assertEquals("userId", verdict.getUserId()); - Assert.assertEquals(AuthenticateAction.ALLOW, authenticateAsyncResult.get().getAction()); - Assertions.assertThat(authenticateAsyncResult.get().isFailover()).isTrue(); - } - - @Test - public void useCustomTrackHandlerWithNoTrackOptionToDisableRequests() { - //Given a mock HTTP request is provided - HttpServletRequest request = new MockHttpServletRequest(); - //And a CastleApi is created with doNotTrack option - CastleApi castleApi = sdk.onRequest(request).doNotTrack(true); - // And a custom handler for the async track call - final AtomicReference trackAsyncResult = new AtomicReference<>(); - - AsyncCallbackHandler trackHandler = new AsyncCallbackHandler() { - @Override - public void onResponse(Boolean response) { - trackAsyncResult.set(response); - } - - @Override - public void onException(Exception exception) { - Assertions.fail("error on request", exception); - - } - }; - - //When an API call is executed - castleApi.track( - "testEvent", - null, - null, - null, - null, - trackHandler - ); - - //Then no calls are made to the backend - Assertions.assertThat(server.getRequestCount()).isEqualTo(0); - - //And the track call returns and the resulting boolean is true - Assert.assertTrue(trackAsyncResult.get()); - } - - @Test - public void noTrackOptionCreateAllowFailoverVerdict() { - - //Given a mock HTTP request is provided - HttpServletRequest request = new MockHttpServletRequest(); - //And a CastleApi is created with doNotTrack option - CastleApi castleApi = sdk.onRequest(request).doNotTrack(true); - //When authenticate is called - - Verdict verdict = castleApi.authenticate("testEvent", "userId"); - - //Then no calls are made to the backend - Assertions.assertThat(server.getRequestCount()).isEqualTo(0); - //And the Verdict is the correct failover value - Assert.assertEquals(AuthenticateAction.ALLOW, verdict.getAction()); - - Assertions.assertThat(verdict.getUserId()).isEqualTo("userId"); - Assertions.assertThat(verdict.getAction()).isEqualTo(AuthenticateAction.ALLOW); - Assertions.assertThat(verdict.getFailoverReason()).isEqualTo("Castle set to do not track."); - Assertions.assertThat(verdict.isFailover()).isTrue(); - } - - - @Test - public void trackOptionAllowRequests() throws InterruptedException { - - //Given a mock HTTP request is provided - HttpServletRequest request = new MockHttpServletRequest(); - //And responses are setup on mock server - server.enqueue(new MockResponse().setResponseCode(200)); - server.enqueue(new MockResponse().setBody( - "{\n" + - " \"action\": \"deny\",\n" + - " \"user_id\": \"12345\"\n" + - "}")); - server.enqueue(new MockResponse().setBody( - "{\n" + - " \"action\": \"deny\",\n" + - " \"user_id\": \"12345\"\n" + - "}")); - //And a CastleApi is created with doNotTrack option - CastleApi castleApi = sdk.onRequest(request).doNotTrack(false); - // And a custom handler for the async authenticate call - final AtomicReference result = new AtomicReference<>(); - AsyncCallbackHandler handler = new AsyncCallbackHandler() { - @Override - public void onResponse(Verdict response) { - result.set(response); - } - - @Override - public void onException(Exception exception) { - Assertions.fail("error on request", exception); - } - }; - - //When all API call are executed - castleApi.track("testEvent"); - server.takeRequest(); - castleApi.authenticate("testEvent", "userId"); - server.takeRequest(); - castleApi.authenticateAsync("testEvent", "userId", handler); - server.takeRequest(); - - //Then all the calls are executed - waitForValue(result); - Assertions.assertThat(server.getRequestCount()).isEqualTo(3); - Assert.assertEquals(AuthenticateAction.DENY, result.get().getAction()); - } - -} diff --git a/src/test/java/io/castle/client/CastlePrivacyHttpTest.java b/src/test/java/io/castle/client/CastlePrivacyHttpTest.java new file mode 100644 index 00000000..59cf2c16 --- /dev/null +++ b/src/test/java/io/castle/client/CastlePrivacyHttpTest.java @@ -0,0 +1,57 @@ +package io.castle.client; + +import com.google.common.collect.ImmutableMap; +import io.castle.client.model.CastleResponse; +import okhttp3.mockwebserver.MockResponse; +import okhttp3.mockwebserver.RecordedRequest; +import org.assertj.core.api.Assertions; +import org.json.JSONException; +import org.junit.Assert; +import org.junit.Test; +import org.skyscreamer.jsonassert.JSONAssert; +import org.springframework.mock.web.MockHttpServletRequest; + +import jakarta.servlet.http.HttpServletRequest; + +public class CastlePrivacyHttpTest extends AbstractCastleHttpLayerTest { + + @Test + public void requestUserData() throws InterruptedException, JSONException { + server.enqueue(new MockResponse().setBody("{}")); + HttpServletRequest request = new MockHttpServletRequest(); + + CastleResponse response = sdk.onRequest(request).requestUserData(ImmutableMap.builder() + .put("identifier", "12345") + .put("identifier_type", "$id") + .build()); + if (response == null) { + Assertions.fail("error on request"); + } + + RecordedRequest recordedRequest = server.takeRequest(); + String body = recordedRequest.getBody().readUtf8(); + Assert.assertEquals(testServerBaseUrl.resolve("v1/privacy/users"), recordedRequest.getRequestUrl()); + Assert.assertEquals("POST", recordedRequest.getMethod()); + JSONAssert.assertEquals("{\"identifier\":\"12345\",\"identifier_type\":\"$id\"}", body, false); + } + + @Test + public void deleteUserData() throws InterruptedException, JSONException { + server.enqueue(new MockResponse().setBody("{}")); + HttpServletRequest request = new MockHttpServletRequest(); + + CastleResponse response = sdk.onRequest(request).deleteUserData(ImmutableMap.builder() + .put("identifier", "12345") + .put("identifier_type", "$id") + .build()); + if (response == null) { + Assertions.fail("error on request"); + } + + RecordedRequest recordedRequest = server.takeRequest(); + String body = recordedRequest.getBody().readUtf8(); + Assert.assertEquals(testServerBaseUrl.resolve("v1/privacy/users"), recordedRequest.getRequestUrl()); + Assert.assertEquals("DELETE", recordedRequest.getMethod()); + JSONAssert.assertEquals("{\"identifier\":\"12345\",\"identifier_type\":\"$id\"}", body, false); + } +} diff --git a/src/test/java/io/castle/client/CastleRecoverHttpTest.java b/src/test/java/io/castle/client/CastleRecoverHttpTest.java deleted file mode 100644 index cdc76bd0..00000000 --- a/src/test/java/io/castle/client/CastleRecoverHttpTest.java +++ /dev/null @@ -1,38 +0,0 @@ -package io.castle.client; - -import io.castle.client.model.*; -import io.castle.client.utils.DeviceUtils; -import okhttp3.mockwebserver.MockResponse; -import okhttp3.mockwebserver.RecordedRequest; -import org.assertj.core.api.Assertions; -import org.junit.Assert; -import org.junit.Test; -import org.springframework.mock.web.MockHttpServletRequest; - -import jakarta.servlet.http.HttpServletRequest; - -import static okhttp3.mockwebserver.SocketPolicy.NO_RESPONSE; - -public class CastleRecoverHttpTest extends AbstractCastleHttpLayerTest { - - public CastleRecoverHttpTest() { - super(new AuthenticateFailoverStrategy(AuthenticateAction.CHALLENGE)); - } - - @Test public void recoverDevices() throws InterruptedException { - MockResponse mockResponse = new MockResponse(); - mockResponse.setBody(DeviceUtils.getDefaultDeviceJSON()); - server.enqueue(mockResponse); - - // And a mock Request - HttpServletRequest request = new MockHttpServletRequest(); - - // And an report device request is made - CastleResponse response = sdk.onRequest(request).recover("1234"); - - // Then - RecordedRequest recordedRequest = server.takeRequest(); - Assert.assertEquals(testServerBaseUrl.resolve("v1/users/1234/recover"), recordedRequest.getRequestUrl()); - Assert.assertEquals("PUT", recordedRequest.getMethod()); - } -} diff --git a/src/test/java/io/castle/client/CastleRemoveUserHttpTest.java b/src/test/java/io/castle/client/CastleRemoveUserHttpTest.java deleted file mode 100644 index 06233e8d..00000000 --- a/src/test/java/io/castle/client/CastleRemoveUserHttpTest.java +++ /dev/null @@ -1,76 +0,0 @@ -package io.castle.client; - -import io.castle.client.model.AuthenticateAction; -import io.castle.client.model.AuthenticateFailoverStrategy; -import io.castle.client.model.CastleApiInternalServerErrorException; -import okhttp3.mockwebserver.MockResponse; -import okhttp3.mockwebserver.RecordedRequest; -import org.junit.Assert; -import org.junit.Test; -import org.springframework.mock.web.MockHttpServletRequest; - -import jakarta.servlet.http.HttpServletRequest; - -public class CastleRemoveUserHttpTest extends AbstractCastleHttpLayerTest { - - public CastleRemoveUserHttpTest() { - super(new AuthenticateFailoverStrategy(AuthenticateAction.CHALLENGE)); - } - - @Test - public void removeUser() throws Exception { - // Mock a response - MockResponse mockResponse = new MockResponse(); - mockResponse.setResponseCode(202); - server.enqueue(mockResponse); - - // And a mock Request - HttpServletRequest request = new MockHttpServletRequest(); - - // And an privacy remove user request is made - Boolean response = sdk.onRequest(request).removeUser("user-test-id"); - - // Then - RecordedRequest recordedRequest = server.takeRequest(); - Assert.assertEquals(testServerBaseUrl.resolve("v1/privacy/users/user-test-id"), recordedRequest.getRequestUrl()); - Assert.assertEquals("DELETE", recordedRequest.getMethod()); - Assert.assertTrue(response); - } - - @Test - public void removeUserNotFoundTest() throws Exception { - // Mock a response - MockResponse mockResponse = new MockResponse(); - mockResponse.setResponseCode(404); - server.enqueue(mockResponse); - - // And a mock Request - HttpServletRequest request = new MockHttpServletRequest(); - - // And an privacy remove user request is made - Boolean response = sdk.onRequest(request).removeUser("user-test-id"); - - // Then it doesn't throw exception - RecordedRequest recordedRequest = server.takeRequest(); - Assert.assertEquals(testServerBaseUrl.resolve("v1/privacy/users/user-test-id"), recordedRequest.getRequestUrl()); - Assert.assertEquals("DELETE", recordedRequest.getMethod()); - - Assert.assertNull(response); - } - - @Test(expected = CastleApiInternalServerErrorException.class) - public void removeUserServerError() { - // Mock a response - MockResponse mockResponse = new MockResponse(); - mockResponse.setResponseCode(500); - server.enqueue(mockResponse); - - // And a mock Request - HttpServletRequest request = new MockHttpServletRequest(); - - // And a privacy remove user request is made, exception throw - Boolean response = sdk.onRequest(request).removeUser("user-test-id"); - - Assert.assertNull(response); - } -} \ No newline at end of file diff --git a/src/test/java/io/castle/client/CastleRiskHttpTest.java b/src/test/java/io/castle/client/CastleRiskHttpTest.java index a3bb7a26..cae714b0 100644 --- a/src/test/java/io/castle/client/CastleRiskHttpTest.java +++ b/src/test/java/io/castle/client/CastleRiskHttpTest.java @@ -2,8 +2,6 @@ import com.google.gson.JsonParser; import io.castle.client.internal.json.CastleGsonModel; -import io.castle.client.model.AuthenticateAction; -import io.castle.client.model.AuthenticateFailoverStrategy; import io.castle.client.model.generated.*; import jakarta.servlet.http.HttpServletRequest; import okhttp3.mockwebserver.MockResponse; @@ -18,10 +16,6 @@ public class CastleRiskHttpTest extends AbstractCastleHttpLayerTest { - public CastleRiskHttpTest() { - super(new AuthenticateFailoverStrategy(AuthenticateAction.CHALLENGE)); - } - @Test public void risk() throws InterruptedException { MockResponse mockResponse = new MockResponse(); mockResponse.setBody("{\n" + @@ -229,7 +223,7 @@ public CastleRiskHttpTest() { Assert.assertEquals("America/New_York", response.getDevice().getTimezone().getName()); Assert.assertEquals(-300, response.getDevice().getTimezone().getOffset()); - Assert.assertEquals(2, response.getDevice().getScreen().getScreen()); + Assert.assertEquals(Integer.valueOf(2), response.getDevice().getScreen().getScreen()); Assert.assertEquals("landscape", response.getDevice().getScreen().getOrientation()); Assert.assertEquals("ASZoelALT5-PaVw2pAVMXg", response.getId()); @@ -433,4 +427,15 @@ public void compareRiskJson() { // Compare the JSON strings Assert.assertEquals(JsonParser.parseString(providedJson), JsonParser.parseString(riskJson)); } + + @Test + public void expandedEventAllowsMissingOptionalIntegers() { + FilterAndRiskResponse response = new CastleGsonModel().getGson().fromJson( + "{\"type\":\"$login\",\"status\":\"$succeeded\",\"id\":\"evt_1\"," + + "\"device\":{\"screen\":{}},\"ip\":{}}", + FilterAndRiskResponse.class); + + Assert.assertNull(response.getDevice().getScreen().getScreen()); + Assert.assertNull(response.getIp().getAsn()); + } } diff --git a/src/test/java/io/castle/client/CastleTest.java b/src/test/java/io/castle/client/CastleTest.java index 4fd5a9cf..e02b8484 100644 --- a/src/test/java/io/castle/client/CastleTest.java +++ b/src/test/java/io/castle/client/CastleTest.java @@ -1,11 +1,9 @@ package io.castle.client; -import io.castle.client.internal.backend.RestApiFactory; import io.castle.client.internal.config.CastleConfiguration; import io.castle.client.model.CastleSdkConfigurationException; import org.assertj.core.api.Assertions; import org.junit.Test; -import org.mockito.Mockito; public class CastleTest { @@ -82,6 +80,7 @@ public void sdkProvideASingleton() throws CastleSdkConfigurationException { //Then the same singleton instance is returned Assertions.assertThat(sdk1).isSameAs(sdk2); + sdk.close(); } @Test @@ -125,5 +124,35 @@ public void sdkOnConfigureWithSecretOnly() throws CastleSdkConfigurationExceptio Assertions.assertThat(sdkConfiguration) .extracting("apiSecret") .isEqualTo("abcd"); + sdk.close(); + } + + @Test + public void closeReleasesHttpClient() throws CastleSdkConfigurationException { + Castle sdk = Castle.initialize("abcd"); + sdk.close(); + } + + @Test + public void closeClearsTheSingletonWhenThisInstanceIsTheSingleton() throws CastleSdkConfigurationException { + Castle sdk = Castle.verifySdkConfigurationAndInitialize(); + Castle.setSingletonInstance(sdk); + + sdk.close(); + + Assertions.assertThatThrownBy(Castle::instance) + .isInstanceOf(IllegalStateException.class); + } + + @Test + public void closeDoesNotClearADifferentSingleton() throws CastleSdkConfigurationException { + Castle singleton = Castle.verifySdkConfigurationAndInitialize(); + Castle.setSingletonInstance(singleton); + Castle other = Castle.initialize("abcd"); + + other.close(); + + Assertions.assertThat(Castle.instance()).isSameAs(singleton); + singleton.close(); } } diff --git a/src/test/java/io/castle/client/CastleThrowStrategyTest.java b/src/test/java/io/castle/client/CastleThrowStrategyTest.java deleted file mode 100644 index c5c7df0e..00000000 --- a/src/test/java/io/castle/client/CastleThrowStrategyTest.java +++ /dev/null @@ -1,92 +0,0 @@ -package io.castle.client; - -import io.castle.client.model.*; -import okhttp3.mockwebserver.MockResponse; -import org.assertj.core.api.Assertions; -import org.junit.Test; -import org.springframework.mock.web.MockHttpServletRequest; - -import jakarta.servlet.http.HttpServletRequest; -import java.util.concurrent.atomic.AtomicReference; - -import static okhttp3.mockwebserver.SocketPolicy.NO_RESPONSE; - -public class CastleThrowStrategyTest extends AbstractCastleHttpLayerTest { - - public CastleThrowStrategyTest() { - super(new AuthenticateFailoverStrategy()); - } - - @Test(expected = CastleApiTimeoutException.class) - public void onThrowStrategyAuthenticateThrowCastleRuntimeExceptionWhenNoResponse() { - // given the throw strategy is setup (see constructor) - // and backend request timeouts - server.enqueue(new MockResponse().setSocketPolicy(NO_RESPONSE)); - // and - String id = "12345"; - String event = "$login.succeeded"; - - // and a mock Request - HttpServletRequest request = new MockHttpServletRequest(); - - // when authenticate request is made - sdk.onRequest(request).authenticate(event, id); - - // then the exception is throw - } - - @Test(expected = CastleApiInternalServerErrorException.class) - public void onThrowStrategyAuthenticateThrowCastleRuntimeExceptionWhenResponseIs500() { - // given the throw strategy is setup (see constructor) - // and backend request timeouts - server.enqueue(new MockResponse().setResponseCode(500)); - // and - String id = "12345"; - String event = "$login.succeeded"; - - // and a mock Request - HttpServletRequest request = new MockHttpServletRequest(); - - // when authenticate request is made - Verdict authenticate = sdk.onRequest(request).authenticate(event, id); - - // then the exception is throw - Assertions.fail("A exception should be throw"); - } - - @Test - public void onThrowStrategyAuthenticateAsyncThrowCastleRuntimeException() { - // given the throw strategy is setup (see constructor) - // and backend request timeouts - server.enqueue(new MockResponse().setSocketPolicy(NO_RESPONSE)); - // and - String id = "12345"; - String event = "$login.succeeded"; - - // and a mock Request - HttpServletRequest request = new MockHttpServletRequest(); - // and a async callback is prepared - final AtomicReference exceptionAtomicReference = new AtomicReference<>(); - AsyncCallbackHandler callback = new AsyncCallbackHandler() { - @Override - public void onResponse(Verdict response) { - Assertions.fail("A exceptions was expected"); - } - - @Override - public void onException(Exception exception) { - exceptionAtomicReference.set(exception); - } - }; - // when authenticate request is made - sdk.onRequest(request).authenticateAsync(event, id, callback); - - // then the exception is throw in the callback - Exception exception = waitForValue(exceptionAtomicReference); - Assertions.assertThat(exception) - .isNotNull() - .isInstanceOf(CastleRuntimeException.class); - } - - -} diff --git a/src/test/java/io/castle/client/CastleTrackHttpTest.java b/src/test/java/io/castle/client/CastleTrackHttpTest.java deleted file mode 100644 index 9fb036db..00000000 --- a/src/test/java/io/castle/client/CastleTrackHttpTest.java +++ /dev/null @@ -1,313 +0,0 @@ -package io.castle.client; - -import io.castle.client.model.AsyncCallbackHandler; -import io.castle.client.model.AuthenticateAction; -import io.castle.client.model.AuthenticateFailoverStrategy; -import io.castle.client.model.CastleContext; -import io.castle.client.model.CastleHeaders; -import io.castle.client.utils.SDKVersion; -import io.castle.client.model.CastleMessage; -import okhttp3.mockwebserver.MockResponse; -import okhttp3.mockwebserver.RecordedRequest; -import okhttp3.mockwebserver.SocketPolicy; -import org.assertj.core.api.Assertions; -import org.json.JSONException; -import org.json.JSONObject; -import org.junit.Assert; -import org.junit.Test; -import org.skyscreamer.jsonassert.JSONAssert; -import org.springframework.mock.web.MockHttpServletRequest; - -import com.google.common.collect.ImmutableMap; - -import jakarta.servlet.http.HttpServletRequest; -import java.util.concurrent.atomic.AtomicReference; - -public class CastleTrackHttpTest extends AbstractCastleHttpLayerTest { - - public CastleTrackHttpTest() { - super(new AuthenticateFailoverStrategy(AuthenticateAction.CHALLENGE)); - } - - @Test public void trackWithManualContext() throws Exception { - server.enqueue(new MockResponse()); - - - // When - CastleContext context = sdk.contextBuilder() - .ip("1.1.1.1") - .userAgent("Mozilla/5.0") - .clientId("") - .headers(CastleHeaders.builder() - .add("User-Agent", "Mozilla/5.0") - .add("Accept-Language", "sv-se") - .build() - ) - .build(); - - // And a mock Request - HttpServletRequest request = new MockHttpServletRequest(); - - // And a track request is made - sdk.onRequest(request).track(CastleMessage.builder("$login.succeeded") - .userId("12345") - .context(context) - .build() - ); - - - // Then - RecordedRequest recordedRequest = server.takeRequest(); - String body = recordedRequest.getBody().readUtf8(); - JSONAssert.assertEquals("{\"event\":\"$login.succeeded\",\"user_id\":\"12345\",\"context\":{\"active\":true,\"client_id\":\"\",\"ip\":\"1.1.1.1\",\"headers\":{\"User-Agent\":\"Mozilla/5.0\",\"Accept-Language\":\"sv-se\"}," + SDKVersion.getLibraryString() +",\"user_agent\":\"Mozilla/5.0\"}}", - body, false); - - Assert.assertTrue(new JSONObject(body).has("sent_at")); - } - - @Test - public void trackEndpointHttpAuthorize() throws Exception { - // Given - Castle.verifySdkConfigurationAndInitialize(); - - server.enqueue(new MockResponse()); - - // And a mock Request - HttpServletRequest request = new MockHttpServletRequest(); - - // And an track request is made - sdk.onRequest(request).track("$login.successful", "1234"); - - // Then - RecordedRequest recordedRequest = server.takeRequest(); - Assert.assertEquals("Basic OnRlc3RfYXBpX3NlY3JldA==", recordedRequest.getHeader("Authorization")); - } - - @Test - public void trackEndpointWithPaylod() throws InterruptedException, JSONException { - // Given - server.enqueue(new MockResponse()); - CastleMessage payload = CastleMessage.builder("$login.succeeded") - .userId("12345") - .build(); - HttpServletRequest request = new MockHttpServletRequest(); - - // When - sdk.onRequest(request).track(payload); - - // Then - RecordedRequest recordedRequest = server.takeRequest(); - String body = recordedRequest.getBody().readUtf8(); - JSONAssert.assertEquals("{\"event\":\"$login.succeeded\",\"user_id\":\"12345\",\"context\":{\"active\":true,\"ip\":\"127.0.0.1\",\"headers\":{\"REMOTE_ADDR\":\"127.0.0.1\"}," + SDKVersion.getLibraryString() +"}}", - body, false); - - Assert.assertTrue(new JSONObject(body).has("sent_at")); - } - - @Test - public void trackEndpointWithUserIDTest() throws InterruptedException, JSONException { - //given - server.enqueue(new MockResponse()); - String id = "12345"; - String event = "$login.succeeded"; - - // And a mock Request - HttpServletRequest request = new MockHttpServletRequest(); - - // and an track request is made - sdk.onRequest(request).track(event, id); - - // then - RecordedRequest recordedRequest = server.takeRequest(); - String body = recordedRequest.getBody().readUtf8(); - JSONAssert.assertEquals("{\"event\":\"$login.succeeded\",\"user_id\":\"12345\",\"context\":{\"active\":true,\"ip\":\"127.0.0.1\",\"headers\":{\"REMOTE_ADDR\":\"127.0.0.1\"}," + SDKVersion.getLibraryString() +"}}", - body, false); - - Assert.assertTrue(new JSONObject(body).has("sent_at")); - } - - @Test - public void trackEndpointWithUserIDAndReviewIdTest() throws InterruptedException, JSONException { - //given - server.enqueue(new MockResponse()); - String userId = "12345"; - String reviewId = "r45677"; - String event = "$login.succeeded"; - - // And a mock Request - HttpServletRequest request = new MockHttpServletRequest(); - - // and an track request is made - sdk.onRequest(request).track(event, userId,reviewId); - - // then - RecordedRequest recordedRequest = server.takeRequest(); - String body = recordedRequest.getBody().readUtf8(); - JSONAssert.assertEquals("{\"event\":\"$login.succeeded\",\"review_id\":\"r45677\",\"user_id\":\"12345\",\"context\":{\"active\":true,\"ip\":\"127.0.0.1\",\"headers\":{\"REMOTE_ADDR\":\"127.0.0.1\"}," + SDKVersion.getLibraryString() +"}}", - body, false); - - Assert.assertTrue(new JSONObject(body).has("sent_at")); - } - - @Test - public void trackEndpointWithUserIDAndPropertiesTest() throws InterruptedException, JSONException { - //given - server.enqueue(new MockResponse()); - String userId = "12345"; - String reviewId = "r987"; - String event = "$login.succeeded"; - - // And a mock Request - HttpServletRequest request = new MockHttpServletRequest(); - - // and an track request is made - sdk.onRequest(request).track(event, userId, reviewId, ImmutableMap.builder() - .put("a","valueA") - .put("b",123456) - .build()); - - // then - RecordedRequest recordedRequest = server.takeRequest(); - String body = recordedRequest.getBody().readUtf8(); - JSONAssert.assertEquals("{\"event\":\"$login.succeeded\",\"properties\":{\"a\":\"valueA\",\"b\":123456},\"review_id\":\"r987\",\"user_id\":\"12345\",\"context\":{\"active\":true,\"ip\":\"127.0.0.1\",\"headers\":{\"REMOTE_ADDR\":\"127.0.0.1\"}," + SDKVersion.getLibraryString() +"}}", - body, false); - - Assert.assertTrue(new JSONObject(body).has("sent_at")); - } - - @Test - public void trackEndpointWithUserIDAndPropertiesAndTraitTest() throws InterruptedException, JSONException { - //given - server.enqueue(new MockResponse()); - String userId = "23456"; - String reviewId = "r987"; - String event = "$login.succeeded"; - - // And a mock Request - HttpServletRequest request = new MockHttpServletRequest(); - - // and an track request is made - sdk.onRequest(request).track(event, userId, reviewId, ImmutableMap.builder() - .put("a","valueA") - .put("b",123456) - .build(), ImmutableMap.builder() - .put("x", "x value") - .put("y", 2342) - .build()); - - // then - RecordedRequest recordedRequest = server.takeRequest(); - String body = recordedRequest.getBody().readUtf8(); - JSONAssert.assertEquals("{\"event\":\"$login.succeeded\",\"properties\":{\"a\":\"valueA\",\"b\":123456},\"review_id\":\"r987\",\"user_id\":\"23456\",\"user_traits\":{\"x\":\"x value\",\"y\":2342},\"context\":{\"active\":true,\"ip\":\"127.0.0.1\",\"headers\":{\"REMOTE_ADDR\":\"127.0.0.1\"}," + SDKVersion.getLibraryString() +"}}", - body, false); - - Assert.assertTrue(new JSONObject(body).has("sent_at")); - } - - @Test - public void trackEndpointWithMinimalInformationTest() throws InterruptedException, JSONException { - //given - server.enqueue(new MockResponse()); - String event = "any.valid.event"; - // And a mock Request - HttpServletRequest request = new MockHttpServletRequest(); - - // and an track request is made - sdk.onRequest(request).track(event); - - // then - RecordedRequest recordedRequest = server.takeRequest(); - String body = recordedRequest.getBody().readUtf8(); - JSONAssert.assertEquals("{\"event\":\"any.valid.event\",\"context\":{\"active\":true,\"ip\":\"127.0.0.1\",\"headers\":{\"REMOTE_ADDR\":\"127.0.0.1\"}," + SDKVersion.getLibraryString() +"}}", - body, false); - - Assert.assertTrue(new JSONObject(body).has("sent_at")); - } - - @Test - public void trackEndpointAsyncTest() throws InterruptedException { - //given - server.enqueue(new MockResponse().setResponseCode(200)); - String event = "any.valid.event"; - // And a mock Request - HttpServletRequest request = new MockHttpServletRequest(); - - // and an async track request is made - final AtomicReference result = new AtomicReference<>(); - AsyncCallbackHandler callback = new AsyncCallbackHandler() { - @Override - public void onResponse(Boolean response) { - result.set(response); - } - - @Override - public void onException(Exception exception) { - Assertions.fail("No exception expected here"); - } - }; - sdk.onRequest(request).track(event, null, null, null,null,callback); - - // then the callback response is called with true - Boolean extracted = waitForValue(result); - Assertions.assertThat(extracted).isTrue(); - } - - @Test - public void trackEndpointTimeoutTest() throws InterruptedException, JSONException { - // given the backend will timeout - server.enqueue(new MockResponse().setSocketPolicy(SocketPolicy.NO_RESPONSE)); - String event = "any.valid.event"; - // and a mock Request - final HttpServletRequest request = new MockHttpServletRequest(); - - // and a async handler is prepared - final AtomicReference result = new AtomicReference<>(); - AsyncCallbackHandler callback = new AsyncCallbackHandler() { - @Override - public void onResponse(Boolean response) { - Assertions.fail("Request should end with an timeout exception and not result"); - } - - @Override - public void onException(Exception exception) { - result.set(false); - } - }; - // when an track request is made - sdk.onRequest(request).track(event, null, null, null,null,callback); - - // then the track request must be send - RecordedRequest recordedRequest = server.takeRequest(); - String body = recordedRequest.getBody().readUtf8(); - JSONAssert.assertEquals("{\"event\":\"any.valid.event\",\"context\":{\"active\":true,\"ip\":\"127.0.0.1\",\"headers\":{\"REMOTE_ADDR\":\"127.0.0.1\"}," + SDKVersion.getLibraryString() +"}}", - body, false); - - Assert.assertTrue(new JSONObject(body).has("sent_at")); - - // and the onException method must be called - waitForValueAndVerify(result, Boolean.FALSE); - } - - @Test - public void trackEndpointTimeoutAreIgnoreWhenNoCallbackIsProvidedTest() throws InterruptedException, JSONException { - // given the backend will timeout - server.enqueue(new MockResponse().setSocketPolicy(SocketPolicy.NO_RESPONSE)); - String event = "any.valid.event"; - // and a mock Request - final HttpServletRequest request = new MockHttpServletRequest(); - - // when an track request is made - sdk.onRequest(request).track(event, null, null, null); - - // then the track request must be send - RecordedRequest recordedRequest = server.takeRequest(); - String body = recordedRequest.getBody().readUtf8(); - JSONAssert.assertEquals("{\"event\":\"any.valid.event\",\"context\":{\"active\":true,\"ip\":\"127.0.0.1\",\"headers\":{\"REMOTE_ADDR\":\"127.0.0.1\"}," + SDKVersion.getLibraryString() +"}}", - body, false); - - Assert.assertTrue(new JSONObject(body).has("sent_at")); - - // and no exceptions are thrown in any thread - } - -} \ No newline at end of file diff --git a/src/test/java/io/castle/client/CastleWebhookTest.java b/src/test/java/io/castle/client/CastleWebhookTest.java new file mode 100644 index 00000000..c6478a97 --- /dev/null +++ b/src/test/java/io/castle/client/CastleWebhookTest.java @@ -0,0 +1,38 @@ +package io.castle.client; + +import io.castle.client.internal.utils.Webhook; +import io.castle.client.model.CastleSdkConfigurationException; +import org.assertj.core.api.Assertions; +import org.junit.Test; + +import java.nio.charset.StandardCharsets; + +public class CastleWebhookTest { + + private static final String SECRET = "test_api_secret"; + + @Test + public void verifiesValidSignature() throws CastleSdkConfigurationException { + Castle sdk = Castle.verifySdkConfigurationAndInitialize(); + byte[] body = "{\"type\":\"$review.opened\"}".getBytes(StandardCharsets.UTF_8); + String signature = Webhook.computeSignature(SECRET, body); + + Assertions.assertThat(sdk.verifyWebhookSignature(signature, body)).isTrue(); + } + + @Test + public void rejectsInvalidSignature() throws CastleSdkConfigurationException { + Castle sdk = Castle.verifySdkConfigurationAndInitialize(); + byte[] body = "{\"type\":\"$review.opened\"}".getBytes(StandardCharsets.UTF_8); + + Assertions.assertThat(sdk.verifyWebhookSignature("not-the-signature", body)).isFalse(); + } + + @Test + public void rejectsNullSignature() throws CastleSdkConfigurationException { + Castle sdk = Castle.verifySdkConfigurationAndInitialize(); + byte[] body = "{}".getBytes(StandardCharsets.UTF_8); + + Assertions.assertThat(sdk.verifyWebhookSignature((String) null, body)).isFalse(); + } +} diff --git a/src/test/java/io/castle/client/internal/config/ConfigurationLoaderTest.java b/src/test/java/io/castle/client/internal/config/ConfigurationLoaderTest.java index 25d0ac7d..490082d6 100644 --- a/src/test/java/io/castle/client/internal/config/ConfigurationLoaderTest.java +++ b/src/test/java/io/castle/client/internal/config/ConfigurationLoaderTest.java @@ -1,7 +1,5 @@ package io.castle.client.internal.config; -import io.castle.client.model.AuthenticateAction; -import io.castle.client.model.AuthenticateFailoverStrategy; import io.castle.client.model.CastleRuntimeException; import io.castle.client.model.CastleSdkConfigurationException; import org.assertj.core.api.Assertions; @@ -88,7 +86,6 @@ public void loadFromProperties() throws CastleSdkConfigurationException { ) .withDefaultBackendProvider() .withTimeout(100) - .withAuthenticateFailoverStrategy(new AuthenticateFailoverStrategy(AuthenticateAction.CHALLENGE)) .build(); //Then the value of the timeout should be the one in the properties file castle_sdk.properties @@ -105,10 +102,6 @@ public void loadFromEnv() throws CastleSdkConfigurationException { "CASTLE_SDK_TIMEOUT", "700" ); - setEnvAndTestCorrectness( - "CASTLE_SDK_AUTHENTICATE_FAILOVER_STRATEGY", - "DENY" - ); setEnvAndTestCorrectness( "CASTLE_SDK_ALLOWLIST_HEADERS", "Accept-Encoding,Accept-Charset" @@ -145,7 +138,6 @@ public void loadFromEnv() throws CastleSdkConfigurationException { .withApiBaseUrl("https://api.dev.castle.io/v1/") .withTimeout(700) .withLogHttpRequests(true) - .withAuthenticateFailoverStrategy(new AuthenticateFailoverStrategy(AuthenticateAction.DENY)) .build(); testLoad(expectedConfiguration); @@ -160,7 +152,6 @@ public void loadLegacyApiSecretEnv() throws CastleSdkConfigurationException { "CASTLE_SDK_API_SECRET", "1234" ); - // and a expected config is the default configuration with the throw strategy CastleConfiguration expectedConfiguration = CastleConfigurationBuilder .defaultConfigBuilder() .withApiSecret("1234") @@ -170,37 +161,6 @@ public void loadLegacyApiSecretEnv() throws CastleSdkConfigurationException { testLoad(expectedConfiguration); } - @Test - public void loadFailoverThrowStrategyFromEnv() throws CastleSdkConfigurationException { - //given a property file not existing is provided - setEnvAndTestCorrectness("CASTLE_PROPERTIES_FILE", "notExistingFile.properties"); - // and a minimal sdk configuration is provided in environment - setEnvAndTestCorrectness( - "CASTLE_SDK_APP_ID", - "test_app_id_env" - ); - setEnvAndTestCorrectness( - "CASTLE_SDK_API_SECRET", - "1234" - ); - // and the failover strategy environment value is throw - setEnvAndTestCorrectness( - "CASTLE_SDK_AUTHENTICATE_FAILOVER_STRATEGY", - "throw" - ); - // and an expected config is the default configuration with the throw strategy - CastleConfiguration expectedConfiguration = CastleConfigurationBuilder - .defaultConfigBuilder() - .withApiSecret("1234") - .withCastleAppId("test_app_id_env") - .withAuthenticateFailoverStrategy(new AuthenticateFailoverStrategy()) - .build(); - - // when then - testLoad(expectedConfiguration); - } - - @Test public void ignoreNonExistingPropertiesFileTest() throws CastleSdkConfigurationException { //given a property file not existing is provided diff --git a/src/test/java/io/castle/client/internal/utils/VerdictTransportModelTest.java b/src/test/java/io/castle/client/internal/utils/VerdictTransportModelTest.java deleted file mode 100644 index 057c1279..00000000 --- a/src/test/java/io/castle/client/internal/utils/VerdictTransportModelTest.java +++ /dev/null @@ -1,40 +0,0 @@ -package io.castle.client.internal.utils; - -import io.castle.client.internal.json.CastleGsonModel; -import io.castle.client.model.AuthenticateAction; -import io.castle.client.model.RiskPolicyType; -import org.assertj.core.api.Assertions; -import org.junit.Test; - -public class VerdictTransportModelTest { - - @Test - public void transportProtocolIsWellDeserialized() { - // given a json input value - String json = "{\n" + - " \"action\": \"deny\",\n" + - " \"user_id\": \"12345\",\n" + - " \"device_token\": \"abcdefg1234\",\n" + - " \"risk_policy\": {\n" + - " \"id\": \"q-rbeMzBTdW2Fd09sbz55A\",\n" + - " \"revision_id\": \"pke4zqO2TnqVr-NHJOAHEg\",\n" + - " \"name\": \"Block Users from X\",\n" + - " \"type\": \"bot\"\n" + - " }\n" + - "}"; - // and castle gson model - CastleGsonModel model = new CastleGsonModel(); - - - // when the Verdict transport object is deserialized - VerdictTransportModel loaded = model.getGson().fromJson(json, VerdictTransportModel.class); - - Assertions.assertThat(loaded.getAction()).isEqualTo(AuthenticateAction.DENY); - Assertions.assertThat(loaded.getDeviceToken()).isEqualTo("abcdefg1234"); - Assertions.assertThat(loaded.getUserId()).isEqualTo("12345"); - Assertions.assertThat(loaded.getRiskPolicy().getId()).isEqualTo("q-rbeMzBTdW2Fd09sbz55A"); - Assertions.assertThat(loaded.getRiskPolicy().getRevisionId()).isEqualTo("pke4zqO2TnqVr-NHJOAHEg"); - Assertions.assertThat(loaded.getRiskPolicy().getName()).isEqualTo("Block Users from X"); - Assertions.assertThat(loaded.getRiskPolicy().getType()).isEqualTo(RiskPolicyType.BOT); - } -} diff --git a/src/test/java/io/castle/client/model/AuthenticationActionTest.java b/src/test/java/io/castle/client/model/AuthenticationActionTest.java deleted file mode 100644 index 57c1e5d0..00000000 --- a/src/test/java/io/castle/client/model/AuthenticationActionTest.java +++ /dev/null @@ -1,46 +0,0 @@ -package io.castle.client.model; - -import org.junit.Assert; -import org.junit.Test; - -public class AuthenticationActionTest { - - @Test - public void testFromAllow() { - testFromActionMethod("AllOw", AuthenticateAction.ALLOW); - } - - @Test - public void testFromDeny() { - testFromActionMethod("Deny", AuthenticateAction.DENY); - } - - @Test - public void testFromChallenge() { - testFromActionMethod("cHallenge", AuthenticateAction.CHALLENGE); - } - - @Test - public void testFromNull() { - testFromActionMethod(null, null); - } - - @Test - public void testFromEmptyString() { - testFromActionMethod("", null); - } - - @Test - public void testFromWrongString() { - testFromActionMethod("getMeANull", null); - } - - private void testFromActionMethod(String from, AuthenticateAction expected) { - - AuthenticateAction authenticateAction = AuthenticateAction.fromAction(from); - Assert.assertEquals(expected, authenticateAction); - - } - - -} diff --git a/src/test/java/io/castle/client/model/CastleConfigurationBuilderTest.java b/src/test/java/io/castle/client/model/CastleConfigurationBuilderTest.java index 7805885c..a5eddd87 100644 --- a/src/test/java/io/castle/client/model/CastleConfigurationBuilderTest.java +++ b/src/test/java/io/castle/client/model/CastleConfigurationBuilderTest.java @@ -5,6 +5,8 @@ import org.assertj.core.api.Assertions; import org.junit.Test; +import java.time.Duration; + public class CastleConfigurationBuilderTest { @Test @@ -23,10 +25,20 @@ public void buildDefaultConfiguration() throws CastleSdkConfigurationException { Assertions.assertThat(config.getDenyListHeaders()).contains("cookie"); Assertions.assertThat(config.getDenyListHeaders()).contains("authorization"); Assertions.assertThat(config.getAllowListHeaders()).isEmpty(); - Assertions.assertThat(config.getAuthenticateFailoverStrategy().getDefaultAction()).isEqualTo(AuthenticateAction.ALLOW); - Assertions.assertThat(config.getTimeout()).isEqualTo(500); + Assertions.assertThat(config.getTimeout()).isEqualTo(1000); + + } + + @Test + public void buildConfigurationWithDurationTimeout() throws CastleSdkConfigurationException { + CastleConfiguration config = CastleConfigurationBuilder.defaultConfigBuilder() + .withApiSecret("TestApiSecret") + .withCastleAppId("TestCastleAppId") + .withTimeout(Duration.ofSeconds(2)) + .build(); + Assertions.assertThat(config.getTimeout()).isEqualTo(2000); } @Test(expected = CastleSdkConfigurationException.class) diff --git a/src/test/java/io/castle/client/model/CastleMessageTest.java b/src/test/java/io/castle/client/model/CastleMessageTest.java deleted file mode 100644 index 1cbbce0e..00000000 --- a/src/test/java/io/castle/client/model/CastleMessageTest.java +++ /dev/null @@ -1,129 +0,0 @@ -package io.castle.client.model; - -import com.google.common.collect.ImmutableMap; -import com.google.gson.JsonObject; -import com.google.gson.JsonParser; -import io.castle.client.internal.json.CastleGsonModel; -import org.assertj.core.api.Assertions; -import org.junit.Assert; -import org.junit.Test; - -import java.util.HashMap; - -public class CastleMessageTest { - - private CastleGsonModel model = new CastleGsonModel(); - - @Test - public void jsonSerialized() { - // Given - CastleMessage message = new CastleMessage("event"); - message.setCreatedAt("2018-01-01"); - message.setTimestamp("2018-01-01"); - message.setDeviceToken("1234"); - message.setReviewId("2345"); - message.setProperties(ImmutableMap.builder() - .put("key", "val") - .build()); - message.setUserId("3456"); - message.setUserTraits(ImmutableMap.builder() - .put("key", "val") - .build()); - - // When - String payloadJson = model.getGson().toJson(message); - String expected = "{\"created_at\":\"2018-01-01\",\"timestamp\":\"2018-01-01\",\"device_token\":\"1234\",\"event\":\"event\",\"properties\":{\"key\":\"val\"},\"review_id\":\"2345\",\"user_id\":\"3456\",\"user_traits\":{\"key\":\"val\"}}"; - - // Then - Assertions.assertThat(JsonParser.parseString(payloadJson)).isEqualTo(JsonParser.parseString(expected)); - - } - - @Test - public void fullBuilderJson() { - // Given - CastleMessage message = CastleMessage.builder("event") - .createdAt("2018-01-01") - .timestamp("2018-01-01") - .deviceToken("1234") - .reviewId("2345") - .properties(ImmutableMap.builder() - .put("key", "val") - .build()) - .userId("3456") - .userTraits(ImmutableMap.builder() - .put("key", "val") - .build()) - .build(); - - Assert.assertEquals(message.getCreatedAt(), "2018-01-01"); - Assert.assertEquals(message.getDeviceToken(), "1234"); - Assert.assertEquals(message.getReviewId(), "2345"); - Assert.assertEquals(message.getUserId(), "3456"); - Assert.assertEquals(message.getEvent(), "event"); - Assert.assertEquals(message.getUserTraits(), ImmutableMap.builder() - .put("key", "val") - .build()); - Assert.assertEquals(message.getProperties(), ImmutableMap.builder() - .put("key", "val") - .build()); - - // When - String payloadJson = model.getGson().toJson(message); - String expected = "{\"created_at\":\"2018-01-01\",\"timestamp\":\"2018-01-01\",\"device_token\":\"1234\",\"event\":\"event\",\"properties\":{\"key\":\"val\"},\"review_id\":\"2345\",\"user_id\":\"3456\",\"user_traits\":{\"key\":\"val\"}}"; - - // Then - Assertions.assertThat(JsonParser.parseString(payloadJson)).isEqualTo(JsonParser.parseString(expected)); - } - - @Test - public void properties() { - JsonObject jsonObject = new JsonObject(); - jsonObject.addProperty("key", "value"); - - CastleMessage message = CastleMessage.builder("event") - .properties(jsonObject) - .build(); - - String payloadJson = model.getGson().toJson(message); - String expected = "{\"event\":\"event\",\"properties\":{\"key\":\"value\"}}"; - - Assertions.assertThat(JsonParser.parseString(payloadJson)).isEqualTo(JsonParser.parseString(expected)); - } - - @Test - public void otherProperties() { - CastleMessage message = CastleMessage.builder("event") - .put("key", "value") - .build(); - - String payloadJson = model.getGson().toJson(message); - - Assertions.assertThat(payloadJson).isEqualTo("{\"event\":\"event\",\"key\":\"value\"}"); - - HashMap other = new HashMap(); - other.put("key", "value"); - - message = CastleMessage.builder("event") - .other(other) - .build(); - - payloadJson = model.getGson().toJson(message); - - Assertions.assertThat(payloadJson).isEqualTo("{\"event\":\"event\",\"key\":\"value\"}"); - } - - @Test(expected = NullPointerException.class) - public void propertiesNull() { - CastleMessage message = CastleMessage.builder("event") - .properties(null) - .build(); - } - - @Test(expected = NullPointerException.class) - public void userTraitsNull() { - CastleMessage message = CastleMessage.builder("event") - .userTraits(null) - .build(); - } -} diff --git a/src/test/java/io/castle/client/model/CastleUserDeviceContextTest.java b/src/test/java/io/castle/client/model/CastleUserDeviceContextTest.java deleted file mode 100644 index b46563bf..00000000 --- a/src/test/java/io/castle/client/model/CastleUserDeviceContextTest.java +++ /dev/null @@ -1,57 +0,0 @@ -package io.castle.client.model; - -import com.google.gson.JsonParser; -import io.castle.client.internal.json.CastleGsonModel; -import io.castle.client.utils.DeviceUtils; -import org.assertj.core.api.Assertions; -import org.junit.Assert; -import org.junit.Test; - -import java.util.ArrayList; -import java.util.List; - -public class CastleUserDeviceContextTest { - private CastleGsonModel model = new CastleGsonModel(); - - @Test - public void jsonSerialized() { - // Given - CastleUserDeviceContext deviceContext = new CastleUserDeviceContext(); - - // When - String payloadJson = model.getGson().toJson(deviceContext); - - // Then - Assertions.assertThat(payloadJson).isEqualTo("{}"); - } - - @Test - public void fullBuilderJson() { - // Given - CastleUserDeviceContext deviceContext = new CastleUserDeviceContext(); - deviceContext.setIp(DeviceUtils.CONTEXT_IP); - deviceContext.setType(DeviceUtils.CONTEXT_TYPE); - - DeviceUserAgent userAgent = new DeviceUserAgent(); - userAgent.setBrowser(DeviceUtils.USER_AGENT_BROWSER); - userAgent.setDevice(DeviceUtils.USER_AGENT_DEVICE); - userAgent.setFamily(DeviceUtils.USER_AGENT_FAMILY); - userAgent.setMobile(DeviceUtils.USER_AGENT_MOBILE); - userAgent.setOs(DeviceUtils.USER_AGENT_OS); - userAgent.setPlatform(DeviceUtils.USER_AGENT_PLATFORM); - userAgent.setRaw(DeviceUtils.USER_AGENT_RAW); - userAgent.setVersion(DeviceUtils.USER_AGENT_VERSION); - deviceContext.setUserAgent(userAgent); - - // When - String payloadJson = model.getGson().toJson(deviceContext); - String expected = "{\"ip\":\"1.1.1.1\",\"user_agent\":{\"raw\":\"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/67.0.3396.87 Safari/537.36 OPR/54.0.2952.51\",\"browser\":\"Opera\",\"version\":\"54.0.2952\",\"os\":\"Mac OS X 10.13.6\",\"mobile\":false,\"platform\":\"Mac OS X\",\"device\":\"Unknown\",\"family\":\"Opera\"},\"type\":\"desktop\"}"; - - // Then - Assertions.assertThat(JsonParser.parseString(payloadJson)).isEqualTo(JsonParser.parseString(expected)); - - Assert.assertEquals(deviceContext.getIp(), DeviceUtils.CONTEXT_IP); - Assert.assertEquals(deviceContext.getType(), DeviceUtils.CONTEXT_TYPE); - Assert.assertEquals(deviceContext.getUserAgent(), userAgent); - } -} diff --git a/src/test/java/io/castle/client/model/CastleUserDeviceTest.java b/src/test/java/io/castle/client/model/CastleUserDeviceTest.java deleted file mode 100644 index 8011231c..00000000 --- a/src/test/java/io/castle/client/model/CastleUserDeviceTest.java +++ /dev/null @@ -1,72 +0,0 @@ -package io.castle.client.model; - -import com.google.gson.JsonParser; -import io.castle.client.internal.json.CastleGsonModel; -import io.castle.client.utils.DeviceUtils; -import org.assertj.core.api.Assertions; -import org.junit.Assert; -import org.junit.Test; - -public class CastleUserDeviceTest { - - private final CastleGsonModel model = new CastleGsonModel(); - - @Test - public void jsonSerialized() { - // Given - CastleUserDevice device = new CastleUserDevice(); - - // When - String payloadJson = model.getGson().toJson(device); - String expected = "{\"risk\":0.0,\"is_current_device\":false}"; - - // Then - Assert.assertEquals(JsonParser.parseString(payloadJson), JsonParser.parseString(expected)); - } - - @Test - public void fullBuilderJson() { - // Given - CastleUserDevice device = new CastleUserDevice(); - device.setToken(DeviceUtils.DEVICE_TOKEN); - device.setCreatedAt(DeviceUtils.DEVICE_CREATED_AT); - device.setLastSeenAt(DeviceUtils.DEVICE_LAST_SEEN_AT); - device.setApprovedAt(DeviceUtils.DEVICE_APPROVED_AT); - device.setEscalatedAt(DeviceUtils.DEVICE_ESCALATED_AT); - device.setMitigatedAt(DeviceUtils.DEVICE_MITIGATED_AT); - device.setCurrentDevice(true); - - CastleUserDeviceContext deviceContext = new CastleUserDeviceContext(); - deviceContext.setIp(DeviceUtils.CONTEXT_IP); - deviceContext.setType(DeviceUtils.CONTEXT_TYPE); - - DeviceUserAgent userAgent = new DeviceUserAgent(); - userAgent.setBrowser(DeviceUtils.USER_AGENT_BROWSER); - userAgent.setDevice(DeviceUtils.USER_AGENT_DEVICE); - userAgent.setFamily(DeviceUtils.USER_AGENT_FAMILY); - userAgent.setMobile(DeviceUtils.USER_AGENT_MOBILE); - userAgent.setOs(DeviceUtils.USER_AGENT_OS); - userAgent.setPlatform(DeviceUtils.USER_AGENT_PLATFORM); - userAgent.setRaw(DeviceUtils.USER_AGENT_RAW); - userAgent.setVersion(DeviceUtils.USER_AGENT_VERSION); - deviceContext.setUserAgent(userAgent); - - device.setContext(deviceContext); - - // When - String payloadJson = model.getGson().toJson(device); - String expected = "{\"token\":\"abcdefg12345\",\"risk\":0.0,\"created_at\":\"2018-06-15T16:36:22.916Z\",\"last_seen_at\":\"2018-07-19T23:09:29.681Z\",\"context\":{\"ip\":\"1.1.1.1\",\"user_agent\":{\"raw\":\"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/67.0.3396.87 Safari/537.36 OPR/54.0.2952.51\",\"browser\":\"Opera\",\"version\":\"54.0.2952\",\"os\":\"Mac OS X 10.13.6\",\"mobile\":false,\"platform\":\"Mac OS X\",\"device\":\"Unknown\",\"family\":\"Opera\"},\"type\":\"desktop\"},\"is_current_device\":true}"; - - // Then - Assertions.assertThat(JsonParser.parseString(payloadJson)).isEqualTo(JsonParser.parseString(expected)); - - Assert.assertEquals(device.getToken(), DeviceUtils.DEVICE_TOKEN); - Assert.assertEquals(device.getCreatedAt(), DeviceUtils.DEVICE_CREATED_AT); - Assert.assertEquals(device.getLastSeenAt(), DeviceUtils.DEVICE_LAST_SEEN_AT); - Assert.assertEquals(device.getApprovedAt(), DeviceUtils.DEVICE_APPROVED_AT); - Assert.assertEquals(device.getEscalatedAt(), DeviceUtils.DEVICE_ESCALATED_AT); - Assert.assertEquals(device.getMitigatedAt(), DeviceUtils.DEVICE_MITIGATED_AT); - Assert.assertTrue(device.isCurrentDevice()); - Assert.assertEquals(device.getContext(), deviceContext); - } -} diff --git a/src/test/java/io/castle/client/model/CastleUserDevicesTest.java b/src/test/java/io/castle/client/model/CastleUserDevicesTest.java deleted file mode 100644 index a78050b0..00000000 --- a/src/test/java/io/castle/client/model/CastleUserDevicesTest.java +++ /dev/null @@ -1,78 +0,0 @@ -package io.castle.client.model; - -import com.google.gson.JsonParser; -import io.castle.client.internal.json.CastleGsonModel; -import io.castle.client.utils.DeviceUtils; -import org.assertj.core.api.Assertions; -import org.junit.Assert; -import org.junit.Test; - -import java.util.ArrayList; -import java.util.List; - -public class CastleUserDevicesTest { - private CastleGsonModel model = new CastleGsonModel(); - - @Test - public void jsonSerialized() { - // Given - CastleUserDevices devices = new CastleUserDevices(); - - // When - String payloadJson = model.getGson().toJson(devices); - - // Then - Assertions.assertThat(payloadJson).isEqualTo("{\"total_count\":0}"); - } - - @Test - public void fullBuilderJson() { - // Given - CastleUserDevices devices = new CastleUserDevices(); - - List devicesList = new ArrayList<>(); - - CastleUserDevice device = new CastleUserDevice(); - device.setToken(DeviceUtils.DEVICE_TOKEN); - device.setCreatedAt(DeviceUtils.DEVICE_CREATED_AT); - device.setLastSeenAt(DeviceUtils.DEVICE_LAST_SEEN_AT); - device.setApprovedAt(DeviceUtils.DEVICE_APPROVED_AT); - device.setEscalatedAt(DeviceUtils.DEVICE_ESCALATED_AT); - device.setMitigatedAt(DeviceUtils.DEVICE_MITIGATED_AT); - device.setCurrentDevice(true); - - CastleUserDeviceContext deviceContext = new CastleUserDeviceContext(); - deviceContext.setIp(DeviceUtils.CONTEXT_IP); - deviceContext.setType(DeviceUtils.CONTEXT_TYPE); - - DeviceUserAgent userAgent = new DeviceUserAgent(); - userAgent.setBrowser(DeviceUtils.USER_AGENT_BROWSER); - userAgent.setDevice(DeviceUtils.USER_AGENT_DEVICE); - userAgent.setFamily(DeviceUtils.USER_AGENT_FAMILY); - userAgent.setMobile(DeviceUtils.USER_AGENT_MOBILE); - userAgent.setOs(DeviceUtils.USER_AGENT_OS); - userAgent.setPlatform(DeviceUtils.USER_AGENT_PLATFORM); - userAgent.setRaw(DeviceUtils.USER_AGENT_RAW); - userAgent.setVersion(DeviceUtils.USER_AGENT_VERSION); - deviceContext.setUserAgent(userAgent); - - device.setContext(deviceContext); - - devicesList.add(device); - - devices.setDevices(devicesList); - - // When - String payloadJson = model.getGson().toJson(devices); - String expected = "{\"total_count\":1,\"data\":[{\"token\":\"abcdefg12345\",\"risk\":0.0,\"created_at\":\"2018-06-15T16:36:22.916Z\",\"last_seen_at\":\"2018-07-19T23:09:29.681Z\",\"context\":{\"ip\":\"1.1.1.1\",\"user_agent\":{\"raw\":\"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/67.0.3396.87 Safari/537.36 OPR/54.0.2952.51\",\"browser\":\"Opera\",\"version\":\"54.0.2952\",\"os\":\"Mac OS X 10.13.6\",\"mobile\":false,\"platform\":\"Mac OS X\",\"device\":\"Unknown\",\"family\":\"Opera\"},\"type\":\"desktop\"},\"is_current_device\":true}]}"; - - // Then - Assertions.assertThat(JsonParser.parseString(payloadJson)).isEqualTo(JsonParser.parseString(expected)); - - Assert.assertEquals(devices.getTotalCount(), devicesList.size()); - Assert.assertEquals(devices.getDevices(), devicesList); - - devices.setDevices(null); - Assert.assertEquals(0, devices.getTotalCount()); - } -} diff --git a/src/test/java/io/castle/client/model/DeviceUserAgentTest.java b/src/test/java/io/castle/client/model/DeviceUserAgentTest.java deleted file mode 100644 index 022f48ca..00000000 --- a/src/test/java/io/castle/client/model/DeviceUserAgentTest.java +++ /dev/null @@ -1,53 +0,0 @@ -package io.castle.client.model; - -import com.google.gson.JsonParser; -import io.castle.client.internal.json.CastleGsonModel; -import io.castle.client.utils.DeviceUtils; -import org.assertj.core.api.Assertions; -import org.junit.Assert; -import org.junit.Test; - -public class DeviceUserAgentTest { - private CastleGsonModel model = new CastleGsonModel(); - - @Test - public void jsonSerialized() { - // Given - DeviceUserAgent userAgent = new DeviceUserAgent(); - - // When - String payloadJson = model.getGson().toJson(userAgent); - - // Then - Assertions.assertThat(payloadJson).isEqualTo("{\"mobile\":false}"); - } - - @Test - public void fullBuilderJson() { - // Given - DeviceUserAgent userAgent = new DeviceUserAgent(); - userAgent.setBrowser(DeviceUtils.USER_AGENT_BROWSER); - userAgent.setDevice(DeviceUtils.USER_AGENT_DEVICE); - userAgent.setFamily(DeviceUtils.USER_AGENT_FAMILY); - userAgent.setMobile(DeviceUtils.USER_AGENT_MOBILE); - userAgent.setOs(DeviceUtils.USER_AGENT_OS); - userAgent.setPlatform(DeviceUtils.USER_AGENT_PLATFORM); - userAgent.setRaw(DeviceUtils.USER_AGENT_PLATFORM); - userAgent.setVersion(DeviceUtils.USER_AGENT_PLATFORM); - - // When - String payloadJson = model.getGson().toJson(userAgent); - String expected = "{\"raw\":\"Mac OS X\",\"browser\":\"Opera\",\"version\":\"Mac OS X\",\"os\":\"Mac OS X 10.13.6\",\"mobile\":false,\"platform\":\"Mac OS X\",\"device\":\"Unknown\",\"family\":\"Opera\"}"; - // Then - Assertions.assertThat(JsonParser.parseString(payloadJson)).isEqualTo(JsonParser.parseString(expected)); - - Assert.assertEquals(userAgent.getBrowser(), DeviceUtils.USER_AGENT_BROWSER); - Assert.assertEquals(userAgent.getDevice(), DeviceUtils.USER_AGENT_DEVICE); - Assert.assertEquals(userAgent.getFamily(), DeviceUtils.USER_AGENT_FAMILY); - Assert.assertEquals(userAgent.isMobile(), DeviceUtils.USER_AGENT_MOBILE); - Assert.assertEquals(userAgent.getOs(), DeviceUtils.USER_AGENT_OS); - Assert.assertEquals(userAgent.getPlatform(), DeviceUtils.USER_AGENT_PLATFORM); - Assert.assertEquals(userAgent.getRaw(), DeviceUtils.USER_AGENT_PLATFORM); - Assert.assertEquals(userAgent.getVersion(), DeviceUtils.USER_AGENT_PLATFORM); - } -} diff --git a/src/test/java/io/castle/client/utils/DeviceUtils.java b/src/test/java/io/castle/client/utils/DeviceUtils.java deleted file mode 100644 index b1692e68..00000000 --- a/src/test/java/io/castle/client/utils/DeviceUtils.java +++ /dev/null @@ -1,72 +0,0 @@ -package io.castle.client.utils; - -import io.castle.client.model.CastleUserDevice; -import io.castle.client.model.CastleUserDeviceContext; -import io.castle.client.model.CastleUserDevices; -import io.castle.client.model.DeviceUserAgent; - -import java.util.Collections; - -public class DeviceUtils { - - public static final String DEVICE_TOKEN = "abcdefg12345"; - public static final double DEVICE_RISK = 0.000154; - public static final String DEVICE_CREATED_AT = "2018-06-15T16:36:22.916Z"; - public static final String DEVICE_LAST_SEEN_AT = "2018-07-19T23:09:29.681Z"; - public static final String DEVICE_APPROVED_AT = null; - public static final String DEVICE_ESCALATED_AT = null; - public static final String DEVICE_MITIGATED_AT = null; - - public static final String CONTEXT_TYPE = "desktop"; - public static final String CONTEXT_IP = "1.1.1.1"; - - public static final String USER_AGENT_FAMILY = "Opera"; - public static final String USER_AGENT_DEVICE = "Unknown"; - public static final String USER_AGENT_PLATFORM = "Mac OS X"; - public static final String USER_AGENT_OS = "Mac OS X 10.13.6"; - public static final String USER_AGENT_VERSION = "54.0.2952"; - public static final String USER_AGENT_BROWSER = "Opera"; - public static final String USER_AGENT_RAW = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/67.0.3396.87 Safari/537.36 OPR/54.0.2952.51"; - public static final boolean USER_AGENT_MOBILE = false; - - public static CastleUserDevice createExpectedDevice() { - CastleUserDevice device = new CastleUserDevice(); - device.setToken(DEVICE_TOKEN); - device.setRisk(DEVICE_RISK); - device.setCreatedAt(DEVICE_CREATED_AT); - device.setLastSeenAt(DEVICE_LAST_SEEN_AT); - device.setApprovedAt(null); - device.setEscalatedAt(null); - device.setMitigatedAt(null); - - CastleUserDeviceContext deviceContext = new CastleUserDeviceContext(); - deviceContext.setIp(CONTEXT_IP); - deviceContext.setType(CONTEXT_TYPE); - - DeviceUserAgent userAgent = new DeviceUserAgent(); - userAgent.setFamily(USER_AGENT_FAMILY); - userAgent.setDevice(USER_AGENT_DEVICE); - userAgent.setPlatform(USER_AGENT_PLATFORM); - userAgent.setOs(USER_AGENT_OS); - userAgent.setVersion(USER_AGENT_VERSION); - userAgent.setBrowser(USER_AGENT_BROWSER); - userAgent.setRaw(USER_AGENT_RAW); - userAgent.setMobile(USER_AGENT_MOBILE); - - deviceContext.setUserAgent(userAgent); - - device.setContext(deviceContext); - - return device; - } - - public static CastleUserDevices createExpectedDevices() { - CastleUserDevices devices = new CastleUserDevices(); - devices.setDevices(Collections.singletonList(createExpectedDevice())); - return devices; - } - - public static String getDefaultDeviceJSON() { - return "{\"token\":\"abcdefg12345\",\"risk\":0.000154,\"created_at\":\"2018-06-15T16:36:22.916Z\",\"last_seen_at\":\"2018-07-19T23:09:29.681Z\",\"approved_at\":null,\"escalated_at\":null,\"mitigated_at\":null,\"context\":{\"ip\":\"1.1.1.1\",\"location\":{\"country_code\":\"US\",\"country\":\"United States\",\"region\":\"New Jersey\",\"region_code\":\"NJ\",\"city\":\"Lyndhurst\",\"lat\":40.7923,\"lon\":-74.1001},\"user_agent\":{\"raw\":\"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/67.0.3396.87 Safari/537.36 OPR/54.0.2952.51\",\"browser\":\"Opera\",\"version\":\"54.0.2952\",\"os\":\"Mac OS X 10.13.6\",\"mobile\":false,\"platform\":\"Mac OS X\",\"device\":\"Unknown\",\"family\":\"Opera\"},\"type\":\"desktop\"}}"; - } -} diff --git a/src/test/resources/castle_sdk.properties b/src/test/resources/castle_sdk.properties index c22bb141..2b8d1b56 100644 --- a/src/test/resources/castle_sdk.properties +++ b/src/test/resources/castle_sdk.properties @@ -4,5 +4,4 @@ allow_list=TestAllow,User-Agent,Accept-Language,Accept-Encoding,Accept-Charset,A deny_list=TestDeny,Cookie timeout=100 backend_provider=OKHTTP -failover_strategy=CHALLENGE base_url=https://testing.api.dev.castle/v1/