diff --git a/.cspell.json b/.cspell.json index bb8aef81..57a9d3af 100644 --- a/.cspell.json +++ b/.cspell.json @@ -99,7 +99,9 @@ "detok", "qhdmceurtnlz", "ngrok", - "obac" + "obac", + "siom", + "recordss" ], "languageSettings": [ { @@ -119,7 +121,7 @@ "**/target/**", "*.lock", "Rule/**", - "src/main/java/com/skyflow/generated/**", + "**/generated/**", "**/*.ts", "**/processed-*", "samples/src/main/java/com/example/credentials.json", diff --git a/.github/workflows/endorlabsScan.yml b/.github/workflows/endorlabsScan.yml index 3c316417..927009fe 100644 --- a/.github/workflows/endorlabsScan.yml +++ b/.github/workflows/endorlabsScan.yml @@ -36,6 +36,15 @@ jobs: name: "credentials.json" json: ${{ secrets.TEST_CREDENTIALS_FILE_STRING }} + - name: Distribute test fixtures to modules + run: | + # Surefire runs each module's tests with the module directory as the working + # directory, so dotenv and ./credentials.json lookups miss the repo-root copies. + for module in common v2 flowvault; do + cp .env "$module/.env" + cp credentials.json "$module/credentials.json" + done + - name: Compile Package run: mvn -B package -f pom.xml -Dmaven.javadoc.skip=true diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 4386de84..c7360ea3 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -30,6 +30,15 @@ jobs: echo TEST_EXPIRED_TOKEN=${{ secrets.TEST_EXPIRED_TOKEN }} >> .env echo TEST_REUSABLE_TOKEN=${{ secrets.TEST_REUSABLE_TOKEN }} >> .env + - name: Distribute test fixtures to modules + run: | + # Surefire runs each module's tests with the module directory as the working + # directory, so dotenv and ./credentials.json lookups miss the repo-root copies. + for module in common v2 flowvault; do + cp .env "$module/.env" + cp credentials.json "$module/credentials.json" + done + - name: Build & Run tests with Maven run: mvn -B package -f pom.xml -Dmaven.javadoc.skip=true diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index bb89e32f..0002d3eb 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -39,14 +39,27 @@ jobs: echo TEST_EXPIRED_TOKEN=${{ secrets.TEST_EXPIRED_TOKEN }} >> .env echo TEST_REUSABLE_TOKEN=${{ secrets.TEST_REUSABLE_TOKEN }} >> .env + - name: Distribute test fixtures to modules + run: | + # Surefire runs each module's tests with the module directory as the working + # directory, so dotenv and ./credentials.json lookups miss the repo-root copies. + for module in common v2 flowvault; do + cp .env "$module/.env" + cp credentials.json "$module/credentials.json" + done + - name: Build & Run tests with Maven run: mvn -B package -f pom.xml -Dmaven.javadoc.skip=true + # JaCoCo writes one report per module. The root pom is pom + # with no sources, so target/site/jacoco/jacoco.xml - correct when this was a + # single-module build - is never generated post common/v2/flowvault split, and + # codecov was silently uploading nothing. - name: Codecov uses: codecov/codecov-action@v2.1.0 with: token: ${{ secrets.CODECOV_REPO_UPLOAD_TOKEN }} - files: target/site/jacoco/jacoco.xml + files: common/target/site/jacoco/jacoco.xml,v2/target/site/jacoco/jacoco.xml,flowvault/target/site/jacoco/jacoco.xml name: codecov-skyflow-java verbose: true diff --git a/.github/workflows/shared-build-and-deploy.yml b/.github/workflows/shared-build-and-deploy.yml index 894b6ec2..0a3a4c84 100644 --- a/.github/workflows/shared-build-and-deploy.yml +++ b/.github/workflows/shared-build-and-deploy.yml @@ -131,6 +131,15 @@ jobs: name: "credentials.json" json: ${{ secrets.TEST_CREDENTIALS_FILE_STRING }} + - name: Distribute test fixtures to modules + run: | + # Surefire runs each module's tests with the module directory as the working + # directory, so dotenv and ./credentials.json lookups miss the repo-root copies. + for module in common v2 flowvault; do + cp .env "$module/.env" + cp credentials.json "$module/credentials.json" + done + - name: Publish package run: | if [[ "${{ inputs.tag }}" == "internal" ]]; then diff --git a/codecov.yml b/codecov.yml index 05c58c3e..2a9a1108 100644 --- a/codecov.yml +++ b/codecov.yml @@ -1,5 +1,10 @@ comment: false +# Component paths are prefixed with **/ (quoted - a bare leading * is a YAML +# alias) so they match every Maven module. Before the common/v2/flowvault +# split sources lived at src/main/java/...; they are now +# /src/main/java/..., so the unprefixed globs matched zero files and +# every component reported no coverage data. component_management: default_rules: statuses: @@ -9,44 +14,44 @@ component_management: - component_id: service_account name: Service Account paths: - - src/main/java/com/skyflow/serviceaccount/** + - "**/src/main/java/com/skyflow/serviceaccount/**" - component_id: vault_data name: Vault Data paths: - - src/main/java/com/skyflow/vault/data/** + - "**/src/main/java/com/skyflow/vault/data/**" - component_id: vault_tokens name: Vault Tokens paths: - - src/main/java/com/skyflow/vault/tokens/** + - "**/src/main/java/com/skyflow/vault/tokens/**" - component_id: vault_connection name: Vault Connection paths: - - src/main/java/com/skyflow/vault/connection/** + - "**/src/main/java/com/skyflow/vault/connection/**" - component_id: vault_controller name: Vault Controller paths: - - src/main/java/com/skyflow/vault/controller/** + - "**/src/main/java/com/skyflow/vault/controller/**" - component_id: vault_detect name: Detect paths: - - src/main/java/com/skyflow/vault/detect/** + - "**/src/main/java/com/skyflow/vault/detect/**" - component_id: vault_audit name: Audit paths: - - src/main/java/com/skyflow/vault/audit/** + - "**/src/main/java/com/skyflow/vault/audit/**" - component_id: vault_bin name: BIN Lookup paths: - - src/main/java/com/skyflow/vault/bin/** + - "**/src/main/java/com/skyflow/vault/bin/**" - component_id: config name: Config paths: - - src/main/java/com/skyflow/config/** + - "**/src/main/java/com/skyflow/config/**" - component_id: utils name: Utils paths: - - src/main/java/com/skyflow/utils/** + - "**/src/main/java/com/skyflow/utils/**" - component_id: errors name: Errors paths: - - src/main/java/com/skyflow/errors/** + - "**/src/main/java/com/skyflow/errors/**" diff --git a/common/src/main/java/com/skyflow/enums/CustomHeaderKey.java b/common/src/main/java/com/skyflow/enums/CustomHeaderKey.java index 03649eee..8d3285ae 100644 --- a/common/src/main/java/com/skyflow/enums/CustomHeaderKey.java +++ b/common/src/main/java/com/skyflow/enums/CustomHeaderKey.java @@ -1,9 +1,9 @@ package com.skyflow.enums; public enum CustomHeaderKey { - SkyflowAccountID("x-skyflow-account-id"), + SkyflowAccountId("x-skyflow-account-id"), SkyflowAccountName("x-skyflow-account-name"), - RequestIDHeader("x-request-id"); + RequestIdHeader("x-request-id"); private final String value; diff --git a/common/src/main/java/com/skyflow/errors/ErrorMessage.java b/common/src/main/java/com/skyflow/errors/ErrorMessage.java index d835cb98..ccad4b13 100644 --- a/common/src/main/java/com/skyflow/errors/ErrorMessage.java +++ b/common/src/main/java/com/skyflow/errors/ErrorMessage.java @@ -79,6 +79,7 @@ public enum ErrorMessage { EmptyKeyInTokens("%s0 Validation error. Invalid key tokens. Specify a valid key."), EmptyValueInTokens("%s0 Validation error. Invalid value in tokens. Specify a valid value."), EmptyUpsert("%s0 Validation error. 'upsert' key can't be empty. Specify an upsert column."), + InvalidUpsertUpdateType("%s0 Validation error. Invalid upsert updateType. Specify either 'UPDATE' or 'REPLACE'."), EmptyUpsertValues("%s0 Validation error. Upsert column values can't be empty. Specify at least one upsert column."), HomogenousNotSupportedWithUpsert("%s0 Validation error. 'homogenous' is not supported with 'upsert'. Specify either 'homogenous' or 'upsert'."), TokensPassedForTokenModeDisable("%s0 Validation error. 'tokenMode' wasn't specified. Set 'tokenMode' to 'ENABLE' to insert tokens."), diff --git a/common/src/main/java/com/skyflow/logs/ErrorLogs.java b/common/src/main/java/com/skyflow/logs/ErrorLogs.java index 0156680c..b1f0109b 100644 --- a/common/src/main/java/com/skyflow/logs/ErrorLogs.java +++ b/common/src/main/java/com/skyflow/logs/ErrorLogs.java @@ -63,6 +63,7 @@ public enum ErrorLogs { EMPTY_OR_NULL_VALUE_IN_VALUES("Invalid %s1 request. Value can not be null or empty in values for key \"%s2\"."), EMPTY_OR_NULL_KEY_IN_VALUES("Invalid %s1 request. Key can not be null or empty in values"), EMPTY_UPSERT("Invalid %s1 request. Upsert can not be empty."), + INVALID_UPSERT_UPDATE_TYPE("Invalid %s1 request. Upsert updateType must be either UPDATE or REPLACE."), EMPTY_UPSERT_VALUES("Invalid %s1 request. Upsert values can not be empty."), HOMOGENOUS_NOT_SUPPORTED_WITH_UPSERT("Invalid %s1 request. Homogenous is not supported when upsert is passed."), TOKENS_NOT_ALLOWED_WITH_TOKEN_MODE_DISABLE("Invalid %s1 request. Tokens are not allowed when tokenMode is DISABLE."), diff --git a/common/src/main/java/com/skyflow/vault/data/BaseDetokenizeData.java b/common/src/main/java/com/skyflow/vault/data/BaseDetokenizeData.java index 0da23763..6ff0ee87 100644 --- a/common/src/main/java/com/skyflow/vault/data/BaseDetokenizeData.java +++ b/common/src/main/java/com/skyflow/vault/data/BaseDetokenizeData.java @@ -1,14 +1,7 @@ package com.skyflow.vault.data; +// Shared extension point for module-specific detokenize request data. Intentionally empty: +// v2 and flowvault no longer have any field in common here, so each owns its own state. +// Retained so the modules keep a shared supertype for future use. public class BaseDetokenizeData { - private final String token; - - public BaseDetokenizeData(String token) { - this.token = token; - } - - public String getToken() { - return this.token; - } - } diff --git a/common/src/main/java/com/skyflow/vault/data/BaseInsertRequest.java b/common/src/main/java/com/skyflow/vault/data/BaseInsertRequest.java index d2f6f812..04f82831 100644 --- a/common/src/main/java/com/skyflow/vault/data/BaseInsertRequest.java +++ b/common/src/main/java/com/skyflow/vault/data/BaseInsertRequest.java @@ -1,25 +1,7 @@ package com.skyflow.vault.data; -class BaseInsertRequest { - private final BaseInsertRequestBuilder builder; - - protected BaseInsertRequest(BaseInsertRequestBuilder builder) { - this.builder = builder; - } - - public String getTable() { - return this.builder.table; - } - - static class BaseInsertRequestBuilder { - protected String table; - protected BaseInsertRequestBuilder() { - } - - public BaseInsertRequestBuilder table(String table) { - this.table = table; - return this; - } - - } +// Shared extension point for module-specific insert requests. Intentionally empty: +// v2 and flowvault insert requests no longer have any field in common, so each owns +// its own state. Retained so the modules keep a shared supertype for future use. +public class BaseInsertRequest { } diff --git a/common/src/main/java/com/skyflow/vault/data/BaseInsertResponse.java b/common/src/main/java/com/skyflow/vault/data/BaseInsertResponse.java index 84196bf7..bb10a688 100644 --- a/common/src/main/java/com/skyflow/vault/data/BaseInsertResponse.java +++ b/common/src/main/java/com/skyflow/vault/data/BaseInsertResponse.java @@ -1,31 +1,7 @@ package com.skyflow.vault.data; -import com.google.gson.Gson; - -import java.util.ArrayList; -import java.util.HashMap; - +// Shared extension point for module-specific insert responses. Intentionally empty: +// v2 and flowvault insert responses no longer have any field in common, so each owns +// its own state. Retained so the modules keep a shared supertype for future use. public class BaseInsertResponse { - private final ArrayList> insertedFields; - private final ArrayList> errors; - - public BaseInsertResponse(ArrayList> insertedFields, ArrayList> errors) { - this.insertedFields = insertedFields; - this.errors = errors; - } - - public ArrayList> getInsertedFields() { - return insertedFields; - } - - public ArrayList> getErrors() { - return errors; - } - - @Override - public String toString() { - Gson gson = new Gson().newBuilder().serializeNulls().create(); - return gson.toJson(this); - } } - diff --git a/common/src/test/java/com/skyflow/vault/data/BaseDetokenizeDataTests.java b/common/src/test/java/com/skyflow/vault/data/BaseDetokenizeDataTests.java index 08b4f9da..ba75144c 100644 --- a/common/src/test/java/com/skyflow/vault/data/BaseDetokenizeDataTests.java +++ b/common/src/test/java/com/skyflow/vault/data/BaseDetokenizeDataTests.java @@ -6,23 +6,19 @@ public class BaseDetokenizeDataTests { @Test - public void testGetTokenReturnsConstructorValue() { - BaseDetokenizeData data = new BaseDetokenizeData("token-value"); + public void testInstantiationDoesNotThrow() { + BaseDetokenizeData data = new BaseDetokenizeData(); - Assert.assertEquals("token-value", data.getToken()); + Assert.assertNotNull(data); } @Test - public void testNullToken() { - BaseDetokenizeData data = new BaseDetokenizeData(null); + public void testUsableAsExtensionPointForSubclasses() { + // BaseDetokenizeData carries no state of its own; it exists purely so module-specific + // classes (v2's DetokenizeData, flowvault's TokenGroupRedactions) share a supertype. + BaseDetokenizeData data = new BaseDetokenizeData() { + }; - Assert.assertNull(data.getToken()); - } - - @Test - public void testEmptyToken() { - BaseDetokenizeData data = new BaseDetokenizeData(""); - - Assert.assertEquals("", data.getToken()); + Assert.assertTrue(data instanceof BaseDetokenizeData); } } diff --git a/common/src/test/java/com/skyflow/vault/data/BaseInsertRequestTests.java b/common/src/test/java/com/skyflow/vault/data/BaseInsertRequestTests.java index e8ce8e06..5b59e261 100644 --- a/common/src/test/java/com/skyflow/vault/data/BaseInsertRequestTests.java +++ b/common/src/test/java/com/skyflow/vault/data/BaseInsertRequestTests.java @@ -6,36 +6,19 @@ public class BaseInsertRequestTests { @Test - public void testBuilderSetsTable() { - BaseInsertRequest.BaseInsertRequestBuilder builder = new BaseInsertRequest.BaseInsertRequestBuilder(); - builder.table("test_table"); - BaseInsertRequest request = new BaseInsertRequest(builder); + public void testInstantiationDoesNotThrow() { + BaseInsertRequest request = new BaseInsertRequest(); - Assert.assertEquals("test_table", request.getTable()); + Assert.assertNotNull(request); } @Test - public void testBuilderTableMethodReturnsSameBuilderInstance() { - BaseInsertRequest.BaseInsertRequestBuilder builder = new BaseInsertRequest.BaseInsertRequestBuilder(); - BaseInsertRequest.BaseInsertRequestBuilder returned = builder.table("test_table"); + public void testUsableAsExtensionPointForSubclasses() { + // BaseInsertRequest carries no state of its own; it exists purely so module-specific + // InsertRequest classes (v2's and flowvault's) share a supertype. Verify the subtype relationship holds. + BaseInsertRequest request = new BaseInsertRequest() { + }; - Assert.assertSame(builder, returned); - } - - @Test - public void testNullTableWhenNeverSet() { - BaseInsertRequest.BaseInsertRequestBuilder builder = new BaseInsertRequest.BaseInsertRequestBuilder(); - BaseInsertRequest request = new BaseInsertRequest(builder); - - Assert.assertNull(request.getTable()); - } - - @Test - public void testEmptyTable() { - BaseInsertRequest.BaseInsertRequestBuilder builder = new BaseInsertRequest.BaseInsertRequestBuilder(); - builder.table(""); - BaseInsertRequest request = new BaseInsertRequest(builder); - - Assert.assertEquals("", request.getTable()); + Assert.assertTrue(request instanceof BaseInsertRequest); } } diff --git a/common/src/test/java/com/skyflow/vault/data/BaseInsertResponseTests.java b/common/src/test/java/com/skyflow/vault/data/BaseInsertResponseTests.java index 93b47f32..182d0d05 100644 --- a/common/src/test/java/com/skyflow/vault/data/BaseInsertResponseTests.java +++ b/common/src/test/java/com/skyflow/vault/data/BaseInsertResponseTests.java @@ -3,67 +3,22 @@ import org.junit.Assert; import org.junit.Test; -import java.util.ArrayList; -import java.util.HashMap; - public class BaseInsertResponseTests { @Test - public void testGettersReturnConstructorValues() { - ArrayList> insertedFields = new ArrayList<>(); - HashMap field = new HashMap<>(); - field.put("skyflow_id", "id-1"); - insertedFields.add(field); - - ArrayList> errors = new ArrayList<>(); - HashMap error = new HashMap<>(); - error.put("error", "some error"); - errors.add(error); - - BaseInsertResponse response = new BaseInsertResponse(insertedFields, errors); - - Assert.assertEquals(insertedFields, response.getInsertedFields()); - Assert.assertEquals(errors, response.getErrors()); - } - - @Test - public void testNullInsertedFieldsAndErrors() { - BaseInsertResponse response = new BaseInsertResponse(null, null); - - Assert.assertNull(response.getInsertedFields()); - Assert.assertNull(response.getErrors()); - } - - @Test - public void testEmptyInsertedFieldsAndErrors() { - BaseInsertResponse response = new BaseInsertResponse(new ArrayList<>(), new ArrayList<>()); - - Assert.assertTrue(response.getInsertedFields().isEmpty()); - Assert.assertTrue(response.getErrors().isEmpty()); - } - - @Test - public void testToStringWithPopulatedFieldsContainsValues() { - ArrayList> insertedFields = new ArrayList<>(); - HashMap field = new HashMap<>(); - field.put("skyflow_id", "id-1"); - insertedFields.add(field); - - BaseInsertResponse response = new BaseInsertResponse(insertedFields, new ArrayList<>()); - String result = response.toString(); + public void testInstantiationDoesNotThrow() { + BaseInsertResponse response = new BaseInsertResponse(); - Assert.assertNotNull(result); - Assert.assertTrue(result.contains("insertedFields")); - Assert.assertTrue(result.contains("skyflow_id")); - Assert.assertTrue(result.contains("id-1")); + Assert.assertNotNull(response); } @Test - public void testToStringWithNullFieldsDoesNotThrowAndSerializesNulls() { - BaseInsertResponse response = new BaseInsertResponse(null, null); - String result = response.toString(); + public void testUsableAsExtensionPointForSubclasses() { + // BaseInsertResponse carries no state of its own; it exists purely so module-specific + // InsertResponse classes (v2's and flowvault's) share a supertype. Verify the subtype relationship holds. + BaseInsertResponse response = new BaseInsertResponse() { + }; - Assert.assertNotNull(result); - Assert.assertTrue(result.contains("null")); + Assert.assertTrue(response instanceof BaseInsertResponse); } } diff --git a/common/src/test/java/com/skyflow/vault/data/RequestContextTests.java b/common/src/test/java/com/skyflow/vault/data/RequestContextTests.java index d8747425..0c6700d8 100644 --- a/common/src/test/java/com/skyflow/vault/data/RequestContextTests.java +++ b/common/src/test/java/com/skyflow/vault/data/RequestContextTests.java @@ -32,28 +32,28 @@ public void testGetHeadersReturnsEmptyMapByDefault() { @Test public void testAddHeaderIsReflectedInGetHeaders() { RequestContext context = new RequestContext("INSERT"); - context.addHeader(CustomHeaderKey.SkyflowAccountID, "account-id-value"); + context.addHeader(CustomHeaderKey.SkyflowAccountId, "account-id-value"); Map headers = context.getHeaders(); Assert.assertEquals(1, headers.size()); - Assert.assertEquals("account-id-value", headers.get(CustomHeaderKey.SkyflowAccountID)); + Assert.assertEquals("account-id-value", headers.get(CustomHeaderKey.SkyflowAccountId)); } @Test public void testAddHeaderOverwritesExistingValueForSameKey() { RequestContext context = new RequestContext("INSERT"); - context.addHeader(CustomHeaderKey.SkyflowAccountID, "first-value"); - context.addHeader(CustomHeaderKey.SkyflowAccountID, "second-value"); + context.addHeader(CustomHeaderKey.SkyflowAccountId, "first-value"); + context.addHeader(CustomHeaderKey.SkyflowAccountId, "second-value"); Assert.assertEquals(1, context.getHeaders().size()); - Assert.assertEquals("second-value", context.getHeaders().get(CustomHeaderKey.SkyflowAccountID)); + Assert.assertEquals("second-value", context.getHeaders().get(CustomHeaderKey.SkyflowAccountId)); } @Test public void testAddMultipleDistinctHeaders() { RequestContext context = new RequestContext("DETOKENIZE"); - context.addHeader(CustomHeaderKey.SkyflowAccountID, "account-id-value"); + context.addHeader(CustomHeaderKey.SkyflowAccountId, "account-id-value"); context.addHeader(CustomHeaderKey.SkyflowAccountName, "account-name-value"); Assert.assertEquals(2, context.getHeaders().size()); @@ -63,6 +63,6 @@ public void testAddMultipleDistinctHeaders() { public void testGetHeadersReturnsUnmodifiableMap() { RequestContext context = new RequestContext("INSERT"); - context.getHeaders().put(CustomHeaderKey.RequestIDHeader, "request-id-value"); + context.getHeaders().put(CustomHeaderKey.RequestIdHeader, "request-id-value"); } } diff --git a/common/src/test/java/com/skyflow/vault/data/RequestInterceptorTests.java b/common/src/test/java/com/skyflow/vault/data/RequestInterceptorTests.java index a03e773d..d7bd3193 100644 --- a/common/src/test/java/com/skyflow/vault/data/RequestInterceptorTests.java +++ b/common/src/test/java/com/skyflow/vault/data/RequestInterceptorTests.java @@ -8,12 +8,12 @@ public class RequestInterceptorTests { @Test public void testInterceptMutatesRequestContext() { - RequestInterceptor interceptor = context -> context.addHeader(CustomHeaderKey.SkyflowAccountID, "account-id-value"); + RequestInterceptor interceptor = context -> context.addHeader(CustomHeaderKey.SkyflowAccountId, "account-id-value"); RequestContext context = new RequestContext("INSERT"); interceptor.intercept(context); - Assert.assertEquals("account-id-value", context.getHeaders().get(CustomHeaderKey.SkyflowAccountID)); + Assert.assertEquals("account-id-value", context.getHeaders().get(CustomHeaderKey.SkyflowAccountId)); } @Test diff --git a/flowvault/src/main/java/com/skyflow/Skyflow.java b/flowvault/src/main/java/com/skyflow/Skyflow.java index 96b5f45a..f5b2a948 100644 --- a/flowvault/src/main/java/com/skyflow/Skyflow.java +++ b/flowvault/src/main/java/com/skyflow/Skyflow.java @@ -46,6 +46,15 @@ public VaultController vault() throws SkyflowException { public static final class SkyflowClientBuilder extends BaseSkyflowClientBuilder { private final LinkedHashMap vaultClientsMap = new LinkedHashMap<>(); + // Client-wide HTTP config. Resolution per vault, most specific first: + // VaultConfig value -> the value set here -> SDK default (60s call timeout, 0 retries). + // null here means "not set", so the SDK default applies to vaults that don't override it. + // Only null means inherit: an explicit 0 is a real value and wins over the level below. + private Integer timeout; + private Integer connectTimeout; + private Integer readTimeout; + private Integer writeTimeout; + private Integer maxRetries; @Override protected void validateVaultConfig(VaultConfig vaultConfig) throws SkyflowException { @@ -54,13 +63,26 @@ protected void validateVaultConfig(VaultConfig vaultConfig) throws SkyflowExcept @Override protected void onVaultConfigAdded(VaultConfig vaultConfig) throws SkyflowException { - this.vaultClientsMap.put(vaultConfig.getVaultId(), new VaultController(vaultConfig, this.skyflowCredentials)); + VaultController controller = new VaultController(vaultConfig, this.skyflowCredentials); + controller.setCommonHttpConfig(this.timeout, this.connectTimeout, this.readTimeout, + this.writeTimeout, this.maxRetries); + this.vaultClientsMap.put(vaultConfig.getVaultId(), controller); LogUtil.printInfoLog(Utils.parameterizedString(InfoLogs.VAULT_CONTROLLER_INITIALIZED.getLog(), vaultConfig.getVaultId())); } @Override protected void onVaultConfigUpdated(VaultConfig updatedConfig) throws SkyflowException { - this.vaultClientsMap.put(updatedConfig.getVaultId(), new VaultController(updatedConfig, this.skyflowCredentials)); + // Update the existing controller in place — replacing it would leave any VaultController + // reference the caller already holds pointing at the previous config. + VaultController updated = this.vaultClientsMap.get(updatedConfig.getVaultId()); + if (updated == null) { + updated = new VaultController(updatedConfig, this.skyflowCredentials); + this.vaultClientsMap.put(updatedConfig.getVaultId(), updated); + } else { + updated.setVaultConfig(updatedConfig); + } + updated.setCommonHttpConfig(this.timeout, this.connectTimeout, this.readTimeout, + this.writeTimeout, this.maxRetries); } @Override @@ -89,9 +111,48 @@ public SkyflowClientBuilder addVaultConfig(VaultConfig vaultConfig) throws Skyfl @Override public SkyflowClientBuilder updateVaultConfig(VaultConfig vaultConfig) throws SkyflowException { super.updateVaultConfig(vaultConfig); + carryVaultOverrides(vaultConfig); return this; } + /** + * BaseSkyflow.mergeVaultConfig() only carries env, clusterId and credentials across, so the + * flowvault-specific fields on an incoming update — vaultUrl and the HTTP settings — would + * be dropped silently. Apply them to the merged config the new controller is holding. A null + * on the incoming config means "leave as is", matching how the base class merges every + * other field. + */ + private void carryVaultOverrides(VaultConfig incoming) throws SkyflowException { + VaultConfig merged = this.vaultConfigMap.get(incoming.getVaultId()); + if (merged == null || merged == incoming) { + return; + } + if (incoming.getTimeout() != null) { + merged.setTimeout(incoming.getTimeout()); + } + if (incoming.getConnectTimeout() != null) { + merged.setConnectTimeout(incoming.getConnectTimeout()); + } + if (incoming.getReadTimeout() != null) { + merged.setReadTimeout(incoming.getReadTimeout()); + } + if (incoming.getWriteTimeout() != null) { + merged.setWriteTimeout(incoming.getWriteTimeout()); + } + if (incoming.getMaxRetries() != null) { + merged.setMaxRetries(incoming.getMaxRetries()); + } + // The HTTP settings above are resolved lazily on the next request, but the URL is + // resolved once in the VaultClient constructor — which already ran with the old value. + if (incoming.getVaultUrl() != null) { + merged.setVaultUrl(incoming.getVaultUrl()); + VaultController controller = this.vaultClientsMap.get(incoming.getVaultId()); + if (controller != null) { + controller.refreshVaultUrl(); + } + } + } + @Override public SkyflowClientBuilder removeVaultConfig(String vaultId) throws SkyflowException { super.removeVaultConfig(vaultId); @@ -110,6 +171,75 @@ public SkyflowClientBuilder setLogLevel(LogLevel logLevel) { return this; } + /** + * Overall call timeout in seconds, including retries. Default 60. + *

+ * Precedence: a vault that sets {@link VaultConfig#setTimeout(Integer)} wins; this + * value applies only to vaults that leave it unset. + */ + public SkyflowClientBuilder timeout(int timeout) { + this.timeout = timeout; + propagateHttpConfig(); + return this; + } + + /** + * Per-attempt connection-establishment timeout in seconds. Unset => HTTP client default (10s). + *

+ * Precedence: a vault that sets {@link VaultConfig#setConnectTimeout(Integer)} wins; + * this value applies only to vaults that leave it unset. + */ + public SkyflowClientBuilder connectTimeout(int connectTimeout) { + this.connectTimeout = connectTimeout; + propagateHttpConfig(); + return this; + } + + /** + * Per-attempt response-read timeout in seconds. Unset => HTTP client default (10s). + *

+ * Precedence: a vault that sets {@link VaultConfig#setReadTimeout(Integer)} wins; + * this value applies only to vaults that leave it unset. + */ + public SkyflowClientBuilder readTimeout(int readTimeout) { + this.readTimeout = readTimeout; + propagateHttpConfig(); + return this; + } + + /** + * Per-attempt request-write timeout in seconds. Unset => HTTP client default (10s). + *

+ * Precedence: a vault that sets {@link VaultConfig#setWriteTimeout(Integer)} wins; + * this value applies only to vaults that leave it unset. + */ + public SkyflowClientBuilder writeTimeout(int writeTimeout) { + this.writeTimeout = writeTimeout; + propagateHttpConfig(); + return this; + } + + /** + * Retry attempts after the first failure. Default 0 — retries are opt-in so non-idempotent + * bulk writes are not replayed automatically. + *

+ * Precedence: a vault that sets {@link VaultConfig#setMaxRetries(Integer)} wins; + * this value applies only to vaults that leave it unset. + */ + public SkyflowClientBuilder maxRetries(int maxRetries) { + this.maxRetries = maxRetries; + propagateHttpConfig(); + return this; + } + + /** Push the current client-wide HTTP settings onto every vault controller built so far. */ + private void propagateHttpConfig() { + for (VaultController vault : this.vaultClientsMap.values()) { + vault.setCommonHttpConfig(this.timeout, this.connectTimeout, this.readTimeout, + this.writeTimeout, this.maxRetries); + } + } + public Skyflow build() { return new Skyflow(this); } diff --git a/flowvault/src/main/java/com/skyflow/VaultClient.java b/flowvault/src/main/java/com/skyflow/VaultClient.java index 9033e642..a8a2e2d7 100644 --- a/flowvault/src/main/java/com/skyflow/VaultClient.java +++ b/flowvault/src/main/java/com/skyflow/VaultClient.java @@ -5,19 +5,67 @@ import com.skyflow.errors.SkyflowException; import com.skyflow.generated.rest.ApiClient; import com.skyflow.generated.rest.ApiClientBuilder; +import com.skyflow.generated.rest.core.RetryInterceptor; import com.skyflow.generated.rest.resources.flowservice.FlowserviceClient; import com.skyflow.generated.rest.resources.records.RecordsClient; import com.skyflow.utils.Utils; +import java.util.concurrent.TimeUnit; + +import okhttp3.ConnectionPool; +import okhttp3.OkHttpClient; +import okhttp3.Request; + public class VaultClient extends BaseVaultClient { private final ApiClientBuilder apiClientBuilder; private ApiClient apiClient; + // Client-wide (Skyflow builder) HTTP config; null => fall back to the SDK defaults below. + private Integer commonTimeout; + private Integer commonConnectTimeout; + private Integer commonReadTimeout; + private Integer commonWriteTimeout; + private Integer commonMaxRetries; + // SDK defaults, used when neither the vault-level nor the client-wide value is set. + private static final int DEFAULT_TIMEOUT_SECONDS = 60; + // Retries OFF by default (opt-in) so non-idempotent bulk writes aren't replayed automatically. + private static final int DEFAULT_MAX_RETRIES = 0; protected VaultClient(VaultConfig vaultConfig, Credentials credentials) throws SkyflowException { super(vaultConfig, credentials); this.apiClientBuilder = new ApiClientBuilder(); this.apiClient = null; - updateVaultURL(); + updateVaultUrl(); + } + + /** + * Applies the client-wide HTTP settings from the Skyflow builder. Discards the cached HTTP + * client and ApiClient so the next call rebuilds them with the new values. + */ + protected void setCommonHttpConfig(Integer timeout, Integer connectTimeout, Integer readTimeout, + Integer writeTimeout, Integer maxRetries) { + this.commonTimeout = timeout; + this.commonConnectTimeout = connectTimeout; + this.commonReadTimeout = readTimeout; + this.commonWriteTimeout = writeTimeout; + this.commonMaxRetries = maxRetries; + this.sharedHttpClient = null; + this.apiClient = null; + } + + /** Resolve a setting: vault-level override, else client-wide default, else the SDK default. */ + private static int resolveInt(Integer vaultLevel, Integer clientLevel, int defaultValue) { + if (vaultLevel != null) { + return vaultLevel; + } + return clientLevel != null ? clientLevel : defaultValue; + } + + /** + * Resolve an optional setting: vault-level override, else client-wide default, else null. + * Null means "not configured" — the caller leaves the underlying HTTP client default in place. + */ + private static Integer resolveNullableInt(Integer vaultLevel, Integer clientLevel) { + return vaultLevel != null ? vaultLevel : clientLevel; } protected FlowserviceClient getRecordsApi() { @@ -41,29 +89,79 @@ protected synchronized void setBearerToken() throws SkyflowException { } } - private void updateVaultURL() throws SkyflowException { - // Fetch vaultURL from ENV - String vaultURL = Utils.getEnvVaultURL(); + /** + * Adopts an updated config in place, so a VaultController reference the caller already holds + * keeps working instead of silently serving the previous config. Discards the cached HTTP and + * API clients; the bearer token is re-resolved by setBearerToken, which drops it when the + * effective credentials changed. + */ + protected void setVaultConfig(VaultConfig vaultConfig) throws SkyflowException { + this.vaultConfig = vaultConfig; + this.sharedHttpClient = null; + this.apiClient = null; + updateVaultUrl(); + } + + /** + * Re-resolves the vault URL from the current config. The constructor resolves it once, so a + * vaultUrl supplied later through updateVaultConfig would otherwise never take effect. + */ + protected void refreshVaultUrl() throws SkyflowException { + updateVaultUrl(); + } + + private void updateVaultUrl() throws SkyflowException { + // Fetch vaultUrl from ENV + String vaultUrl = Utils.getEnvVaultUrl(); - // If vaultURL from ENV is null or empty, fetch vaultURL from vault config - if (vaultURL == null || vaultURL.isEmpty()) { - vaultURL = this.vaultConfig.getVaultURL(); + // If vaultUrl from ENV is null or empty, fetch vaultUrl from vault config + if (vaultUrl == null || vaultUrl.isEmpty()) { + vaultUrl = this.vaultConfig.getVaultUrl(); } - // If vaultURL from vault config is also null or empty, construct vaultURL from clusterId passed in vault config - if (vaultURL == null || vaultURL.isEmpty()) { - vaultURL = Utils.getVaultURL(this.vaultConfig.getClusterId(), this.vaultConfig.getEnv()); + // If vaultUrl from vault config is also null or empty, construct vaultUrl from clusterId passed in vault config + if (vaultUrl == null || vaultUrl.isEmpty()) { + vaultUrl = Utils.getVaultUrl(this.vaultConfig.getClusterId(), this.vaultConfig.getEnv()); } - this.apiClientBuilder.url(vaultURL); - if (!vaultURL.equals(this.currentVaultURL)) { - this.currentVaultURL = vaultURL; + this.apiClientBuilder.url(vaultUrl); + if (!vaultUrl.equals(this.currentVaultURL)) { + this.currentVaultURL = vaultUrl; this.apiClient = null; } } protected void updateExecutorInHTTP() { if (sharedHttpClient == null) { - sharedHttpClient = buildSharedHttpClient(() -> this.token); + int timeoutSeconds = resolveInt(vaultConfig.getTimeout(), commonTimeout, DEFAULT_TIMEOUT_SECONDS); + int maxRetries = resolveInt(vaultConfig.getMaxRetries(), commonMaxRetries, DEFAULT_MAX_RETRIES); + // Per-attempt timeouts: null => leave OkHttp's built-in default (backward compatible). + Integer connectTimeout = resolveNullableInt(vaultConfig.getConnectTimeout(), commonConnectTimeout); + Integer readTimeout = resolveNullableInt(vaultConfig.getReadTimeout(), commonReadTimeout); + Integer writeTimeout = resolveNullableInt(vaultConfig.getWriteTimeout(), commonWriteTimeout); + + OkHttpClient.Builder httpBuilder = new OkHttpClient.Builder() + .connectionPool(new ConnectionPool(10, 1, TimeUnit.MINUTES)) + // Overall ceiling; bounds the whole call including retries. + .callTimeout(timeoutSeconds, TimeUnit.SECONDS) + // OUTER: retries. Must wrap the auth interceptor so each attempt re-reads the + // (possibly refreshed) bearer token rather than replaying a stale one. + .addInterceptor(new RetryInterceptor(maxRetries)) + .addInterceptor(chain -> { // INNER: auth + Request requestWithAuth = chain.request().newBuilder() + .header("Authorization", "Bearer " + this.token) + .build(); + return chain.proceed(requestWithAuth); + }); + if (connectTimeout != null) { + httpBuilder.connectTimeout(connectTimeout, TimeUnit.SECONDS); + } + if (readTimeout != null) { + httpBuilder.readTimeout(readTimeout, TimeUnit.SECONDS); + } + if (writeTimeout != null) { + httpBuilder.writeTimeout(writeTimeout, TimeUnit.SECONDS); + } + sharedHttpClient = httpBuilder.build(); apiClientBuilder.httpClient(sharedHttpClient); } } diff --git a/flowvault/src/main/java/com/skyflow/config/VaultConfig.java b/flowvault/src/main/java/com/skyflow/config/VaultConfig.java index 8966d29f..b14ed6b5 100644 --- a/flowvault/src/main/java/com/skyflow/config/VaultConfig.java +++ b/flowvault/src/main/java/com/skyflow/config/VaultConfig.java @@ -1,20 +1,117 @@ package com.skyflow.config; +/** + * Per-vault configuration. + *

+ * The HTTP timeout and retry settings below are vault-level overrides. Each one resolves + * most-specific-first: the value set here, else the client-wide value set on + * {@code Skyflow.builder()}, else the SDK default. So when the same setting is supplied at both + * levels, the value on this VaultConfig takes precedence and the client-wide value is + * ignored for this vault. + *

+ * Only {@code null} means "inherit" — an explicit {@code 0} is a real value and wins over the + * client-wide setting. + */ public class VaultConfig extends BaseVaultConfig { - private String vaultURL; + private String vaultUrl; + // HTTP timeout & retry config (vault-level overrides). null => inherit client-wide default, then SDK default. + private Integer timeout; // overall call timeout, in seconds + private Integer connectTimeout; // per-attempt connection-establishment timeout, in seconds + private Integer readTimeout; // per-attempt response-read timeout, in seconds + private Integer writeTimeout; // per-attempt request-write timeout, in seconds + private Integer maxRetries; // retry attempts after the first failure public VaultConfig() { super(); - this.vaultURL = null; + this.vaultUrl = null; + this.timeout = null; + this.connectTimeout = null; + this.readTimeout = null; + this.writeTimeout = null; + this.maxRetries = null; } - public String getVaultURL() { - return vaultURL; + public String getVaultUrl() { + return vaultUrl; } - public void setVaultURL(String vaultURL) { - this.vaultURL = vaultURL; + public void setVaultUrl(String vaultUrl) { + this.vaultUrl = vaultUrl; + } + + public Integer getTimeout() { + return timeout; + } + + /** + * Overall call timeout in seconds for this vault, including retries. + *

+ * Takes precedence over the client-wide {@code Skyflow.builder().timeout(...)}. Leave unset + * (null) to inherit that value, or the SDK default of 60s if it is also unset. + */ + public void setTimeout(Integer timeout) { + this.timeout = timeout; + } + + public Integer getConnectTimeout() { + return connectTimeout; + } + + /** + * Per-attempt connection-establishment timeout in seconds for this vault. + *

+ * Takes precedence over the client-wide {@code Skyflow.builder().connectTimeout(...)}. Leave + * unset (null) to inherit that value; if neither is set, the underlying HTTP client default + * (10s) applies. Note the overall {@code timeout} still bounds the whole call, including retries. + */ + public void setConnectTimeout(Integer connectTimeout) { + this.connectTimeout = connectTimeout; + } + + public Integer getReadTimeout() { + return readTimeout; + } + + /** + * Per-attempt response-read timeout in seconds for this vault. + *

+ * Takes precedence over the client-wide {@code Skyflow.builder().readTimeout(...)}. Leave + * unset (null) to inherit that value; if neither is set, the underlying HTTP client default + * (10s) applies. Note the overall {@code timeout} still bounds the whole call, including retries. + */ + public void setReadTimeout(Integer readTimeout) { + this.readTimeout = readTimeout; + } + + public Integer getWriteTimeout() { + return writeTimeout; + } + + /** + * Per-attempt request-write timeout in seconds for this vault. + *

+ * Takes precedence over the client-wide {@code Skyflow.builder().writeTimeout(...)}. Leave + * unset (null) to inherit that value; if neither is set, the underlying HTTP client default + * (10s) applies. Note the overall {@code timeout} still bounds the whole call, including retries. + */ + public void setWriteTimeout(Integer writeTimeout) { + this.writeTimeout = writeTimeout; + } + + public Integer getMaxRetries() { + return maxRetries; + } + + /** + * Retry attempts after the first failure for this vault. + *

+ * Takes precedence over the client-wide {@code Skyflow.builder().maxRetries(...)}. Leave unset + * (null) to inherit that value, or the SDK default of 0 if it is also unset — retries are + * opt-in, so non-idempotent bulk writes are not replayed silently. + */ + public void setMaxRetries(Integer maxRetries) { + this.maxRetries = maxRetries; } } diff --git a/flowvault/src/main/java/com/skyflow/enums/UpsertType.java b/flowvault/src/main/java/com/skyflow/enums/UpsertType.java deleted file mode 100644 index fa2350d2..00000000 --- a/flowvault/src/main/java/com/skyflow/enums/UpsertType.java +++ /dev/null @@ -1,18 +0,0 @@ -package com.skyflow.enums; - -public enum UpsertType { - UPDATE("UPDATE"), - - REPLACE("REPLACE"); - - private final String value; - - UpsertType(String value) { - this.value = value; - } - - @Override - public String toString() { - return this.value; - } -} diff --git a/flowvault/src/main/java/com/skyflow/utils/Constants.java b/flowvault/src/main/java/com/skyflow/utils/Constants.java index 983e82b3..acbd471f 100644 --- a/flowvault/src/main/java/com/skyflow/utils/Constants.java +++ b/flowvault/src/main/java/com/skyflow/utils/Constants.java @@ -10,6 +10,7 @@ public final class Constants extends BaseConstants { public static final String VAULT_DOMAIN = ".skyvault."; public static final String SDK_PREFIX; public static final String SDK_METRIC_NAME_VERSION_PREFIX = "skyflow-flowvault-java@"; + public static final Integer MAX_BULK_DATA_SIZE = 10000; public static final Integer INSERT_BATCH_SIZE = 50; public static final Integer MAX_INSERT_BATCH_SIZE = 1000; public static final Integer INSERT_CONCURRENCY_LIMIT = 1; diff --git a/flowvault/src/main/java/com/skyflow/utils/Utils.java b/flowvault/src/main/java/com/skyflow/utils/Utils.java index e26286c0..a88b3b15 100644 --- a/flowvault/src/main/java/com/skyflow/utils/Utils.java +++ b/flowvault/src/main/java/com/skyflow/utils/Utils.java @@ -1,41 +1,64 @@ package com.skyflow.utils; +import java.net.MalformedURLException; +import java.net.URL; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + import com.google.gson.JsonObject; -import com.skyflow.config.Credentials; import com.skyflow.config.VaultConfig; import com.skyflow.enums.Env; -import com.skyflow.enums.UpsertType; import com.skyflow.errors.ErrorCode; import com.skyflow.errors.ErrorMessage; import com.skyflow.errors.SkyflowException; import com.skyflow.generated.rest.core.ApiClientApiException; import com.skyflow.generated.rest.resources.flowservice.requests.V1FlowDetokenizeRequest; -import com.skyflow.generated.rest.resources.flowservice.requests.V1GetRequest; import com.skyflow.generated.rest.resources.flowservice.requests.V1InsertRequest; -import com.skyflow.generated.rest.resources.records.requests.V1ExecuteQueryRequest; -import com.skyflow.generated.rest.types.*; +import com.skyflow.generated.rest.types.FlowEnumUpdateType; +import com.skyflow.generated.rest.types.FlowTokenizeResponseObjectToken; +import com.skyflow.generated.rest.types.V1DeleteTokenResponseObject; +import com.skyflow.generated.rest.types.V1FlowDeleteTokenResponse; +import com.skyflow.generated.rest.types.V1FlowDetokenizeResponse; +import com.skyflow.generated.rest.types.V1FlowDetokenizeResponseObject; +import com.skyflow.generated.rest.types.V1FlowTokenizeRequestObject; +import com.skyflow.generated.rest.types.V1FlowTokenizeResponse; +import com.skyflow.generated.rest.types.V1FlowTokenizeResponseObject; +import com.skyflow.generated.rest.types.V1InsertRecordData; +import com.skyflow.generated.rest.types.V1InsertResponse; +import com.skyflow.generated.rest.types.V1RecordResponseObject; +import com.skyflow.generated.rest.types.V1TokenGroupRedactions; +import com.skyflow.generated.rest.types.V1Upsert; import com.skyflow.logs.ErrorLogs; -import com.skyflow.logs.InfoLogs; -import com.skyflow.logs.WarningLogs; -import com.skyflow.serviceaccount.util.BearerToken; -import com.skyflow.serviceaccount.util.Token; import com.skyflow.utils.logger.LogUtil; -import com.skyflow.vault.data.*; +import com.skyflow.vault.data.BulkDeleteTokensRequest; +import com.skyflow.vault.data.BulkDeleteTokensResponse; +import com.skyflow.vault.data.BulkDetokenizeRequest; +import com.skyflow.vault.data.BulkDetokenizeResponse; +import com.skyflow.vault.data.BulkDetokenizeResponseRecord; +import com.skyflow.vault.data.BulkInsertRequest; +import com.skyflow.vault.data.BulkInsertResponse; +import com.skyflow.vault.data.BulkInsertResponseRecord; +import com.skyflow.vault.data.BulkTokenizeRecord; +import com.skyflow.vault.data.BulkTokenizeRequest; +import com.skyflow.vault.data.BulkTokenizeResponse; +import com.skyflow.vault.data.DeleteTokensSuccess; +import com.skyflow.vault.data.ErrorRecord; +import com.skyflow.vault.data.InsertRequest; +import com.skyflow.vault.data.InsertRequestRecord; +import com.skyflow.vault.data.TokenGroupRedactions; +import com.skyflow.vault.data.TokenizeSuccess; +import com.skyflow.vault.data.UpsertOptions; + import io.github.cdimascio.dotenv.Dotenv; import io.github.cdimascio.dotenv.DotenvException; -import java.io.File; -import java.net.MalformedURLException; -import java.net.URL; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - public final class Utils extends BaseUtils { - public static String getVaultURL(String clusterId, Env env) { - return getVaultURL(clusterId, env, Constants.VAULT_DOMAIN); + public static String getVaultUrl(String clusterId, Env env) { + // The 3-arg overload is inherited from common's BaseUtils, which keeps the older + // getVaultURL spelling (shared with v2), so it is qualified rather than renamed here. + return BaseUtils.getVaultURL(clusterId, env, Constants.VAULT_DOMAIN); } public static JsonObject getMetrics() { @@ -46,27 +69,27 @@ public static JsonObject getMetrics() { } - public static String getEnvVaultURL() throws SkyflowException { + public static String getEnvVaultUrl() throws SkyflowException { try { - String vaultURL = System.getenv("VAULT_URL"); - if (vaultURL == null) { + String vaultUrl = System.getenv("VAULT_URL"); + if (vaultUrl == null) { Dotenv dotenv = Dotenv.load(); - vaultURL = dotenv.get("VAULT_URL"); + vaultUrl = dotenv.get("VAULT_URL"); } - if (vaultURL != null && vaultURL.trim().isEmpty()) { + if (vaultUrl != null && vaultUrl.trim().isEmpty()) { LogUtil.printErrorLog(ErrorLogs.EMPTY_VAULT_URL.getLog()); throw new SkyflowException(ErrorCode.INVALID_INPUT.getCode(), ErrorMessage.EmptyVaultUrl.getMessage()); - } else if (vaultURL != null && !isValidURL(vaultURL)) { + } else if (vaultUrl != null && !isValidUrl(vaultUrl)) { LogUtil.printErrorLog(ErrorLogs.INVALID_VAULT_URL_FORMAT.getLog()); throw new SkyflowException(ErrorCode.INVALID_INPUT.getCode(), ErrorMessage.InvalidVaultUrlFormat.getMessage()); } - return vaultURL; + return vaultUrl; } catch (DotenvException e) { return null; } } - public static boolean isValidURL(String url) { + public static boolean isValidUrl(String url) { URL parsedUrl; try { parsedUrl = new URL(url); @@ -82,57 +105,40 @@ public static boolean isValidURL(String url) { } - public static String generateBearerToken(Credentials credentials) throws SkyflowException { - if (credentials.getPath() != null) { - BearerToken.BearerTokenBuilder builder = BearerToken.builder() - .setCredentials(new File(credentials.getPath())) - .setRoles(credentials.getRoles()); - Object ctx = credentials.getContext(); - if (ctx instanceof String) { - builder.setCtx((String) ctx); - } else if (ctx instanceof Map) { - builder.setCtx((Map) ctx); - } - return builder.build().getBearerToken(); - } else if (credentials.getCredentialsString() != null) { - BearerToken.BearerTokenBuilder builder = BearerToken.builder() - .setCredentials(credentials.getCredentialsString()) - .setRoles(credentials.getRoles()); - Object ctx = credentials.getContext(); - if (ctx instanceof String) { - builder.setCtx((String) ctx); - } else if (ctx instanceof Map) { - builder.setCtx((Map) ctx); + // Mirrors the "present" test used by the request validators: null and blank both count as absent. + private static boolean hasText(String value) { + return value != null && !value.trim().isEmpty(); + } + + private static V1Upsert toV1Upsert(UpsertOptions upsert) { + V1Upsert.Builder builder = V1Upsert.builder().uniqueColumns(upsert.getUniqueColumns()); + // updateType is a String on the request; the legal values come from the wire enum itself + // so there is a single source of truth. Validations rejects anything that does not match. + String updateType = upsert.getUpdateType(); + for (FlowEnumUpdateType type : FlowEnumUpdateType.values()) { + if (type.toString().equalsIgnoreCase(updateType)) { + builder.updateType(type); + break; } - return builder.build().getBearerToken(); - } else { - return credentials.getToken(); } + return builder.build(); } - public static V1InsertRequest getBulkInsertRequestBody(InsertRequest request, VaultConfig config) { - ArrayList records = request.getRecords(); + public static V1InsertRequest getInsertRequestBody(InsertRequest request, VaultConfig config) { + List records = request.getRecords(); List insertRecordDataList = new ArrayList<>(); - for (InsertRecord record : records) { - V1InsertRecordData.Builder data = V1InsertRecordData.builder(); - data.data(record.getData()); - if (record.getTable() != null && !record.getTable().isEmpty()) { - data.tableName(record.getTable()); - } - if (record.getUpsert() != null && !record.getUpsert().isEmpty()) { - if (record.getUpsertType() != null) { - FlowEnumUpdateType updateType = null; - if (record.getUpsertType() == UpsertType.REPLACE) { - updateType = FlowEnumUpdateType.REPLACE; - } else if (record.getUpsertType() == UpsertType.UPDATE) { - updateType = FlowEnumUpdateType.UPDATE; - } - V1Upsert upsert = V1Upsert.builder().uniqueColumns(record.getUpsert()).updateType(updateType).build(); - data.upsert(upsert); - } else { - V1Upsert upsert = V1Upsert.builder().uniqueColumns(record.getUpsert()).build(); - data.upsert(upsert); - } + for (InsertRequestRecord record : records) { + V1InsertRecordData.Builder data = V1InsertRecordData.builder() + .data(record.getData()) + // A blank record-level table name counts as absent, matching + // validateInsertRequest, so it falls back to the request-level one. + .tableName(hasText(record.getTableName()) ? record.getTableName() : request.getTableName()); + if (record.getTokens() != null && !record.getTokens().isEmpty()) { + data.tokens(record.getTokens()); + } + UpsertOptions upsert = record.getUpsert() != null ? record.getUpsert() : request.getUpsert(); + if (upsert != null && upsert.getUniqueColumns() != null && !upsert.getUniqueColumns().isEmpty()) { + data.upsert(toV1Upsert(upsert)); } insertRecordDataList.add(data.build()); } @@ -140,249 +146,23 @@ public static V1InsertRequest getBulkInsertRequestBody(InsertRequest request, Va V1InsertRequest.Builder builder = V1InsertRequest.builder() .vaultId(config.getVaultId()) .records(insertRecordDataList); - - if (request.getTable() != null && !request.getTable().isEmpty()) { - builder.tableName(request.getTable()); - } - - if (request.getUpsert() != null && !request.getUpsert().isEmpty()) { - if (request.getUpsertType() != null) { - FlowEnumUpdateType updateType = null; - if (request.getUpsertType() == UpsertType.REPLACE) { - updateType = FlowEnumUpdateType.REPLACE; - } else if (request.getUpsertType() == UpsertType.UPDATE) { - updateType = FlowEnumUpdateType.UPDATE; - } - V1Upsert upsert = V1Upsert.builder().uniqueColumns(request.getUpsert()).updateType(updateType).build(); - builder.upsert(upsert); - } else { - V1Upsert upsert = V1Upsert.builder().uniqueColumns(request.getUpsert()).build(); - builder.upsert(upsert); - } + if (hasText(request.getTableName())) { + builder.tableName(request.getTableName()); } return builder.build(); - } - public static InsertResponse buildInsertResponse(V1InsertResponse res) { - ArrayList> insertedFields = new ArrayList<>(); - ArrayList> errors = new ArrayList<>(); - - if (res != null && res.getRecords().isPresent()) { - for (V1RecordResponseObject record : res.getRecords().get()) { - if (record.getError().isPresent()) { - HashMap errorRecord = new HashMap<>(); - record.getSkyflowId().ifPresent(skyflowId -> errorRecord.put("skyflowId", skyflowId)); - record.getTableName().ifPresent(tableName -> errorRecord.put("tableName", tableName)); - errorRecord.put("error", record.getError().get()); - record.getHttpCode().ifPresent(httpCode -> errorRecord.put("httpCode", httpCode)); - errors.add(errorRecord); - } else { - HashMap insertedRecord = new HashMap<>(); - record.getSkyflowId().ifPresent(skyflowId -> insertedRecord.put("skyflowId", skyflowId)); - record.getTokens().ifPresent(insertedRecord::putAll); - insertedFields.add(insertedRecord); - } - } - } - return new InsertResponse(insertedFields, errors); - } - - public static com.skyflow.generated.rest.resources.flowservice.requests.V1FlowDetokenizeRequest getDetokenizeRequestBody(DetokenizeRequest request, String vaultid) { - List detokenizeData = request.getDetokenizeData(); - List tokens = new ArrayList<>(); - for(int i = 0; i< detokenizeData.size(); i++){ - tokens.add(detokenizeData.get(i).getToken()); - } - com.skyflow.generated.rest.resources.flowservice.requests.V1FlowDetokenizeRequest.Builder builder = - com.skyflow.generated.rest.resources.flowservice.requests.V1FlowDetokenizeRequest.builder() - .vaultId(vaultid) - .tokens(tokens); - if (request.getTokenGroupRedactions() != null) { - List tokenGroupRedactionsList = new ArrayList<>(); - for (com.skyflow.vault.data.TokenGroupRedactions tokenGroupRedactions : request.getTokenGroupRedactions()) { - com.skyflow.generated.rest.types.V1TokenGroupRedactions redactions = - com.skyflow.generated.rest.types.V1TokenGroupRedactions.builder() - .tokenGroupName(tokenGroupRedactions.getTokenGroupName()) - .redaction(tokenGroupRedactions.getRedaction()) - .build(); - tokenGroupRedactionsList.add(redactions); - } - - builder.tokenGroupRedactions(tokenGroupRedactionsList); - } - return builder.build(); - } - - public static DetokenizeResponse buildDetokenizeResponse(V1FlowDetokenizeResponse res) { - ArrayList detokenizedFields = new ArrayList<>(); - ArrayList errors = new ArrayList<>(); - - if (res != null && res.getResponse().isPresent()) { - for (V1FlowDetokenizeResponseObject record : res.getResponse().get()) { - String token = record.getToken().orElse(null); - String tokenGroupName = record.getTokenGroupName().orElse(null); - Map metadata = record.getMetadata().orElse(null); - if (record.getError().isPresent()) { - errors.add(new DetokenizeRecordResponse(token, null, record.getError().get(), tokenGroupName, metadata)); - } else { - Object value = record.getValue().orElse(null); - detokenizedFields.add(new DetokenizeRecordResponse(token, value, null, tokenGroupName, metadata)); - } - } - } - return new DetokenizeResponse(detokenizedFields, errors); - } - - public static com.skyflow.generated.rest.resources.flowservice.requests.V1FlowDeleteTokenRequest getDeleteTokensRequestBody(DeleteTokensRequest request, String vaultid) { - return com.skyflow.generated.rest.resources.flowservice.requests.V1FlowDeleteTokenRequest.builder() - .vaultId(vaultid) - .tokens(request.getTokens()) - .build(); - } private static String extractRequestId(Map> headers) { if (headers == null) return null; List ids = headers.get(BaseConstants.REQUEST_ID_HEADER_KEY); return (ids == null || ids.isEmpty()) ? null : ids.get(0); } - public static DeleteTokensResponse buildDeleteTokensResponse(V1FlowDeleteTokenResponse res, Map> headers, int requestedTokenCount) { - ArrayList deletedTokens = new ArrayList<>(); - ArrayList> errors = new ArrayList<>(); - String requestId = extractRequestId(headers); - if (res != null && res.getTokens().isPresent()) { - for (V1DeleteTokenResponseObject record : res.getTokens().get()) { - if (record.getError().isPresent()) { - HashMap errorRecord = new HashMap<>(); - errorRecord.put("error", record.getError().get()); - record.getHttpCode().ifPresent(httpCode -> errorRecord.put("httpCode", httpCode)); - errorRecord.put("requestId", requestId); - errors.add(errorRecord); - } else { - record.getValue().ifPresent(deletedTokens::add); - } - } - if (deletedTokens.size() + errors.size() != requestedTokenCount) { - LogUtil.printWarningLog(WarningLogs.INCOMPLETE_DELETE_TOKENS_RESPONSE.getLog()); - } - } else { - LogUtil.printWarningLog(WarningLogs.EMPTY_DELETE_TOKENS_RESPONSE.getLog()); - } - return new DeleteTokensResponse(deletedTokens, errors); - } - - public static com.skyflow.generated.rest.resources.flowservice.requests.V1FlowTokenizeRequest getTokenizeRequestBody(TokenizeRequest request, String vaultid) { - List dataList = new ArrayList<>(); - for (TokenizeRecord record : request.getData()) { - V1FlowTokenizeRequestObject obj = V1FlowTokenizeRequestObject.builder() - .value(record.getValue()) - .tokenGroupNames(record.getTokenGroupNames()) - .build(); - dataList.add(obj); - } - return com.skyflow.generated.rest.resources.flowservice.requests.V1FlowTokenizeRequest.builder() - .vaultId(vaultid) - .data(dataList) - .build(); - } - - public static TokenizeResponse buildTokenizeResponse(V1FlowTokenizeResponse res, Map> headers, int requestedRecordCount) { - List tokenizedData = new ArrayList<>(); - ArrayList> errors = new ArrayList<>(); - String requestId = extractRequestId(headers); - if (res != null && res.getResponse().isPresent()) { - List records = res.getResponse().get(); - int indexNumber = 0; - for (V1FlowTokenizeResponseObject record : records) { - Object value = record.getValue().orElse(null); - TokenizeData tokenizeData = new TokenizeData(value, indexNumber); - boolean hasAnySuccess = false; - if (record.getTokens().isPresent()) { - for (FlowTokenizeResponseObjectToken tokenObj : record.getTokens().get()) { - if (tokenObj.getError().isPresent()) { - HashMap errorRecord = new HashMap<>(); - errorRecord.put("error", tokenObj.getError().get()); - tokenObj.getHttpCode().ifPresent(httpCode -> errorRecord.put("httpCode", httpCode)); - tokenObj.getTokenGroupName().ifPresent(name -> errorRecord.put("tokenGroupName", name)); - errorRecord.put("index", indexNumber); - errorRecord.put("requestId", requestId); - errors.add(errorRecord); - } else if (tokenObj.getTokenGroupName().isPresent() && tokenObj.getToken().isPresent()) { - tokenizeData.addToken(tokenObj.getTokenGroupName().get(), tokenObj.getToken().get()); - hasAnySuccess = true; - } - } - } - if (hasAnySuccess) { - tokenizedData.add(tokenizeData); - } - indexNumber++; - } - if (indexNumber != requestedRecordCount) { - LogUtil.printWarningLog(WarningLogs.INCOMPLETE_TOKENIZE_RESPONSE.getLog()); - } - } else { - LogUtil.printWarningLog(WarningLogs.EMPTY_TOKENIZE_RESPONSE.getLog()); - } - TokenizeResponse tokenizeResponse = new TokenizeResponse(errors); - tokenizeResponse.setTokenizedData(tokenizedData); - return tokenizeResponse; - } - // ── Bulk (batched/concurrent) request-body builders ────────────────────── + // BulkInsertRequest is an InsertRequest, so the bulk body is built exactly the same way. public static com.skyflow.generated.rest.resources.flowservice.requests.V1InsertRequest getBulkInsertRequestBody(BulkInsertRequest request, VaultConfig config) { - ArrayList records = request.getRecords(); - List insertRecordDataList = new ArrayList<>(); - for (BulkInsertRecord record : records) { - V1InsertRecordData.Builder data = V1InsertRecordData.builder(); - data.data(record.getData()); - if (record.getTable() != null && !record.getTable().isEmpty()) { - data.tableName(record.getTable()); - } - if (record.getUpsert() != null && !record.getUpsert().isEmpty()) { - if (record.getUpsertType() != null) { - FlowEnumUpdateType updateType = null; - if (record.getUpsertType() == UpsertType.REPLACE) { - updateType = FlowEnumUpdateType.REPLACE; - } else if (record.getUpsertType() == UpsertType.UPDATE) { - updateType = FlowEnumUpdateType.UPDATE; - } - V1Upsert upsert = V1Upsert.builder().uniqueColumns(record.getUpsert()).updateType(updateType).build(); - data.upsert(upsert); - } else { - V1Upsert upsert = V1Upsert.builder().uniqueColumns(record.getUpsert()).build(); - data.upsert(upsert); - } - } - insertRecordDataList.add(data.build()); - } - - com.skyflow.generated.rest.resources.flowservice.requests.V1InsertRequest.Builder builder = - com.skyflow.generated.rest.resources.flowservice.requests.V1InsertRequest.builder() - .vaultId(config.getVaultId()) - .records(insertRecordDataList); - - if (request.getTable() != null && !request.getTable().isEmpty()) { - builder.tableName(request.getTable()); - } - - if (request.getUpsert() != null && !request.getUpsert().isEmpty()) { - if (request.getUpsertType() != null) { - FlowEnumUpdateType updateType = null; - if (request.getUpsertType() == UpsertType.REPLACE) { - updateType = FlowEnumUpdateType.REPLACE; - } else if (request.getUpsertType() == UpsertType.UPDATE) { - updateType = FlowEnumUpdateType.UPDATE; - } - V1Upsert upsert = V1Upsert.builder().uniqueColumns(request.getUpsert()).updateType(updateType).build(); - builder.upsert(upsert); - } else { - V1Upsert upsert = V1Upsert.builder().uniqueColumns(request.getUpsert()).build(); - builder.upsert(upsert); - } - } - return builder.build(); + return getInsertRequestBody(request, config); } public static V1FlowDetokenizeRequest getBulkDetokenizeRequestBody(BulkDetokenizeRequest request, String vaultId) { @@ -391,7 +171,7 @@ public static V1FlowDetokenizeRequest getBulkDetokenizeRequestBody(BulkDetokeniz .tokens(request.getTokens()); if (request.getTokenGroupRedactions() != null && !request.getTokenGroupRedactions().isEmpty()) { List tokenGroupRedactionsList = new ArrayList<>(); - for (BulkTokenGroupRedactions tokenGroupRedactions : request.getTokenGroupRedactions()) { + for (TokenGroupRedactions tokenGroupRedactions : request.getTokenGroupRedactions()) { tokenGroupRedactionsList.add(V1TokenGroupRedactions.builder() .tokenGroupName(tokenGroupRedactions.getTokenGroupName()) .redaction(tokenGroupRedactions.getRedaction()) @@ -486,6 +266,59 @@ public static List recordMap, int indexNumber, String requestId) { + BulkInsertResponseRecord err = null; + if (recordMap != null) { + int code = 500; + if (recordMap.containsKey("http_code")) { + code = (Integer) recordMap.get("http_code"); + } else if (recordMap.containsKey("httpCode")) { + code = (Integer) recordMap.get("httpCode"); + } else if (recordMap.containsKey("statusCode")) { + code = (Integer) recordMap.get("statusCode"); + } + // check if skyflowID is present + String skyflowID = null; + if (recordMap.containsKey("skyflowID")) { + skyflowID = recordMap.get("skyflowID").toString(); + } + String tableName = null; + if (recordMap.containsKey("tableName")) { + tableName = recordMap.get("tableName").toString(); + } + String message = recordMap.containsKey("error") ? (String) recordMap.get("error") : + recordMap.containsKey("message") ? (String) recordMap.get("message") : "Unknown error"; + err = new BulkInsertResponseRecord(indexNumber, tableName, skyflowID, null, null, code, message, requestId); + } + return err; + } + + public static BulkDetokenizeResponseRecord createDetokenizeErrorRecord(Map recordMap, int indexNumber, String requestId) { + BulkDetokenizeResponseRecord err = null; + if (recordMap != null) { + int code = 500; + if (recordMap.containsKey("http_code")) { + code = (Integer) recordMap.get("http_code"); + } else if (recordMap.containsKey("httpCode")) { + code = (Integer) recordMap.get("httpCode"); + } else if (recordMap.containsKey("statusCode")) { + code = (Integer) recordMap.get("statusCode"); + } + // the failing token is echoed back so the caller can tell which one it was + String token = null; + if (recordMap.containsKey("token")) { + token = recordMap.get("token").toString(); + } + String tokenGroupName = null; + if (recordMap.containsKey("tokenGroupName")) { + tokenGroupName = recordMap.get("tokenGroupName").toString(); + } + String message = recordMap.containsKey("error") ? (String) recordMap.get("error") : + recordMap.containsKey("message") ? (String) recordMap.get("message") : "Unknown error"; + err = new BulkDetokenizeResponseRecord(indexNumber, token, null, tokenGroupName, null, code, message, requestId); + } + return err; + } public static ErrorRecord createErrorRecord(Map recordMap, int indexNumber, String requestId) { ErrorRecord err = null; @@ -505,10 +338,12 @@ public static ErrorRecord createErrorRecord(Map recordMap, int i return err; } - public static List handleBulkInsertBatchException( + // Errors are parsed into ErrorRecord (shared with the other bulk ops), then projected onto + // the unified BulkInsertResponseRecord shape that bulk insert now returns. + public static List handleBulkInsertBatchException( Throwable ex, List batch, int batchNumber, int batchSize ) { - List errorRecords = new ArrayList<>(); + List allRecords = new ArrayList<>(); Throwable cause = ex.getCause(); if (cause instanceof ApiClientApiException) { ApiClientApiException apiException = (ApiClientApiException) cause; @@ -524,8 +359,8 @@ public static List handleBulkInsertBatchException( for (Object record : recordsList) { if (record instanceof Map) { Map recordMap = (Map) record; - ErrorRecord err = createErrorRecord(recordMap, indexNumber, requestId); - errorRecords.add(err); + BulkInsertResponseRecord err = createInsertErrorRecord(recordMap, indexNumber, requestId); + allRecords.add(err); indexNumber++; } } @@ -535,35 +370,48 @@ public static List handleBulkInsertBatchException( Map recordMap = (errField instanceof Map) ? (Map) errField : null; String fallbackMsg = (errField instanceof String) ? (String) errField : null; for (int j = 0; j < batch.size(); j++) { - ErrorRecord err = (recordMap != null) - ? createErrorRecord(recordMap, indexNumber, requestId) - : new ErrorRecord(indexNumber, fallbackMsg != null ? fallbackMsg : apiException.getMessage(), apiException.statusCode(), requestId); - errorRecords.add(err); + BulkInsertResponseRecord err = null; + if(recordMap != null){ + err = createInsertErrorRecord(recordMap, indexNumber, requestId); + } else { + String errorMessage = null; + if (fallbackMsg != null){ + errorMessage = fallbackMsg; + } else { + errorMessage = apiException.getMessage(); + } + err = new BulkInsertResponseRecord(indexNumber, null, null, null, null, apiException.statusCode(), errorMessage, requestId); + + } + allRecords.add(err); indexNumber++; } } } - if (errorRecords.isEmpty()) { + + if (allRecords.isEmpty()) { for (int j = 0; j < batch.size(); j++) { - errorRecords.add(new ErrorRecord(indexNumber, apiException.getMessage(), apiException.statusCode(), requestId)); + allRecords.add(new BulkInsertResponseRecord(indexNumber, null, null, null, null, apiException.statusCode(), apiException.getMessage(), requestId)); indexNumber++; } } } else { int indexNumber = batchNumber > 0 ? batchNumber * batchSize : 0; for (int j = 0; j < batch.size(); j++) { - ErrorRecord err = new ErrorRecord(indexNumber, ex.getMessage(), 500); - errorRecords.add(err); + BulkInsertResponseRecord err = new BulkInsertResponseRecord(indexNumber, null, null, null, null, 500, ex.getMessage(), null); + allRecords.add(err); indexNumber++; } } - return errorRecords; + return allRecords; } - public static List handleBulkDetokenizeBatchException( + // Errors are parsed into ErrorRecord (shared with the other bulk ops), then projected onto + // the unified BulkDetokenizeResponseRecord shape that bulk detokenize now returns. + public static List handleBulkDetokenizeBatchException( Throwable ex, V1FlowDetokenizeRequest batch, int batchNumber, int batchSize ) { - List errorRecords = new ArrayList<>(); + List allRecords = new ArrayList<>(); Throwable cause = ex.getCause(); if (cause instanceof ApiClientApiException) { ApiClientApiException apiException = (ApiClientApiException) cause; @@ -571,6 +419,7 @@ public static List handleBulkDetokenizeBatchException( Object rawBody = apiException.body(); Map responseBody = (rawBody instanceof Map) ? (Map) rawBody : null; int indexNumber = batchNumber * batchSize; + int tokenCount = batch.getTokens().isPresent() ? batch.getTokens().get().size() : 0; if (responseBody != null) { if (responseBody.containsKey("response")) { Object recordss = responseBody.get("response"); @@ -579,8 +428,8 @@ public static List handleBulkDetokenizeBatchException( for (Object record : recordsList) { if (record instanceof Map) { Map recordMap = (Map) record; - ErrorRecord err = createErrorRecord(recordMap, indexNumber, requestId); - errorRecords.add(err); + BulkDetokenizeResponseRecord err = createDetokenizeErrorRecord(recordMap, indexNumber, requestId); + allRecords.add(err); indexNumber++; } } @@ -589,32 +438,40 @@ public static List handleBulkDetokenizeBatchException( Object errField = responseBody.get("error"); Map recordMap = (errField instanceof Map) ? (Map) errField : null; String fallbackMsg = (errField instanceof String) ? (String) errField : null; - int tokenCount = batch.getTokens().isPresent() ? batch.getTokens().get().size() : 0; for (int j = 0; j < tokenCount; j++) { - ErrorRecord err = (recordMap != null) - ? createErrorRecord(recordMap, indexNumber, requestId) - : new ErrorRecord(indexNumber, fallbackMsg != null ? fallbackMsg : apiException.getMessage(), apiException.statusCode(), requestId); - errorRecords.add(err); + BulkDetokenizeResponseRecord err = null; + if (recordMap != null) { + err = createDetokenizeErrorRecord(recordMap, indexNumber, requestId); + } else { + String errorMessage = null; + if (fallbackMsg != null) { + errorMessage = fallbackMsg; + } else { + errorMessage = apiException.getMessage(); + } + err = new BulkDetokenizeResponseRecord(indexNumber, null, null, null, null, apiException.statusCode(), errorMessage, requestId); + } + allRecords.add(err); indexNumber++; } } } - if (errorRecords.isEmpty()) { - int tokenCount = batch.getTokens().isPresent() ? batch.getTokens().get().size() : 0; + + if (allRecords.isEmpty()) { for (int j = 0; j < tokenCount; j++) { - errorRecords.add(new ErrorRecord(indexNumber, apiException.getMessage(), apiException.statusCode(), requestId)); + allRecords.add(new BulkDetokenizeResponseRecord(indexNumber, null, null, null, null, apiException.statusCode(), apiException.getMessage(), requestId)); indexNumber++; } } } else { int indexNumber = batchNumber * batchSize; for (int j = 0; j < batch.getTokens().get().size(); j++) { - ErrorRecord err = new ErrorRecord(indexNumber, ex.getMessage(), 500); - errorRecords.add(err); + BulkDetokenizeResponseRecord err = new BulkDetokenizeResponseRecord(indexNumber, null, null, null, null, 500, ex.getMessage(), null); + allRecords.add(err); indexNumber++; } } - return errorRecords; + return allRecords; } public static List handleBulkDeleteTokensBatchException( @@ -738,68 +595,49 @@ public static List handleBulkTokenizeBatchException( public static BulkInsertResponse formatBulkInsertResponse(V1InsertResponse response, int batch, int batchSize, Map> headers) { BulkInsertResponse formattedResponse = null; - List successRecords = new ArrayList<>(); - List errorRecords = new ArrayList<>(); + List records = new ArrayList<>(); if (response != null && response.getRecords().isPresent()) { - String requestId = extractRequestId(headers); List record = response.getRecords().get(); int indexNumber = batch * batchSize; int recordsSize = record.size(); for (int index = 0; index < recordsSize; index++) { - if (record.get(index).getError().isPresent()) { - ErrorRecord errorRecord = new ErrorRecord(indexNumber, record.get(index).getError().get(), record.get(index).getHttpCode().orElse(500), requestId); - errorRecords.add(errorRecord); - } else { - Map> tokensMap = null; - if (record.get(index).getTokens().isPresent()) { - tokensMap = new HashMap<>(); - Map tok = record.get(index).getTokens().get(); - for (Map.Entry entry : tok.entrySet()) { - String key = entry.getKey(); - Object value = entry.getValue(); - List tokenList = new ArrayList<>(); - if (value instanceof List) { - List valueList = (List) value; - for (Object item : valueList) { - if (item instanceof Map) { - Map tokenMap = (Map) item; - com.skyflow.vault.data.Token token = new com.skyflow.vault.data.Token((String) tokenMap.get("token"), (String) tokenMap.get("tokenGroupName")); - tokenList.add(token); - } - } - } - tokensMap.put(key, tokenList); - } - } - Success success = new Success(indexNumber, record.get(index).getSkyflowId().orElse(null), tokensMap, record.get(index).getData().isPresent() ? record.get(index).getData().get() : null, record.get(index).getTableName().isPresent() ? record.get(index).getTableName().get() : null); - successRecords.add(success); - } + V1RecordResponseObject current = record.get(index); + records.add(new BulkInsertResponseRecord( + indexNumber, + current.getTableName().orElse(null), + current.getSkyflowId().orElse(null), + current.getTokens().orElse(null), + current.getHashedData().orElse(null), + current.getHttpCode().orElse(current.getError().isPresent() ? 500 : 200), + current.getError().orElse(null), + null)); indexNumber++; } - formattedResponse = new BulkInsertResponse(successRecords, errorRecords); + formattedResponse = new BulkInsertResponse(records); } return formattedResponse; } public static BulkDetokenizeResponse formatBulkDetokenizeResponse(V1FlowDetokenizeResponse response, int batch, int batchSize, Map> headers) { if (response != null && response.getResponse().isPresent()) { - String requestId = extractRequestId(headers); List record = response.getResponse().get(); - List errorRecords = new ArrayList<>(); - List successRecords = new ArrayList<>(); + List records = new ArrayList<>(); int indexNumber = batch * batchSize; int recordsSize = record.size(); for (int index = 0; index < recordsSize; index++) { - if (record.get(index).getError().isPresent()) { - ErrorRecord errorRecord = new ErrorRecord(indexNumber, record.get(index).getError().get(), record.get(index).getHttpCode().orElse(500), requestId); - errorRecords.add(errorRecord); - } else { - DetokenizeResponseObject success = new DetokenizeResponseObject(indexNumber, record.get(index).getToken().orElse(null), record.get(index).getValue().orElse(null), record.get(index).getTokenGroupName().orElse(null), record.get(index).getError().orElse(null), record.get(index).getMetadata().orElse(null)); - successRecords.add(success); - } + V1FlowDetokenizeResponseObject current = record.get(index); + records.add(new BulkDetokenizeResponseRecord( + indexNumber, + current.getToken().orElse(null), + current.getValue().orElse(null), + current.getTokenGroupName().orElse(null), + current.getMetadata().orElse(null), + current.getHttpCode().orElse(current.getError().isPresent() ? 500 : 200), + current.getError().orElse(null), + null)); indexNumber++; } - return new BulkDetokenizeResponse(successRecords, errorRecords); + return new BulkDetokenizeResponse(records); } return null; } @@ -867,120 +705,4 @@ public static BulkTokenizeResponse formatBulkTokenizeResponse( return null; } - public static V1ExecuteQueryRequest getQueryRequestBody(QueryRequest request, String vaultId) { - return V1ExecuteQueryRequest.builder() - .vaultId(vaultId) - .query(request.getQuery()) - .build(); - } - - public static QueryResponse buildQueryResponse(V1ExecuteQueryResponse res) { - ArrayList> fields = new ArrayList<>(); - if (res != null && res.getRecords().isPresent()) { - for (V1ExecuteQueryRecordResponse record : res.getRecords().get()) { - HashMap fieldMap = new HashMap<>(); - if (record.getData().isPresent()) { - fieldMap.putAll(record.getData().get()); - } - fields.add(fieldMap); - } - } - return new QueryResponse(fields); - } - - private static List buildColumnRedactions(List columnRedactions) { - List columnRedactionsList = new ArrayList<>(); - for (ColumnRedaction columnRedaction : columnRedactions) { - columnRedactionsList.add(V1ColumnRedactions.builder() - .columnName(columnRedaction.getColumnName()) - .redaction(columnRedaction.getRedaction()) - .build()); - } - return columnRedactionsList; - } - - private static List buildUniqueValues(List> uniqueValues) { - List uniqueValuesList = new ArrayList<>(); - for (Map uniqueValue : uniqueValues) { - uniqueValuesList.add(V1UniqueValue.builder().data(uniqueValue).build()); - } - return uniqueValuesList; - } - - public static V1GetRequest getGetRequestBody(GetRequest request, String vaultId) { - V1GetRequest.Builder builder = V1GetRequest.builder().vaultId(vaultId); - - if (request.getRecords() != null && !request.getRecords().isEmpty()) { - List recordsList = new ArrayList<>(); - for (GetRecordRequest record : request.getRecords()) { - V1GetRequestData.Builder recordBuilder = V1GetRequestData.builder() - .tableName(record.getTable()); - if (record.getIds() != null) { - recordBuilder.skyflowIDs(record.getIds()); - } - if (record.getFields() != null) { - recordBuilder.columns(record.getFields()); - } - if (record.getColumnRedactions() != null && !record.getColumnRedactions().isEmpty()) { - recordBuilder.columnRedactions(buildColumnRedactions(record.getColumnRedactions())); - } - if (record.getUniqueValues() != null && !record.getUniqueValues().isEmpty()) { - recordBuilder.uniqueValues(buildUniqueValues(record.getUniqueValues())); - } - recordsList.add(recordBuilder.build()); - } - builder.records(recordsList); - } else { - builder.tableName(request.getTable()); - if (request.getIds() != null) { - builder.skyflowIDs(request.getIds()); - } - if (request.getFields() != null) { - builder.columns(request.getFields()); - } - if (request.getColumnRedactions() != null && !request.getColumnRedactions().isEmpty()) { - builder.columnRedactions(buildColumnRedactions(request.getColumnRedactions())); - } - if (request.getUniqueValues() != null && !request.getUniqueValues().isEmpty()) { - builder.uniqueValues(buildUniqueValues(request.getUniqueValues())); - } - } - - if (request.getLimit() != null) { - builder.limit(request.getLimit()); - } - if (request.getOffset() != null) { - builder.offset(request.getOffset()); - } - return builder.build(); - } - - public static GetResponse buildGetResponse(V1GetResponse res) { - ArrayList> data = new ArrayList<>(); - ArrayList> errors = new ArrayList<>(); - if (res != null && res.getRecords().isPresent()) { - for (V1RecordResponseObject record : res.getRecords().get()) { - if (record.getError().isPresent()) { - HashMap errorRecord = new HashMap<>(); - record.getSkyflowId().ifPresent(skyflowId -> errorRecord.put("skyflowId", skyflowId)); - record.getTableName().ifPresent(tableName -> errorRecord.put("tableName", tableName)); - errorRecord.put("error", record.getError().get()); - record.getHttpCode().ifPresent(httpCode -> errorRecord.put("httpCode", httpCode)); - errors.add(errorRecord); - } else { - HashMap getRecord = new HashMap<>(); - if (record.getData().isPresent()) { - getRecord.putAll(record.getData().get()); - } - record.getSkyflowId().ifPresent(skyflowId -> getRecord.put("skyflowId", skyflowId)); - record.getTableName().ifPresent(tableName -> getRecord.put("tableName", tableName)); - if (record.getTokens().isPresent()) { - getRecord.putAll(record.getTokens().get()); - } - data.add(getRecord); - } - } - } - return new GetResponse(data, errors); - } } diff --git a/flowvault/src/main/java/com/skyflow/utils/validations/Validations.java b/flowvault/src/main/java/com/skyflow/utils/validations/Validations.java index 6896e6fe..2be203c4 100644 --- a/flowvault/src/main/java/com/skyflow/utils/validations/Validations.java +++ b/flowvault/src/main/java/com/skyflow/utils/validations/Validations.java @@ -3,6 +3,7 @@ import com.skyflow.config.Credentials; import com.skyflow.config.VaultConfig; import com.skyflow.enums.InterfaceName; +import com.skyflow.generated.rest.types.FlowEnumUpdateType; import com.skyflow.errors.ErrorCode; import com.skyflow.errors.ErrorMessage; import com.skyflow.errors.SkyflowException; @@ -118,7 +119,7 @@ public static void validateCredentials(Credentials credentials) throws SkyflowEx public static void validateVaultConfiguration(VaultConfig vaultConfig) throws SkyflowException { String vaultId = vaultConfig.getVaultId(); String clusterId = vaultConfig.getClusterId(); - String vaultURL = vaultConfig.getVaultURL(); + String vaultUrl = vaultConfig.getVaultUrl(); Credentials credentials = vaultConfig.getCredentials(); if (vaultId == null) { @@ -131,15 +132,15 @@ public static void validateVaultConfiguration(VaultConfig vaultConfig) throws Sk validateCredentials(credentials); } - if (vaultURL != null) { - if (vaultURL.trim().isEmpty()) { + if (vaultUrl != null) { + if (vaultUrl.trim().isEmpty()) { LogUtil.printErrorLog(ErrorLogs.EMPTY_VAULT_URL.getLog()); throw new SkyflowException(ErrorCode.INVALID_INPUT.getCode(), ErrorMessage.EmptyVaultUrl.getMessage()); - } else if (!Utils.isValidURL(vaultURL)) { + } else if (!Utils.isValidUrl(vaultUrl)) { LogUtil.printErrorLog(ErrorLogs.INVALID_VAULT_URL_FORMAT.getLog()); throw new SkyflowException(ErrorCode.INVALID_INPUT.getCode(), ErrorMessage.InvalidVaultUrlFormat.getMessage()); } - } else if (Utils.getEnvVaultURL() == null) { + } else if (Utils.getEnvVaultUrl() == null) { if (clusterId == null) { LogUtil.printErrorLog(ErrorLogs.EITHER_VAULT_URL_OR_CLUSTER_ID_REQUIRED.getLog()); throw new SkyflowException(ErrorCode.INVALID_INPUT.getCode(), ErrorMessage.EitherVaultUrlOrClusterIdRequired.getMessage()); @@ -158,8 +159,7 @@ public static void validateInsertRequest(InsertRequest insertRequest) throws Sky )); throw new SkyflowException(ErrorCode.INVALID_INPUT.getCode(), ErrorMessage.InsertRequestNull.getMessage()); } - String table = insertRequest.getTable(); - ArrayList records = insertRequest.getRecords(); + List records = insertRequest.getRecords(); if (records == null) { LogUtil.printErrorLog(Utils.parameterizedString( ErrorLogs.RECORDS_IS_REQUIRED.getLog(), InterfaceName.INSERT.getName() @@ -172,94 +172,121 @@ public static void validateInsertRequest(InsertRequest insertRequest) throws Sky throw new SkyflowException(ErrorCode.INVALID_INPUT.getCode(), ErrorMessage.EmptyRecords.getMessage()); } - for (InsertRecord record : records) { - if(record == null){ + for (InsertRequestRecord record : records) { + if (record == null) { LogUtil.printErrorLog(Utils.parameterizedString( ErrorLogs.INVALID_RECORD.getLog(), InterfaceName.INSERT.getName() )); throw new SkyflowException(ErrorCode.INVALID_INPUT.getCode(), ErrorMessage.InvalidRecord.getMessage()); } + validateUpsertOptions(record.getUpsert()); } + validateUpsertOptions(insertRequest.getUpsert()); + validateTableAndUpsertPlacement(insertRequest, records); - // table check if specified for both - if (insertRequest.getTable() != null && !table.trim().isEmpty()){ // if table name specified at both place - for (InsertRecord record : records) { - if (record.getTable() != null && !record.getTable().trim().isEmpty()){ - LogUtil.printErrorLog(Utils.parameterizedString( - ErrorLogs.TABLE_SPECIFIED_AT_BOTH_PLACE.getLog(), InterfaceName.INSERT.getName() - )); - throw new SkyflowException(ErrorCode.INVALID_INPUT.getCode(), ErrorMessage.TableSpecifiedInRequestAndRecordObject.getMessage()); + for (InsertRequestRecord record : records) { + if (record.getData() != null) { + for (String key : record.getData().keySet()) { + if (key == null || key.trim().isEmpty()) { + LogUtil.printErrorLog(Utils.parameterizedString( + ErrorLogs.EMPTY_OR_NULL_KEY_IN_VALUES.getLog(), InterfaceName.INSERT.getName() + )); + throw new SkyflowException(ErrorCode.INVALID_INPUT.getCode(), ErrorMessage.EmptyKeyInRecords.getMessage()); + } else { + Object value = record.getData().get(key); + if (value == null || value.toString().trim().isEmpty()) { + LogUtil.printErrorLog(Utils.parameterizedString( + ErrorLogs.EMPTY_OR_NULL_VALUE_IN_VALUES.getLog(), + InterfaceName.INSERT.getName(), key + )); + throw new SkyflowException(ErrorCode.INVALID_INPUT.getCode(), ErrorMessage.EmptyValueInValues.getMessage()); + } + } } } } - // table check if not specified for both or if missing in any object - if (insertRequest.getTable() == null || table.trim().isEmpty()){ // if table name specified at both place - for (InsertRecord record : records) { - if (record.getTable() == null || record.getTable().trim().isEmpty()){ - LogUtil.printErrorLog(Utils.parameterizedString( - ErrorLogs.TABLE_NOT_SPECIFIED_AT_BOTH_PLACE.getLog(), InterfaceName.INSERT.getName() - )); - throw new SkyflowException(ErrorCode.INVALID_INPUT.getCode(), ErrorMessage.TableNotSpecifiedInRequestAndRecordObject.getMessage()); - } - } + } + + /** + * Table name must live at exactly one level — either on the request, or on every record. + * Upsert is optional, but wherever it is supplied it must sit at the same level as the + * table name; it need not appear on every record. + */ + private static void validateTableAndUpsertPlacement( + InsertRequest insertRequest, List records) throws SkyflowException { + boolean tableAtRequest = hasText(insertRequest.getTableName()); + boolean upsertAtRequest = insertRequest.getUpsert() != null; + + int recordsWithTable = 0; + boolean upsertAtRecords = false; + for (InsertRequestRecord record : records) { + if (hasText(record.getTableName())) recordsWithTable++; + if (record.getUpsert() != null) upsertAtRecords = true; + } + boolean tableAtRecords = recordsWithTable > 0; + + // ── table name: exactly one level ──────────────────────────────────── + if (tableAtRequest && tableAtRecords) { + LogUtil.printErrorLog(Utils.parameterizedString( + ErrorLogs.TABLE_SPECIFIED_AT_BOTH_PLACE.getLog(), InterfaceName.INSERT.getName())); + throw new SkyflowException(ErrorCode.INVALID_INPUT.getCode(), + ErrorMessage.TableSpecifiedInRequestAndRecordObject.getMessage()); } - // upsert check 1 - if (insertRequest.getTable() != null && !table.trim().isEmpty()){ // if table name specified at both place - for (InsertRecord record : records) { - if (record.getUpsert() != null && record.getUpsert().isEmpty()) { - LogUtil.printErrorLog(Utils.parameterizedString( - ErrorLogs.EMPTY_UPSERT_VALUES.getLog(), InterfaceName.INSERT.getName() - )); - throw new SkyflowException(ErrorCode.INVALID_INPUT.getCode(), ErrorMessage.EmptyUpsertValues.getMessage()); - } - if (record.getUpsert() != null && !record.getUpsert().isEmpty()){ - LogUtil.printErrorLog(Utils.parameterizedString( - ErrorLogs.UPSERT_TABLE_REQUEST_AT_RECORD_LEVEL.getLog(), InterfaceName.INSERT.getName() - )); - throw new SkyflowException(ErrorCode.INVALID_INPUT.getCode(), ErrorMessage.UpsertTableRequestAtRecordLevel.getMessage()); - } - } + if (!tableAtRequest && recordsWithTable != records.size()) { + LogUtil.printErrorLog(Utils.parameterizedString( + ErrorLogs.TABLE_NOT_SPECIFIED_AT_BOTH_PLACE.getLog(), InterfaceName.INSERT.getName())); + throw new SkyflowException(ErrorCode.INVALID_INPUT.getCode(), + ErrorMessage.TableNotSpecifiedInRequestAndRecordObject.getMessage()); } - // upsert check 2 - if (insertRequest.getTable() == null || table.trim().isEmpty()){ - if (insertRequest.getUpsert() != null && !insertRequest.getUpsert().isEmpty()){ - LogUtil.printErrorLog(Utils.parameterizedString( - ErrorLogs.UPSERT_TABLE_REQUEST_AT_REQUEST_LEVEL.getLog(), InterfaceName.INSERT.getName() - )); - throw new SkyflowException(ErrorCode.INVALID_INPUT.getCode(), ErrorMessage.UpsertTableRequestAtRequestLevel.getMessage()); - } + + // ── upsert (optional) must match the table name's level ────────────── + if (upsertAtRecords && !tableAtRecords) { + LogUtil.printErrorLog(Utils.parameterizedString( + ErrorLogs.UPSERT_TABLE_REQUEST_AT_RECORD_LEVEL.getLog(), InterfaceName.INSERT.getName())); + throw new SkyflowException(ErrorCode.INVALID_INPUT.getCode(), + ErrorMessage.UpsertTableRequestAtRecordLevel.getMessage()); + } + if (upsertAtRequest && !tableAtRequest) { + LogUtil.printErrorLog(Utils.parameterizedString( + ErrorLogs.UPSERT_TABLE_REQUEST_AT_REQUEST_LEVEL.getLog(), InterfaceName.INSERT.getName())); + throw new SkyflowException(ErrorCode.INVALID_INPUT.getCode(), + ErrorMessage.UpsertTableRequestAtRequestLevel.getMessage()); } + } - if (insertRequest.getUpsert() != null && insertRequest.getUpsert().isEmpty()) { + private static boolean hasText(String value) { + return value != null && !value.trim().isEmpty(); + } + + private static void validateUpsertOptions(UpsertOptions upsert) throws SkyflowException { + if (upsert == null) { + return; + } + if (upsert.getUniqueColumns() == null || upsert.getUniqueColumns().isEmpty()) { LogUtil.printErrorLog(Utils.parameterizedString( ErrorLogs.EMPTY_UPSERT_VALUES.getLog(), InterfaceName.INSERT.getName() )); throw new SkyflowException(ErrorCode.INVALID_INPUT.getCode(), ErrorMessage.EmptyUpsertValues.getMessage()); } + // updateType is a free-form String on the request, but only the wire enum's values reach + // the wire. Reject anything else here rather than silently dropping it during mapping. + String updateType = upsert.getUpdateType(); + if (updateType != null && !isKnownUpdateType(updateType)) { + LogUtil.printErrorLog(Utils.parameterizedString( + ErrorLogs.INVALID_UPSERT_UPDATE_TYPE.getLog(), InterfaceName.INSERT.getName() + )); + throw new SkyflowException(ErrorCode.INVALID_INPUT.getCode(), + ErrorMessage.InvalidUpsertUpdateType.getMessage()); + } + } - for (InsertRecord record : records) { - if (record != null ) { - if (record.getData() != null){ - for (String key : record.getData().keySet()) { - if (key == null || key.trim().isEmpty()) { - LogUtil.printErrorLog(Utils.parameterizedString( - ErrorLogs.EMPTY_OR_NULL_KEY_IN_VALUES.getLog(), InterfaceName.INSERT.getName() - )); - throw new SkyflowException(ErrorCode.INVALID_INPUT.getCode(), ErrorMessage.EmptyKeyInRecords.getMessage()); - } else { - Object value = record.getData().get(key); - if (value == null || value.toString().trim().isEmpty()) { - LogUtil.printErrorLog(Utils.parameterizedString( - ErrorLogs.EMPTY_OR_NULL_VALUE_IN_VALUES.getLog(), - InterfaceName.INSERT.getName(), key - )); - throw new SkyflowException(ErrorCode.INVALID_INPUT.getCode(), ErrorMessage.EmptyValueInValues.getMessage()); - } - } - } - } + private static boolean isKnownUpdateType(String updateType) { + for (FlowEnumUpdateType type : FlowEnumUpdateType.values()) { + if (type.toString().equalsIgnoreCase(updateType)) { + return true; } } + return false; } public static void validateDetokenizeRequest(DetokenizeRequest request) throws SkyflowException { @@ -269,7 +296,7 @@ public static void validateDetokenizeRequest(DetokenizeRequest request) throws S )); throw new SkyflowException(ErrorCode.INVALID_INPUT.getCode(), ErrorMessage.DetokenizeRequestNull.getMessage()); } - ArrayList tokens = request.getDetokenizeData(); + List tokens = request.getTokens(); if (tokens == null || tokens.isEmpty()) { LogUtil.printErrorLog(Utils.parameterizedString( ErrorLogs.EMPTY_DETOKENIZE_DATA.getLog(), InterfaceName.DETOKENIZE.getName() @@ -278,8 +305,8 @@ public static void validateDetokenizeRequest(DetokenizeRequest request) throws S } for (int index = 0; index < tokens.size(); index++) { - DetokenizeData token = tokens.get(index); - if (token == null || token.getToken() == null || token.getToken().trim().isEmpty()) { + String token = tokens.get(index); + if (token == null || token.trim().isEmpty()) { LogUtil.printErrorLog(Utils.parameterizedString( ErrorLogs.EMPTY_OR_NULL_TOKEN_IN_DETOKENIZE_DATA.getLog(), InterfaceName.DETOKENIZE.getName(), @@ -310,239 +337,42 @@ public static void validateDetokenizeRequest(DetokenizeRequest request) throws S } - public static void validateDeleteTokensRequest(DeleteTokensRequest request) throws SkyflowException { - if (request == null) { - LogUtil.printErrorLog(Utils.parameterizedString( - ErrorLogs.DELETE_TOKENS_REQUEST_NULL.getLog(), InterfaceName.DELETE.getName() - )); - throw new SkyflowException(ErrorCode.INVALID_INPUT.getCode(), ErrorMessage.DeleteTokensRequestNull.getMessage()); - } - List tokens = request.getTokens(); - if (tokens == null || tokens.isEmpty()) { - LogUtil.printErrorLog(Utils.parameterizedString( - ErrorLogs.EMPTY_DELETE_TOKENS_DATA.getLog(), InterfaceName.DELETE.getName() - )); - throw new SkyflowException(ErrorCode.INVALID_INPUT.getCode(), ErrorMessage.EmptyDeleteTokensData.getMessage()); - } - - for (int index = 0; index < tokens.size(); index++) { - String token = tokens.get(index); - if (token == null || token.trim().isEmpty()) { - LogUtil.printErrorLog(Utils.parameterizedString( - ErrorLogs.EMPTY_OR_NULL_TOKEN_IN_DELETE_TOKENS_DATA.getLog(), - InterfaceName.DELETE.getName(), - String.valueOf(index))); - throw new SkyflowException(ErrorCode.INVALID_INPUT.getCode(), ErrorMessage.EmptyTokenInDeleteTokensData.getMessage()); - } - } - } + // ── Bulk (batched/concurrent) request validations ──────────────────────── - public static void validateTokenizeRequest(TokenizeRequest request) throws SkyflowException { - if (request == null) { - LogUtil.printErrorLog(Utils.parameterizedString( - ErrorLogs.TOKENIZE_REQUEST_NULL.getLog(), InterfaceName.TOKENIZE.getName() - )); - throw new SkyflowException(ErrorCode.INVALID_INPUT.getCode(), ErrorMessage.TokenizeRequestNull.getMessage()); - } - ArrayList data = request.getData(); - if (data == null || data.isEmpty()) { - LogUtil.printErrorLog(Utils.parameterizedString( - ErrorLogs.EMPTY_TOKENIZE_DATA.getLog(), InterfaceName.TOKENIZE.getName() - )); - throw new SkyflowException(ErrorCode.INVALID_INPUT.getCode(), ErrorMessage.EmptyTokenizeData.getMessage()); - } - for (int index = 0; index < data.size(); index++) { - TokenizeRecord record = data.get(index); - if (record == null) { - LogUtil.printErrorLog(Utils.parameterizedString( - ErrorLogs.TOKENIZE_RECORD_NULL.getLog(), InterfaceName.TOKENIZE.getName() - )); - throw new SkyflowException(ErrorCode.INVALID_INPUT.getCode(), ErrorMessage.TokenizeRecordNull.getMessage()); - } - Object value = record.getValue(); - boolean isInvalidValue = value == null || (value instanceof String && ((String) value).trim().isEmpty()); - if (isInvalidValue) { - LogUtil.printErrorLog(Utils.parameterizedString( - ErrorLogs.EMPTY_VALUE_IN_TOKENIZE_RECORD.getLog(), InterfaceName.TOKENIZE.getName() - )); - throw new SkyflowException(ErrorCode.INVALID_INPUT.getCode(), ErrorMessage.EmptyValueInTokenizeRecord.getMessage()); - } - List tokenGroupNames = record.getTokenGroupNames(); - if (tokenGroupNames == null || tokenGroupNames.isEmpty()) { - LogUtil.printErrorLog(Utils.parameterizedString( - ErrorLogs.EMPTY_TOKEN_GROUP_NAMES_IN_TOKENIZE_RECORD.getLog(), InterfaceName.TOKENIZE.getName() - )); - throw new SkyflowException(ErrorCode.INVALID_INPUT.getCode(), ErrorMessage.EmptyTokenGroupNamesInTokenizeRecord.getMessage()); - } - for (int groupIndex = 0; groupIndex < tokenGroupNames.size(); groupIndex++) { - String groupName = tokenGroupNames.get(groupIndex); - if (groupName == null || groupName.trim().isEmpty()) { - LogUtil.printErrorLog(Utils.parameterizedString( - ErrorLogs.EMPTY_TOKEN_GROUP_NAME_IN_TOKENIZE_RECORD.getLog(), - InterfaceName.TOKENIZE.getName(), - String.valueOf(groupIndex))); - throw new SkyflowException(ErrorCode.INVALID_INPUT.getCode(), ErrorMessage.EmptyTokenGroupNameInTokenizeRecord.getMessage()); - } - } + // BulkInsertRequest is an InsertRequest with no extra state, so the field rules are identical. + // The one addition: records must be BulkInsertRequestRecord, since BulkInsertResponse hands + // them back as such from getRecordsToRetry(). `records` is typed to the parent (it is + // inherited), so this is enforced here rather than by the compiler. + // The service accepts at most Constants.MAX_BULK_DATA_SIZE items per bulk call; batching + // splits the payload but does not lift that ceiling. + private static void validateBulkDataSize( + int size, ErrorLogs log, ErrorMessage message, InterfaceName interfaceName) throws SkyflowException { + if (size > Constants.MAX_BULK_DATA_SIZE) { + LogUtil.printErrorLog(Utils.parameterizedString(log.getLog(), interfaceName.getName())); + throw new SkyflowException(ErrorCode.INVALID_INPUT.getCode(), message.getMessage()); } } - // ── Bulk (batched/concurrent) request validations ──────────────────────── - public static void validateBulkInsertRequest(BulkInsertRequest insertRequest) throws SkyflowException { - if (insertRequest == null) { - LogUtil.printErrorLog(Utils.parameterizedString( - ErrorLogs.INSERT_REQUEST_NULL.getLog(), InterfaceName.INSERT.getName() - )); - throw new SkyflowException(ErrorCode.INVALID_INPUT.getCode(), ErrorMessage.InsertRequestNull.getMessage()); - } - String table = insertRequest.getTable(); - ArrayList records = insertRequest.getRecords(); - if (records == null) { - LogUtil.printErrorLog(Utils.parameterizedString( - ErrorLogs.RECORDS_IS_REQUIRED.getLog(), InterfaceName.INSERT.getName() - )); - throw new SkyflowException(ErrorCode.INVALID_INPUT.getCode(), ErrorMessage.RecordsKeyError.getMessage()); - } else if (records.isEmpty()) { - LogUtil.printErrorLog(Utils.parameterizedString( - ErrorLogs.EMPTY_RECORDS.getLog(), InterfaceName.INSERT.getName() - )); - throw new SkyflowException(ErrorCode.INVALID_INPUT.getCode(), ErrorMessage.EmptyRecords.getMessage()); - } + validateInsertRequest(insertRequest); + validateBulkDataSize(insertRequest.getRecords().size(), ErrorLogs.RECORD_SIZE_EXCEED, + ErrorMessage.RecordSizeExceedError, InterfaceName.INSERT); - for (BulkInsertRecord record : records) { - if (record == null) { + for (InsertRequestRecord record : insertRequest.getRecords()) { + if (!(record instanceof BulkInsertRequestRecord)) { LogUtil.printErrorLog(Utils.parameterizedString( ErrorLogs.INVALID_RECORD.getLog(), InterfaceName.INSERT.getName() )); throw new SkyflowException(ErrorCode.INVALID_INPUT.getCode(), ErrorMessage.InvalidRecord.getMessage()); } } - - // table check if specified for both - if (insertRequest.getTable() != null && !table.trim().isEmpty()) { - for (BulkInsertRecord record : records) { - if (record.getTable() != null && !record.getTable().trim().isEmpty()) { - LogUtil.printErrorLog(Utils.parameterizedString( - ErrorLogs.TABLE_SPECIFIED_AT_BOTH_PLACE.getLog(), InterfaceName.INSERT.getName() - )); - throw new SkyflowException(ErrorCode.INVALID_INPUT.getCode(), ErrorMessage.TableSpecifiedInRequestAndRecordObject.getMessage()); - } - } - } - // table check if not specified for both or if missing in any object - if (insertRequest.getTable() == null || table.trim().isEmpty()) { - for (BulkInsertRecord record : records) { - if (record.getTable() == null || record.getTable().trim().isEmpty()) { - LogUtil.printErrorLog(Utils.parameterizedString( - ErrorLogs.TABLE_NOT_SPECIFIED_AT_BOTH_PLACE.getLog(), InterfaceName.INSERT.getName() - )); - throw new SkyflowException(ErrorCode.INVALID_INPUT.getCode(), ErrorMessage.TableNotSpecifiedInRequestAndRecordObject.getMessage()); - } - } - } - // upsert check 1 - if (insertRequest.getTable() != null && !table.trim().isEmpty()) { - for (BulkInsertRecord record : records) { - if (record.getUpsert() != null && record.getUpsert().isEmpty()) { - LogUtil.printErrorLog(Utils.parameterizedString( - ErrorLogs.EMPTY_UPSERT_VALUES.getLog(), InterfaceName.INSERT.getName() - )); - throw new SkyflowException(ErrorCode.INVALID_INPUT.getCode(), ErrorMessage.EmptyUpsertValues.getMessage()); - } - if (record.getUpsert() != null && !record.getUpsert().isEmpty()) { - LogUtil.printErrorLog(Utils.parameterizedString( - ErrorLogs.UPSERT_TABLE_REQUEST_AT_RECORD_LEVEL.getLog(), InterfaceName.INSERT.getName() - )); - throw new SkyflowException(ErrorCode.INVALID_INPUT.getCode(), ErrorMessage.UpsertTableRequestAtRecordLevel.getMessage()); - } - } - } - // upsert check 2 - if (insertRequest.getTable() == null || table.trim().isEmpty()) { - if (insertRequest.getUpsert() != null && !insertRequest.getUpsert().isEmpty()) { - LogUtil.printErrorLog(Utils.parameterizedString( - ErrorLogs.UPSERT_TABLE_REQUEST_AT_REQUEST_LEVEL.getLog(), InterfaceName.INSERT.getName() - )); - throw new SkyflowException(ErrorCode.INVALID_INPUT.getCode(), ErrorMessage.UpsertTableRequestAtRequestLevel.getMessage()); - } - } - - if (insertRequest.getUpsert() != null && insertRequest.getUpsert().isEmpty()) { - LogUtil.printErrorLog(Utils.parameterizedString( - ErrorLogs.EMPTY_UPSERT_VALUES.getLog(), InterfaceName.INSERT.getName() - )); - throw new SkyflowException(ErrorCode.INVALID_INPUT.getCode(), ErrorMessage.EmptyUpsertValues.getMessage()); - } - - for (BulkInsertRecord record : records) { - if (record.getData() != null) { - for (String key : record.getData().keySet()) { - if (key == null || key.trim().isEmpty()) { - LogUtil.printErrorLog(Utils.parameterizedString( - ErrorLogs.EMPTY_OR_NULL_KEY_IN_VALUES.getLog(), InterfaceName.INSERT.getName() - )); - throw new SkyflowException(ErrorCode.INVALID_INPUT.getCode(), ErrorMessage.EmptyKeyInRecords.getMessage()); - } else { - Object value = record.getData().get(key); - if (value == null || value.toString().trim().isEmpty()) { - LogUtil.printErrorLog(Utils.parameterizedString( - ErrorLogs.EMPTY_OR_NULL_VALUE_IN_VALUES.getLog(), - InterfaceName.INSERT.getName(), key - )); - throw new SkyflowException(ErrorCode.INVALID_INPUT.getCode(), ErrorMessage.EmptyValueInValues.getMessage()); - } - } - } - } - } } + // BulkDetokenizeRequest is a DetokenizeRequest with no extra state, so the rules are identical. public static void validateBulkDetokenizeRequest(BulkDetokenizeRequest request) throws SkyflowException { - if (request == null) { - LogUtil.printErrorLog(Utils.parameterizedString( - ErrorLogs.DETOKENIZE_REQUEST_NULL.getLog(), InterfaceName.DETOKENIZE.getName() - )); - throw new SkyflowException(ErrorCode.INVALID_INPUT.getCode(), ErrorMessage.DetokenizeRequestNull.getMessage()); - } - List tokens = request.getTokens(); - if (tokens == null || tokens.isEmpty()) { - LogUtil.printErrorLog(Utils.parameterizedString( - ErrorLogs.EMPTY_DETOKENIZE_DATA.getLog(), InterfaceName.DETOKENIZE.getName() - )); - throw new SkyflowException(ErrorCode.INVALID_INPUT.getCode(), ErrorMessage.EmptyDetokenizeData.getMessage()); - } - - for (int index = 0; index < tokens.size(); index++) { - String token = tokens.get(index); - if (token == null || token.trim().isEmpty()) { - LogUtil.printErrorLog(Utils.parameterizedString( - ErrorLogs.EMPTY_OR_NULL_TOKEN_IN_DETOKENIZE_DATA.getLog(), - InterfaceName.DETOKENIZE.getName(), - String.valueOf(index))); - throw new SkyflowException(ErrorCode.INVALID_INPUT.getCode(), ErrorMessage.EmptyTokenInDetokenizeData.getMessage()); - } - } - - List groupRedactions = request.getTokenGroupRedactions(); - if (groupRedactions != null && !groupRedactions.isEmpty()) { - for (BulkTokenGroupRedactions group : groupRedactions) { - if (group == null) { - LogUtil.printErrorLog(Utils.parameterizedString(ErrorLogs.NULL_TOKEN_REDACTION_GROUP_OBJECT.getLog(), InterfaceName.DETOKENIZE.getName())); - throw new SkyflowException(ErrorCode.INVALID_INPUT.getCode(), ErrorMessage.NullTokenGroupRedactions.getMessage()); - } - String groupName = group.getTokenGroupName(); - String redaction = group.getRedaction(); - if (groupName == null || groupName.trim().isEmpty()) { - LogUtil.printErrorLog(Utils.parameterizedString(ErrorLogs.NULL_TOKEN_GROUP_NAME_IN_TOKEN_GROUP.getLog(), InterfaceName.DETOKENIZE.getName())); - throw new SkyflowException(ErrorCode.INVALID_INPUT.getCode(), ErrorMessage.NullTokenGroupNameInTokenGroup.getMessage()); - } - if (redaction == null || redaction.trim().isEmpty()) { - LogUtil.printErrorLog(Utils.parameterizedString(ErrorLogs.EMPTY_OR_NULL_REDACTION_IN_TOKEN_GROUP.getLog(), InterfaceName.DETOKENIZE.getName())); - throw new SkyflowException(ErrorCode.INVALID_INPUT.getCode(), ErrorMessage.NullRedactionInTokenGroup.getMessage()); - } - } - } + validateDetokenizeRequest(request); + validateBulkDataSize(request.getTokens().size(), ErrorLogs.TOKENS_SIZE_EXCEED, + ErrorMessage.TokensSizeExceedError, InterfaceName.DETOKENIZE); } public static void validateBulkDeleteTokensRequest(BulkDeleteTokensRequest request) throws SkyflowException { @@ -559,6 +389,8 @@ public static void validateBulkDeleteTokensRequest(BulkDeleteTokensRequest reque )); throw new SkyflowException(ErrorCode.INVALID_INPUT.getCode(), ErrorMessage.EmptyDeleteTokensData.getMessage()); } + validateBulkDataSize(tokens.size(), ErrorLogs.DELETE_TOKENS_SIZE_EXCEED, + ErrorMessage.DeleteTokensSizeExceedError, InterfaceName.DELETE); for (int index = 0; index < tokens.size(); index++) { String token = tokens.get(index); @@ -586,6 +418,8 @@ public static void validateBulkTokenizeRequest(BulkTokenizeRequest request) thro )); throw new SkyflowException(ErrorCode.INVALID_INPUT.getCode(), ErrorMessage.EmptyTokenizeData.getMessage()); } + validateBulkDataSize(data.size(), ErrorLogs.TOKENIZE_DATA_SIZE_EXCEED, + ErrorMessage.TokenizeDataSizeExceedError, InterfaceName.TOKENIZE); for (int i = 0; i < data.size(); i++) { BulkTokenizeRecord record = data.get(i); if (record == null) { @@ -620,165 +454,4 @@ public static void validateBulkTokenizeRequest(BulkTokenizeRequest request) thro } } - public static void validateQueryRequest(QueryRequest queryRequest) throws SkyflowException { - String query = queryRequest.getQuery(); - if (query == null) { - LogUtil.printErrorLog(Utils.parameterizedString( - ErrorLogs.QUERY_IS_REQUIRED.getLog(), InterfaceName.QUERY.getName() - )); - throw new SkyflowException(ErrorCode.INVALID_INPUT.getCode(), ErrorMessage.QueryKeyError.getMessage()); - } else if (query.trim().isEmpty()) { - LogUtil.printErrorLog(Utils.parameterizedString( - ErrorLogs.EMPTY_QUERY.getLog(), InterfaceName.QUERY.getName() - )); - throw new SkyflowException(ErrorCode.INVALID_INPUT.getCode(), ErrorMessage.EmptyQuery.getMessage()); - } - } - - public static void validateGetRequest(GetRequest getRequest) throws SkyflowException { - String table = getRequest.getTable(); - ArrayList ids = getRequest.getIds(); - ArrayList fields = getRequest.getFields(); - List> uniqueValues = getRequest.getUniqueValues(); - List columnRedactions = getRequest.getColumnRedactions(); - List records = getRequest.getRecords(); - - boolean hasSingleTableFields = table != null || ids != null || fields != null - || uniqueValues != null || columnRedactions != null; - boolean hasRecords = records != null && !records.isEmpty(); - - if (hasRecords) { - if (hasSingleTableFields) { - LogUtil.printErrorLog(Utils.parameterizedString( - ErrorLogs.BOTH_SINGLE_TABLE_FIELDS_AND_RECORDS_PASSED.getLog(), InterfaceName.GET.getName() - )); - throw new SkyflowException(ErrorCode.INVALID_INPUT.getCode(), ErrorMessage.BothSingleTableFieldsAndRecordsSpecified.getMessage()); - } - for (GetRecordRequest record : records) { - if (record == null) { - LogUtil.printErrorLog(Utils.parameterizedString( - ErrorLogs.NULL_GET_RECORD_REQUEST_OBJECT.getLog(), InterfaceName.GET.getName() - )); - throw new SkyflowException(ErrorCode.INVALID_INPUT.getCode(), ErrorMessage.NullGetRecordRequest.getMessage()); - } - validateSingleTableLookup(record.getTable(), record.getIds(), record.getFields(), - record.getUniqueValues(), record.getColumnRedactions()); - } - } else { - validateSingleTableLookup(table, ids, fields, uniqueValues, columnRedactions); - } - } - - private static void validateSingleTableLookup( - String table, ArrayList ids, ArrayList fields, - List> uniqueValues, List columnRedactions - ) throws SkyflowException { - if (table == null) { - LogUtil.printErrorLog(Utils.parameterizedString( - ErrorLogs.TABLE_IS_REQUIRED.getLog(), InterfaceName.GET.getName() - )); - throw new SkyflowException(ErrorCode.INVALID_INPUT.getCode(), ErrorMessage.TableKeyError.getMessage()); - } else if (table.trim().isEmpty()) { - LogUtil.printErrorLog(Utils.parameterizedString( - ErrorLogs.EMPTY_TABLE_NAME.getLog(), InterfaceName.GET.getName() - )); - throw new SkyflowException(ErrorCode.INVALID_INPUT.getCode(), ErrorMessage.EmptyTable.getMessage()); - } - - if (ids != null) { - if (ids.isEmpty()) { - LogUtil.printErrorLog(Utils.parameterizedString( - ErrorLogs.EMPTY_IDS.getLog(), InterfaceName.GET.getName() - )); - throw new SkyflowException(ErrorCode.INVALID_INPUT.getCode(), ErrorMessage.EmptyIds.getMessage()); - } else { - for (int index = 0; index < ids.size(); index++) { - String id = ids.get(index); - if (id == null || id.trim().isEmpty()) { - LogUtil.printErrorLog(Utils.parameterizedString( - ErrorLogs.EMPTY_OR_NULL_ID_IN_IDS.getLog(), - InterfaceName.GET.getName(), String.valueOf(index) - )); - throw new SkyflowException(ErrorCode.INVALID_INPUT.getCode(), ErrorMessage.EmptyIdInIds.getMessage()); - } - } - } - } - - if (fields != null) { - if (fields.isEmpty()) { - LogUtil.printErrorLog(Utils.parameterizedString( - ErrorLogs.EMPTY_FIELDS.getLog(), InterfaceName.GET.getName() - )); - throw new SkyflowException(ErrorCode.INVALID_INPUT.getCode(), ErrorMessage.EmptyFields.getMessage()); - } else { - for (int index = 0; index < fields.size(); index++) { - String field = fields.get(index); - if (field == null || field.trim().isEmpty()) { - LogUtil.printErrorLog(Utils.parameterizedString( - ErrorLogs.EMPTY_OR_NULL_FIELD_IN_FIELDS.getLog(), - InterfaceName.GET.getName(), String.valueOf(index) - )); - throw new SkyflowException(ErrorCode.INVALID_INPUT.getCode(), ErrorMessage.EmptyFieldInFields.getMessage()); - } - } - } - } - - if (ids == null && uniqueValues == null) { - LogUtil.printErrorLog(Utils.parameterizedString( - ErrorLogs.NEITHER_IDS_NOR_UNIQUE_VALUES_PASSED.getLog(), InterfaceName.GET.getName() - )); - throw new SkyflowException(ErrorCode.INVALID_INPUT.getCode(), ErrorMessage.IdsOrUniqueValuesKeyError.getMessage()); - } else if (ids != null && uniqueValues != null) { - LogUtil.printErrorLog(Utils.parameterizedString( - ErrorLogs.BOTH_IDS_AND_UNIQUE_VALUES_PASSED.getLog(), InterfaceName.GET.getName() - )); - throw new SkyflowException(ErrorCode.INVALID_INPUT.getCode(), ErrorMessage.BothIdsAndUniqueValuesSpecified.getMessage()); - } else if (uniqueValues != null) { - if (uniqueValues.isEmpty()) { - LogUtil.printErrorLog(Utils.parameterizedString( - ErrorLogs.EMPTY_UNIQUE_VALUES.getLog(), InterfaceName.GET.getName() - )); - throw new SkyflowException(ErrorCode.INVALID_INPUT.getCode(), ErrorMessage.EmptyUniqueValues.getMessage()); - } else { - for (int index = 0; index < uniqueValues.size(); index++) { - Map uniqueValue = uniqueValues.get(index); - if (uniqueValue == null || uniqueValue.isEmpty()) { - LogUtil.printErrorLog(Utils.parameterizedString( - ErrorLogs.EMPTY_OR_NULL_UNIQUE_VALUE_IN_UNIQUE_VALUES.getLog(), - InterfaceName.GET.getName(), String.valueOf(index) - )); - throw new SkyflowException(ErrorCode.INVALID_INPUT.getCode(), ErrorMessage.EmptyUniqueValueInUniqueValues.getMessage()); - } - } - } - } - - if (columnRedactions != null && !columnRedactions.isEmpty()) { - for (ColumnRedaction columnRedaction : columnRedactions) { - if (columnRedaction == null) { - LogUtil.printErrorLog(Utils.parameterizedString( - ErrorLogs.NULL_COLUMN_REDACTION_OBJECT.getLog(), InterfaceName.GET.getName() - )); - throw new SkyflowException(ErrorCode.INVALID_INPUT.getCode(), ErrorMessage.NullColumnRedactions.getMessage()); - } - String columnName = columnRedaction.getColumnName(); - String redaction = columnRedaction.getRedaction(); - if (columnName == null || columnName.trim().isEmpty()) { - LogUtil.printErrorLog(Utils.parameterizedString( - ErrorLogs.NULL_COLUMN_NAME_IN_COLUMN_REDACTION.getLog(), InterfaceName.GET.getName() - )); - throw new SkyflowException(ErrorCode.INVALID_INPUT.getCode(), ErrorMessage.NullColumnNameInColumnRedaction.getMessage()); - } - if (redaction == null || redaction.trim().isEmpty()) { - LogUtil.printErrorLog(Utils.parameterizedString( - ErrorLogs.EMPTY_OR_NULL_REDACTION_IN_COLUMN_REDACTION.getLog(), InterfaceName.GET.getName() - )); - throw new SkyflowException(ErrorCode.INVALID_INPUT.getCode(), ErrorMessage.NullRedactionInColumnRedaction.getMessage()); - } - } - } - } - } diff --git a/flowvault/src/main/java/com/skyflow/vault/controller/VaultController.java b/flowvault/src/main/java/com/skyflow/vault/controller/VaultController.java index 342346e8..cbe697e6 100644 --- a/flowvault/src/main/java/com/skyflow/vault/controller/VaultController.java +++ b/flowvault/src/main/java/com/skyflow/vault/controller/VaultController.java @@ -1,5 +1,13 @@ package com.skyflow.vault.controller; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + import com.google.gson.Gson; import com.google.gson.GsonBuilder; import com.google.gson.JsonObject; @@ -8,7 +16,6 @@ import com.skyflow.config.VaultConfig; import com.skyflow.errors.SkyflowException; import com.skyflow.generated.rest.core.ApiClientApiException; -import com.skyflow.generated.rest.core.ApiClientException; import com.skyflow.generated.rest.core.ApiClientHttpResponse; import com.skyflow.generated.rest.core.RequestOptions; import com.skyflow.generated.rest.resources.flowservice.requests.V1InsertRequest; @@ -16,7 +23,6 @@ import com.skyflow.generated.rest.types.V1FlowTokenizeResponse; import com.skyflow.generated.rest.types.V1InsertRecordData; import com.skyflow.generated.rest.types.V1InsertResponse; -import com.skyflow.generated.rest.types.V1Upsert; import com.skyflow.logs.ErrorLogs; import com.skyflow.logs.InfoLogs; import com.skyflow.logs.WarningLogs; @@ -24,22 +30,32 @@ import com.skyflow.utils.Utils; import com.skyflow.utils.logger.LogUtil; import com.skyflow.utils.validations.Validations; -import com.skyflow.vault.data.*; +import com.skyflow.vault.data.BulkDeleteTokensRequest; +import com.skyflow.vault.data.BulkDeleteTokensResponse; +import com.skyflow.vault.data.BulkDetokenizeRequest; +import com.skyflow.vault.data.BulkDetokenizeResponse; +import com.skyflow.vault.data.BulkDetokenizeResponseRecord; +import com.skyflow.vault.data.BulkInsertRequest; +import com.skyflow.vault.data.BulkInsertResponse; +import com.skyflow.vault.data.BulkInsertResponseRecord; +import com.skyflow.vault.data.BulkTokenizeRecord; +import com.skyflow.vault.data.BulkTokenizeRequest; +import com.skyflow.vault.data.BulkTokenizeResponse; +import com.skyflow.vault.data.DeleteTokensOptions; +import com.skyflow.vault.data.DeleteTokensSuccess; +import com.skyflow.vault.data.DetokenizeOptions; +import com.skyflow.vault.data.ErrorRecord; +import com.skyflow.vault.data.InsertOptions; +import com.skyflow.vault.data.InsertRequestRecord; +import com.skyflow.vault.data.RequestContext; +import com.skyflow.vault.data.RequestInterceptor; +import com.skyflow.vault.data.TokenizeOptions; +import com.skyflow.vault.data.TokenizeSuccess; + import io.github.cdimascio.dotenv.Dotenv; import io.github.cdimascio.dotenv.DotenvException; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; - -import static com.skyflow.utils.Utils.*; - -public final class VaultController extends VaultClient - implements IVaultController { +public final class VaultController extends VaultClient { private static final Gson gson = new GsonBuilder().serializeNulls().create(); private JsonObject metrics = Utils.getMetrics(); @@ -70,190 +86,6 @@ private RequestOptions buildRequestOptions(RequestContext context) { return builder.build(); } - @Override - public InsertResponse insert(InsertRequest request) throws SkyflowException { - return insert(request, null); - } - - public InsertResponse insert(InsertRequest request, InsertOptions options) throws SkyflowException { - try { - LogUtil.printInfoLog(InfoLogs.INSERT_TRIGGERED.getLog()); - LogUtil.printInfoLog(InfoLogs.VALIDATE_INSERT_REQUEST.getLog()); - Validations.validateInsertRequest(request); - setBearerToken(); - V1InsertRequest insertRequest = getBulkInsertRequestBody(request, super.getVaultConfig()); - RequestInterceptor interceptor = options != null ? options.getInterceptor() : null; - RequestContext ctx = new RequestContext("INSERT"); - if (interceptor != null) interceptor.intercept(ctx); - V1InsertResponse response = this.getRecordsApi().withRawResponse().insert(insertRequest, buildRequestOptions(ctx)).body(); - return buildInsertResponse(response); - } catch (ApiClientApiException e) { - String bodyString = gson.toJson(e.body()); - LogUtil.printErrorLog(ErrorLogs.INSERT_RECORDS_REJECTED.getLog()); - throw new SkyflowException(e.statusCode(), e, e.headers(), bodyString); - } catch (ApiClientException e) { - LogUtil.printErrorLog(ErrorLogs.INSERT_RECORDS_REJECTED.getLog()); - throw new SkyflowException(e.getMessage()); - } - } - - @Override - public DetokenizeResponse detokenize(DetokenizeRequest request) throws SkyflowException { - return detokenize(request, null); - } - - public DetokenizeResponse detokenize(DetokenizeRequest request, DetokenizeOptions options) throws SkyflowException { - try { - LogUtil.printInfoLog(InfoLogs.DETOKENIZE_TRIGGERED.getLog()); - LogUtil.printInfoLog(InfoLogs.VALIDATE_DETOKENIZE_REQUEST.getLog()); - Validations.validateDetokenizeRequest(request); - setBearerToken(); - - com.skyflow.generated.rest.resources.flowservice.requests.V1FlowDetokenizeRequest requestDetokenize = getDetokenizeRequestBody(request, this.getVaultConfig().getVaultId()); - RequestInterceptor interceptor = options != null ? options.getInterceptor() : null; - RequestContext ctx = new RequestContext("DETOKENIZE"); - if (interceptor != null) interceptor.intercept(ctx); - com.skyflow.generated.rest.types.V1FlowDetokenizeResponse response = this.getRecordsApi().withRawResponse().detokenize(requestDetokenize, buildRequestOptions(ctx)).body(); - - return buildDetokenizeResponse(response); - } catch (ApiClientApiException e) { - String bodyString = gson.toJson(e.body()); - LogUtil.printErrorLog(ErrorLogs.DETOKENIZE_REQUEST_REJECTED.getLog()); - throw new SkyflowException(e.statusCode(), e, e.headers(), bodyString); - } catch (ApiClientException e) { - LogUtil.printErrorLog(ErrorLogs.DETOKENIZE_REQUEST_REJECTED.getLog()); - throw new SkyflowException(e.getMessage()); - } - } - - public TokenizeResponse tokenize(TokenizeRequest request) throws SkyflowException { - return tokenize(request, null); - } - - public TokenizeResponse tokenize(TokenizeRequest request, TokenizeOptions options) throws SkyflowException { - try { - LogUtil.printInfoLog(InfoLogs.TOKENIZE_TRIGGERED.getLog()); - LogUtil.printInfoLog(InfoLogs.VALIDATING_TOKENIZE_REQUEST.getLog()); - Validations.validateTokenizeRequest(request); - setBearerToken(); - - com.skyflow.generated.rest.resources.flowservice.requests.V1FlowTokenizeRequest requestTokenize = getTokenizeRequestBody(request, this.getVaultConfig().getVaultId()); - RequestInterceptor interceptor = options != null ? options.getInterceptor() : null; - RequestContext ctx = new RequestContext("TOKENIZE"); - if (interceptor != null) interceptor.intercept(ctx); - ApiClientHttpResponse response = this.getRecordsApi().withRawResponse().tokenize(requestTokenize, buildRequestOptions(ctx)); - LogUtil.printInfoLog(InfoLogs.TOKENIZE_REQUEST_RESOLVED.getLog()); - - TokenizeResponse tokenizeResponse = buildTokenizeResponse(response.body(), response.headers(), request.getData().size()); - LogUtil.printInfoLog(InfoLogs.TOKENIZE_SUCCESS.getLog()); - return tokenizeResponse; - } catch (ApiClientApiException e) { - String bodyString = gson.toJson(e.body()); - LogUtil.printErrorLog(ErrorLogs.TOKENIZE_REQUEST_REJECTED.getLog()); - throw new SkyflowException(e.statusCode(), e, e.headers(), bodyString); - } catch (ApiClientException e) { - LogUtil.printErrorLog(ErrorLogs.TOKENIZE_REQUEST_REJECTED.getLog()); - throw new SkyflowException(e.getMessage()); - } - } - - public DeleteTokensResponse deleteTokens(DeleteTokensRequest request) throws SkyflowException { - return deleteTokens(request, null); - } - - public DeleteTokensResponse deleteTokens(DeleteTokensRequest request, DeleteTokensOptions options) throws SkyflowException { - try { - LogUtil.printInfoLog(InfoLogs.DELETE_TOKENS_TRIGGERED.getLog()); - LogUtil.printInfoLog(InfoLogs.VALIDATE_DELETE_TOKENS_REQUEST.getLog()); - Validations.validateDeleteTokensRequest(request); - setBearerToken(); - - com.skyflow.generated.rest.resources.flowservice.requests.V1FlowDeleteTokenRequest requestDeleteTokens = getDeleteTokensRequestBody(request, this.getVaultConfig().getVaultId()); - RequestInterceptor interceptor = options != null ? options.getInterceptor() : null; - RequestContext ctx = new RequestContext("DELETE_TOKENS"); - if (interceptor != null) interceptor.intercept(ctx); - ApiClientHttpResponse response = this.getRecordsApi().withRawResponse().deletetoken(requestDeleteTokens, buildRequestOptions(ctx)); - LogUtil.printInfoLog(InfoLogs.DELETE_TOKENS_REQUEST_RESOLVED.getLog()); - - DeleteTokensResponse deleteTokensResponse = buildDeleteTokensResponse(response.body(), response.headers(), request.getTokens().size()); - LogUtil.printInfoLog(InfoLogs.DELETE_TOKENS_SUCCESS.getLog()); - return deleteTokensResponse; - } catch (ApiClientApiException e) { - String bodyString = gson.toJson(e.body()); - LogUtil.printErrorLog(ErrorLogs.DELETE_TOKENS_REQUEST_REJECTED.getLog()); - throw new SkyflowException(e.statusCode(), e, e.headers(), bodyString); - } catch (ApiClientException e) { - LogUtil.printErrorLog(ErrorLogs.DELETE_TOKENS_REQUEST_REJECTED.getLog()); - throw new SkyflowException(e.getMessage()); - } - } - - public QueryResponse query(QueryRequest queryRequest) throws SkyflowException { - return query(queryRequest, null); - } - - public QueryResponse query(QueryRequest queryRequest, QueryOptions options) throws SkyflowException { - try { - LogUtil.printInfoLog(InfoLogs.QUERY_TRIGGERED.getLog()); - LogUtil.printInfoLog(InfoLogs.VALIDATING_QUERY_REQUEST.getLog()); - Validations.validateQueryRequest(queryRequest); - setBearerToken(); - - com.skyflow.generated.rest.resources.records.requests.V1ExecuteQueryRequest requestBody = - getQueryRequestBody(queryRequest, this.getVaultConfig().getVaultId()); - RequestInterceptor interceptor = options != null ? options.getInterceptor() : null; - RequestContext ctx = new RequestContext("QUERY"); - if (interceptor != null) interceptor.intercept(ctx); - com.skyflow.generated.rest.types.V1ExecuteQueryResponse response = - this.getQueryApi().withRawResponse().flowServiceExecuteQuery(requestBody, buildRequestOptions(ctx)).body(); - LogUtil.printInfoLog(InfoLogs.QUERY_REQUEST_RESOLVED.getLog()); - - QueryResponse queryResponse = buildQueryResponse(response); - LogUtil.printInfoLog(InfoLogs.QUERY_SUCCESS.getLog()); - return queryResponse; - } catch (ApiClientApiException e) { - String bodyString = gson.toJson(e.body()); - LogUtil.printErrorLog(ErrorLogs.QUERY_REQUEST_REJECTED.getLog()); - throw new SkyflowException(e.statusCode(), e, e.headers(), bodyString); - } catch (ApiClientException e) { - LogUtil.printErrorLog(ErrorLogs.QUERY_REQUEST_REJECTED.getLog()); - throw new SkyflowException(e.getMessage()); - } - } - - public GetResponse get(GetRequest getRequest) throws SkyflowException { - return get(getRequest, null); - } - - public GetResponse get(GetRequest getRequest, GetOptions options) throws SkyflowException { - try { - LogUtil.printInfoLog(InfoLogs.GET_TRIGGERED.getLog()); - LogUtil.printInfoLog(InfoLogs.VALIDATE_GET_REQUEST.getLog()); - Validations.validateGetRequest(getRequest); - setBearerToken(); - - com.skyflow.generated.rest.resources.flowservice.requests.V1GetRequest requestBody = - getGetRequestBody(getRequest, this.getVaultConfig().getVaultId()); - RequestInterceptor interceptor = options != null ? options.getInterceptor() : null; - RequestContext ctx = new RequestContext("GET"); - if (interceptor != null) interceptor.intercept(ctx); - com.skyflow.generated.rest.types.V1GetResponse response = - this.getRecordsApi().withRawResponse().get(requestBody, buildRequestOptions(ctx)).body(); - LogUtil.printInfoLog(InfoLogs.GET_REQUEST_RESOLVED.getLog()); - - GetResponse getResponse = buildGetResponse(response); - LogUtil.printInfoLog(InfoLogs.GET_SUCCESS.getLog()); - return getResponse; - } catch (ApiClientApiException e) { - String bodyString = gson.toJson(e.body()); - LogUtil.printErrorLog(ErrorLogs.GET_REQUEST_REJECTED.getLog()); - throw new SkyflowException(e.statusCode(), e, e.headers(), bodyString); - } catch (ApiClientException e) { - LogUtil.printErrorLog(ErrorLogs.GET_REQUEST_REJECTED.getLog()); - throw new SkyflowException(e.getMessage()); - } - } - // ── Bulk Insert ─────────────────────────────────────────────────────────── public BulkInsertResponse bulkInsert(BulkInsertRequest insertRequest) throws SkyflowException { @@ -300,26 +132,20 @@ public CompletableFuture bulkInsertAsync(BulkInsertRequest i setBearerToken(); V1InsertRequest request = Utils.getBulkInsertRequestBody(insertRequest, this.getVaultConfig()); RequestInterceptor interceptor = options != null ? options.getInterceptor() : null; - List errorRecords = Collections.synchronizedList(new ArrayList<>()); - List> futures = this.insertBatchFutures(request, errorRecords, interceptor, cfg); + List> futures = this.insertBatchFutures(request, interceptor, cfg); return CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])) .thenApply(v -> { - List successRecords = new ArrayList<>(); + List records = new ArrayList<>(); for (CompletableFuture future : futures) { BulkInsertResponse futureResponse = future.join(); - if (futureResponse != null) { - if (futureResponse.getSuccess() != null) { - successRecords.addAll(futureResponse.getSuccess()); - } - if (futureResponse.getErrors() != null) { - errorRecords.addAll(futureResponse.getErrors()); - } + if (futureResponse != null && futureResponse.getRecords() != null) { + records.addAll(futureResponse.getRecords()); } } - return new BulkInsertResponse(successRecords, errorRecords, insertRequest.getRecords()); + return new BulkInsertResponse(records, insertRequest.getRecords()); }); } catch (ApiClientApiException e) { String bodyString = gson.toJson(e.body()); @@ -375,29 +201,23 @@ public CompletableFuture bulkDetokenizeAsync(BulkDetoken LogUtil.printInfoLog(InfoLogs.PROCESSING_BATCHES.getLog()); - List errorTokens = Collections.synchronizedList(new ArrayList<>()); - List successRecords = new ArrayList<>(); + List records = new ArrayList<>(); List batches = Utils.createBulkDetokenizeBatches(request, cfg.batchSize); executor = Executors.newFixedThreadPool(cfg.concurrencyLimit); - List> futures = this.detokenizeBatchFutures(executor, batches, errorTokens, interceptor, cfg.batchSize); + List> futures = this.detokenizeBatchFutures(executor, batches, interceptor, cfg.batchSize); return CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])) .thenApply(v -> { for (CompletableFuture future : futures) { BulkDetokenizeResponse futureResponse = future.join(); - if (futureResponse != null) { - if (futureResponse.getSuccess() != null) { - successRecords.addAll(futureResponse.getSuccess()); - } - if (futureResponse.getErrors() != null) { - errorTokens.addAll(futureResponse.getErrors()); - } + if (futureResponse != null && futureResponse.getRecords() != null) { + records.addAll(futureResponse.getRecords()); } } LogUtil.printInfoLog(InfoLogs.DETOKENIZE_REQUEST_RESOLVED.getLog()); - return new BulkDetokenizeResponse(successRecords, errorTokens, detokenizeRequest.getTokens()); + return new BulkDetokenizeResponse(records, detokenizeRequest.getTokens()); }); } catch (ApiClientApiException e) { String bodyString = gson.toJson(e.body()); @@ -947,14 +767,13 @@ private BatchConfig configureDetokenizeConcurrencyAndBatchSize(int totalRequests private BulkInsertResponse processBulkInsertSync( V1InsertRequest insertRequest, - ArrayList originalPayload, + List originalPayload, RequestInterceptor interceptor, BatchConfig cfg ) throws ExecutionException, InterruptedException, SkyflowException { LogUtil.printInfoLog(InfoLogs.PROCESSING_BATCHES.getLog()); - List successRecords = new ArrayList<>(); - List errorRecords = Collections.synchronizedList(new ArrayList<>()); - List> futures = this.insertBatchFutures(insertRequest, errorRecords, interceptor, cfg); + List records = new ArrayList<>(); + List> futures = this.insertBatchFutures(insertRequest, interceptor, cfg); try { CompletableFuture allFutures = CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])); @@ -965,20 +784,15 @@ private BulkInsertResponse processBulkInsertSync( } for (CompletableFuture future : futures) { BulkInsertResponse futureResponse = future.get(); - if (futureResponse != null) { - if (futureResponse.getSuccess() != null) { - successRecords.addAll(futureResponse.getSuccess()); - } - if (futureResponse.getErrors() != null) { - errorRecords.addAll(futureResponse.getErrors()); - } + if (futureResponse != null && futureResponse.getRecords() != null) { + records.addAll(futureResponse.getRecords()); } } } catch (Exception e) { LogUtil.printErrorLog(ErrorLogs.INSERT_RECORDS_REJECTED.getLog()); throw new SkyflowException(e.getMessage()); } - BulkInsertResponse response = new BulkInsertResponse(successRecords, errorRecords, originalPayload); + BulkInsertResponse response = new BulkInsertResponse(records, originalPayload); LogUtil.printInfoLog(InfoLogs.INSERT_REQUEST_RESOLVED.getLog()); return response; } @@ -990,13 +804,12 @@ private BulkDetokenizeResponse processBulkDetokenizeSync( BatchConfig cfg ) throws ExecutionException, InterruptedException, SkyflowException { LogUtil.printInfoLog(InfoLogs.PROCESSING_BATCHES.getLog()); - List errorTokens = Collections.synchronizedList(new ArrayList<>()); - List successTokens = new ArrayList<>(); + List records = new ArrayList<>(); ExecutorService executor = Executors.newFixedThreadPool(cfg.concurrencyLimit); List batches = Utils.createBulkDetokenizeBatches(detokenizeRequest, cfg.batchSize); try { - List> futures = this.detokenizeBatchFutures(executor, batches, errorTokens, interceptor, cfg.batchSize); + List> futures = this.detokenizeBatchFutures(executor, batches, interceptor, cfg.batchSize); try { CompletableFuture allFutures = CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])); allFutures.join(); @@ -1005,13 +818,8 @@ private BulkDetokenizeResponse processBulkDetokenizeSync( } for (CompletableFuture future : futures) { BulkDetokenizeResponse futureResponse = future.get(); - if (futureResponse != null) { - if (futureResponse.getSuccess() != null) { - successTokens.addAll(futureResponse.getSuccess()); - } - if (futureResponse.getErrors() != null) { - errorTokens.addAll(futureResponse.getErrors()); - } + if (futureResponse != null && futureResponse.getRecords() != null) { + records.addAll(futureResponse.getRecords()); } } } catch (Exception e) { @@ -1020,7 +828,7 @@ private BulkDetokenizeResponse processBulkDetokenizeSync( } finally { executor.shutdown(); } - BulkDetokenizeResponse response = new BulkDetokenizeResponse(successTokens, errorTokens, originalTokens); + BulkDetokenizeResponse response = new BulkDetokenizeResponse(records, originalTokens); LogUtil.printInfoLog(InfoLogs.DETOKENIZE_REQUEST_RESOLVED.getLog()); return response; } @@ -1028,28 +836,20 @@ private BulkDetokenizeResponse processBulkDetokenizeSync( private List> detokenizeBatchFutures( ExecutorService executor, List batches, - List errorTokens, RequestInterceptor interceptor, int batchSize) { List> futures = new ArrayList<>(); - try { - for (int batchIndex = 0; batchIndex < batches.size(); batchIndex++) { - com.skyflow.generated.rest.resources.flowservice.requests.V1FlowDetokenizeRequest batch = batches.get(batchIndex); - int batchNumber = batchIndex; - RequestContext ctx = new RequestContext("DETOKENIZE"); - if (interceptor != null) interceptor.intercept(ctx); - CompletableFuture future = CompletableFuture - .supplyAsync(() -> processDetokenizeBatch(batch, ctx), executor) - .thenApply(response -> Utils.formatBulkDetokenizeResponse(response.body(), batchNumber, batchSize, response.headers())) - .exceptionally(ex -> { - errorTokens.addAll(Utils.handleBulkDetokenizeBatchException(ex, batch, batchNumber, batchSize)); - return null; - }); - futures.add(future); - } - } catch (Exception e) { - ErrorRecord errorRecord = new ErrorRecord(0, e.getMessage(), 500); - errorTokens.add(errorRecord); + for (int batchIndex = 0; batchIndex < batches.size(); batchIndex++) { + com.skyflow.generated.rest.resources.flowservice.requests.V1FlowDetokenizeRequest batch = batches.get(batchIndex); + int batchNumber = batchIndex; + RequestContext ctx = new RequestContext("DETOKENIZE"); + if (interceptor != null) interceptor.intercept(ctx); + CompletableFuture future = CompletableFuture + .supplyAsync(() -> processDetokenizeBatch(batch, ctx), executor) + .thenApply(response -> Utils.formatBulkDetokenizeResponse(response.body(), batchNumber, batchSize, response.headers())) + .exceptionally(ex -> new BulkDetokenizeResponse( + Utils.handleBulkDetokenizeBatchException(ex, batch, batchNumber, batchSize))); + futures.add(future); } return futures; } @@ -1062,7 +862,6 @@ private ApiClientHttpResponse> insertBatchFutures( V1InsertRequest insertRequest, - List errorRecords, RequestInterceptor interceptor, BatchConfig cfg) { List records = insertRequest.getRecords().get(); @@ -1070,7 +869,6 @@ private List> insertBatchFutures( ExecutorService executor = Executors.newFixedThreadPool(cfg.concurrencyLimit); List> batches = Utils.createBulkInsertBatches(records, cfg.batchSize); List> futures = new ArrayList<>(); - V1Upsert upsert = insertRequest.getUpsert().isPresent() ? insertRequest.getUpsert().get() : null; try { for (int batchIndex = 0; batchIndex < batches.size(); batchIndex++) { @@ -1079,12 +877,10 @@ private List> insertBatchFutures( RequestContext ctx = new RequestContext("INSERT"); if (interceptor != null) interceptor.intercept(ctx); CompletableFuture future = CompletableFuture - .supplyAsync(() -> insertBatch(batch, insertRequest.getTableName().isPresent() ? insertRequest.getTableName().get() : null, upsert, ctx), executor) + .supplyAsync(() -> insertBatch(batch, insertRequest.getTableName().isPresent() ? insertRequest.getTableName().get() : null, ctx), executor) .thenApply(response -> Utils.formatBulkInsertResponse(response.body(), batchNumber, cfg.batchSize, response.headers())) - .exceptionally(ex -> { - errorRecords.addAll(Utils.handleBulkInsertBatchException(ex, batch, batchNumber, cfg.batchSize)); - return null; - }); + .exceptionally(ex -> new BulkInsertResponse( + Utils.handleBulkInsertBatchException(ex, batch, batchNumber, cfg.batchSize))); futures.add(future); } } finally { @@ -1093,11 +889,12 @@ private List> insertBatchFutures( return futures; } - private ApiClientHttpResponse insertBatch(List batch, String tableName, V1Upsert upsert, RequestContext ctx) { + // Upsert is not carried at the envelope level: getInsertRequestBody pushes it down onto every + // record, so each batch already has it. + private ApiClientHttpResponse insertBatch(List batch, String tableName, RequestContext ctx) { V1InsertRequest.Builder req = V1InsertRequest.builder() .vaultId(this.getVaultConfig().getVaultId()) - .records(batch) - .upsert(upsert); + .records(batch); if (tableName != null && !tableName.isEmpty()) { req.tableName(tableName); } diff --git a/flowvault/src/main/java/com/skyflow/vault/data/BulkDetokenizeRequest.java b/flowvault/src/main/java/com/skyflow/vault/data/BulkDetokenizeRequest.java index a7a842b5..dd2c9366 100644 --- a/flowvault/src/main/java/com/skyflow/vault/data/BulkDetokenizeRequest.java +++ b/flowvault/src/main/java/com/skyflow/vault/data/BulkDetokenizeRequest.java @@ -2,40 +2,38 @@ import java.util.List; -public class BulkDetokenizeRequest { - private final BulkDetokenizeRequestBuilder builder; - private BulkDetokenizeRequest(BulkDetokenizeRequestBuilder builder){ - this.builder = builder; +// Bulk counterpart of DetokenizeRequest. Carries no extra state today; all fields +// (tokens, tokenGroupRedactions) are inherited. +public class BulkDetokenizeRequest extends DetokenizeRequest { + + protected BulkDetokenizeRequest(BulkDetokenizeRequestBuilder builder) { + super(builder); } - public static BulkDetokenizeRequestBuilder builder(){ + public static BulkDetokenizeRequestBuilder builder() { return new BulkDetokenizeRequestBuilder(); } - public List getTokens(){ - return this.builder.tokens; - } - public List getTokenGroupRedactions(){ - return this.builder.tokenGroupRedactions; - } - public static final class BulkDetokenizeRequestBuilder{ - private List tokens; + public static final class BulkDetokenizeRequestBuilder extends DetokenizeRequestBuilder { - private List tokenGroupRedactions; + private BulkDetokenizeRequestBuilder() { + } - public BulkDetokenizeRequestBuilder tokens(List tokens){ - this.tokens = tokens; + @Override + public BulkDetokenizeRequestBuilder tokens(List tokens) { + super.tokens(tokens); return this; } - public BulkDetokenizeRequestBuilder tokenGroupRedactions(List tokenGroupRedactions){ - this.tokenGroupRedactions = tokenGroupRedactions; + + @Override + public BulkDetokenizeRequestBuilder tokenGroupRedactions(List tokenGroupRedactions) { + super.tokenGroupRedactions(tokenGroupRedactions); return this; } - public BulkDetokenizeRequest build(){ + + @Override + public BulkDetokenizeRequest build() { return new BulkDetokenizeRequest(this); } - - } - } diff --git a/flowvault/src/main/java/com/skyflow/vault/data/BulkDetokenizeResponse.java b/flowvault/src/main/java/com/skyflow/vault/data/BulkDetokenizeResponse.java index a1ad1ee7..279e1dc6 100644 --- a/flowvault/src/main/java/com/skyflow/vault/data/BulkDetokenizeResponse.java +++ b/flowvault/src/main/java/com/skyflow/vault/data/BulkDetokenizeResponse.java @@ -1,63 +1,59 @@ package com.skyflow.vault.data; import com.google.gson.Gson; -import com.google.gson.GsonBuilder; -import com.google.gson.annotations.Expose; import java.util.ArrayList; import java.util.List; import java.util.stream.Collectors; public class BulkDetokenizeResponse { - @Expose(serialize = true) private DetokenizeSummary summary; + private List records; - @Expose(serialize = true) - private List success; + // Internal fields. transient keeps them out of the toString() output. + private transient List originalPayload; + private transient List tokensToRetry; - @Expose(serialize = true) - private List errors; + public BulkDetokenizeResponse(List records) { + this.records = records; + } - private List originalPayload; - private List tokensToRetry; + public BulkDetokenizeResponse( + List records, + List originalPayload + ) { + this.records = records; + this.originalPayload = originalPayload; + int totalFailed = (int) records.stream().filter(record -> record.getError() != null).count(); + this.summary = new DetokenizeSummary(originalPayload.size(), records.size() - totalFailed, totalFailed); + } - public BulkDetokenizeResponse(List success, List errors) { - this.success = success; - this.errors = errors; + public DetokenizeSummary getSummary() { + return this.summary; } - public BulkDetokenizeResponse(List success, List errors, List originalPayload) { - this.success = success; - this.errors = errors; - this.originalPayload = originalPayload; - this.summary = new DetokenizeSummary(this.originalPayload.size(), this.success.size(), this.errors.size()); + public List getRecords() { + return this.records; } public List getTokensToRetry() { if (tokensToRetry == null) { - tokensToRetry = new ArrayList<>(); - tokensToRetry = errors.stream() - .filter(error -> (error.getCode() >= 500 && error.getCode() <= 599) && error.getCode() != 529) - .map(errorRecord -> originalPayload.get(errorRecord.getIndex())) - .collect(Collectors.toList()); + // Per-batch responses are built without the original payload; nothing to retry from. + if (originalPayload == null) { + return new ArrayList<>(); + } + tokensToRetry = records.stream() + .filter(record -> record.getHttpCode() >= 500 && record.getHttpCode() <= 599 + && record.getHttpCode() != 529) + .map(record -> originalPayload.get(record.getIndex())) + .collect(Collectors.toCollection(ArrayList::new)); } return tokensToRetry; } - public List getSuccess() { - return success; - } - public DetokenizeSummary getSummary() { - return this.summary; - } - - public List getErrors() { - return this.errors; - } @Override public String toString() { - Gson gson = new GsonBuilder().excludeFieldsWithoutExposeAnnotation().create(); + Gson gson = new Gson().newBuilder().serializeNulls().create(); return gson.toJson(this); } - } diff --git a/flowvault/src/main/java/com/skyflow/vault/data/BulkDetokenizeResponseRecord.java b/flowvault/src/main/java/com/skyflow/vault/data/BulkDetokenizeResponseRecord.java new file mode 100644 index 00000000..e38c20d7 --- /dev/null +++ b/flowvault/src/main/java/com/skyflow/vault/data/BulkDetokenizeResponseRecord.java @@ -0,0 +1,34 @@ +package com.skyflow.vault.data; + +import com.google.gson.Gson; + +import java.util.Map; + +// Bulk counterpart of DetokenizeResponseRecord. Adds the caller-facing position of the token +// in the submitted payload; all other fields are inherited. +public class BulkDetokenizeResponseRecord extends DetokenizeResponseRecord { + private final int index; + private final String requestId; + + public BulkDetokenizeResponseRecord(int index, String token, Object value, String tokenGroupName, + Map metadata, int httpCode, String error, + String requestId) { + super(token, value, tokenGroupName, metadata, httpCode, error); + this.index = index; + this.requestId = requestId; + } + + public int getIndex() { + return index; + } + + public String getRequestId() { + return requestId; + } + + @Override + public String toString() { + Gson gson = new Gson().newBuilder().serializeNulls().create(); + return gson.toJson(this); + } +} diff --git a/flowvault/src/main/java/com/skyflow/vault/data/BulkInsertRecord.java b/flowvault/src/main/java/com/skyflow/vault/data/BulkInsertRecord.java deleted file mode 100644 index 3aeae875..00000000 --- a/flowvault/src/main/java/com/skyflow/vault/data/BulkInsertRecord.java +++ /dev/null @@ -1,68 +0,0 @@ -package com.skyflow.vault.data; - -import com.skyflow.enums.UpsertType; - -import java.util.List; -import java.util.Map; - -public class BulkInsertRecord { - private final BulkInsertRecordBuilder builder; - - private BulkInsertRecord(BulkInsertRecordBuilder builder) { - this.builder = builder; - } - - // Getters - public String getTable() { - return this.builder.table; - } - - public Map getData() { - return this.builder.data; - } - - public List getUpsert() { - return this.builder.upsert; - } - - public UpsertType getUpsertType() { - return this.builder.upsertType; - } - - // Builder Class - public static final class BulkInsertRecordBuilder { - private String table; - private Map data; - private List upsert; - private UpsertType upsertType; - - public BulkInsertRecordBuilder table(String table) { - this.table = table; - return this; - } - - public BulkInsertRecordBuilder data(Map data) { - this.data = data; - return this; - } - - public BulkInsertRecordBuilder upsert(List upsert) { - this.upsert = upsert; - return this; - } - - public BulkInsertRecordBuilder upsertType(UpsertType upsertType) { - this.upsertType = upsertType; - return this; - } - - public BulkInsertRecord build() { - return new BulkInsertRecord(this); - } - } - - // Static entry point for builder - public static BulkInsertRecordBuilder builder() { - return new BulkInsertRecordBuilder(); - } -} diff --git a/flowvault/src/main/java/com/skyflow/vault/data/BulkInsertRequest.java b/flowvault/src/main/java/com/skyflow/vault/data/BulkInsertRequest.java index 7eda6f93..82ff45ef 100644 --- a/flowvault/src/main/java/com/skyflow/vault/data/BulkInsertRequest.java +++ b/flowvault/src/main/java/com/skyflow/vault/data/BulkInsertRequest.java @@ -1,67 +1,43 @@ package com.skyflow.vault.data; -import com.skyflow.enums.UpsertType; - -import java.util.ArrayList; import java.util.List; -public class BulkInsertRequest extends BaseInsertRequest { - private final BulkInsertRequestBuilder builder; +// Bulk counterpart of InsertRequest. Carries no extra state today; all fields +// (tableName, records, upsert) are inherited. +public class BulkInsertRequest extends InsertRequest { - private BulkInsertRequest(BulkInsertRequestBuilder builder) { + protected BulkInsertRequest(BulkInsertRequestBuilder builder) { super(builder); - this.builder = builder; } - public static BulkInsertRequestBuilder builder() { return new BulkInsertRequestBuilder(); } - public List getUpsert() { - return this.builder.upsert; - } - - public UpsertType getUpsertType() { - return this.builder.upsertType; - } - - public ArrayList getRecords(){ - return this.builder.records; - } - - public static final class BulkInsertRequestBuilder extends BaseInsertRequestBuilder { - private List upsert; - - private UpsertType upsertType; - - private ArrayList records; + public static final class BulkInsertRequestBuilder extends InsertRequestBuilder { private BulkInsertRequestBuilder() { - super(); } @Override - public BulkInsertRequestBuilder table(String table) { - super.table(table); - return this; - } - - public BulkInsertRequestBuilder upsert(List upsert) { - this.upsert = upsert; + public BulkInsertRequestBuilder tableName(String tableName) { + super.tableName(tableName); return this; } - public BulkInsertRequestBuilder upsertType(UpsertType upsertType) { - this.upsertType = upsertType; + @Override + public BulkInsertRequestBuilder records(List records) { + super.records(records); return this; } - public BulkInsertRequestBuilder records(ArrayList records){ - this.records = records; + @Override + public BulkInsertRequestBuilder upsert(UpsertOptions upsert) { + super.upsert(upsert); return this; } + @Override public BulkInsertRequest build() { return new BulkInsertRequest(this); } diff --git a/flowvault/src/main/java/com/skyflow/vault/data/BulkInsertRequestRecord.java b/flowvault/src/main/java/com/skyflow/vault/data/BulkInsertRequestRecord.java new file mode 100644 index 00000000..6cddda6b --- /dev/null +++ b/flowvault/src/main/java/com/skyflow/vault/data/BulkInsertRequestRecord.java @@ -0,0 +1,51 @@ +package com.skyflow.vault.data; + +import java.util.Map; + +// Bulk counterpart of InsertRequestRecord. Carries no extra state today; it exists so +// bulk-only fields can be added without touching the unary record. +public class BulkInsertRequestRecord extends InsertRequestRecord { + + protected BulkInsertRequestRecord(BulkInsertRequestRecordBuilder builder) { + super(builder); + } + + public static BulkInsertRequestRecordBuilder builder() { + return new BulkInsertRequestRecordBuilder(); + } + + public static final class BulkInsertRequestRecordBuilder extends InsertRequestRecordBuilder { + + private BulkInsertRequestRecordBuilder() { + } + + @Override + public BulkInsertRequestRecordBuilder tableName(String tableName) { + super.tableName(tableName); + return this; + } + + @Override + public BulkInsertRequestRecordBuilder data(Map data) { + super.data(data); + return this; + } + + @Override + public BulkInsertRequestRecordBuilder tokens(Map tokens) { + super.tokens(tokens); + return this; + } + + @Override + public BulkInsertRequestRecordBuilder upsert(UpsertOptions upsert) { + super.upsert(upsert); + return this; + } + + @Override + public BulkInsertRequestRecord build() { + return new BulkInsertRequestRecord(this); + } + } +} diff --git a/flowvault/src/main/java/com/skyflow/vault/data/BulkInsertResponse.java b/flowvault/src/main/java/com/skyflow/vault/data/BulkInsertResponse.java index 72f542d0..b97d2e97 100644 --- a/flowvault/src/main/java/com/skyflow/vault/data/BulkInsertResponse.java +++ b/flowvault/src/main/java/com/skyflow/vault/data/BulkInsertResponse.java @@ -1,60 +1,53 @@ package com.skyflow.vault.data; import com.google.gson.Gson; -import com.google.gson.GsonBuilder; -import com.google.gson.annotations.Expose; import java.util.ArrayList; import java.util.List; import java.util.stream.Collectors; public class BulkInsertResponse { - // These members will be included in the toString() output - @Expose(serialize = true) - private Summary summary; - @Expose(serialize = true) - private List success; - @Expose(serialize = true) - private List errors; + private BulkSummary summary; + private List records; - // Internal fields. Should not be included in toString() output - private ArrayList originalPayload; - private ArrayList recordsToRetry; + // Internal fields. transient keeps them out of the toString() output. + private transient List originalPayload; + private transient List recordsToRetry; - public BulkInsertResponse(List successRecords, List errorRecords) { - this.success = successRecords; - this.errors = errorRecords; + public BulkInsertResponse(List records) { + this.records = records; } public BulkInsertResponse( - List successRecords, - List errorRecords, - ArrayList originalPayload + List records, + List originalPayload ) { - this.success = successRecords; - this.errors = errorRecords; + this.records = records; this.originalPayload = originalPayload; - this.summary = new Summary(this.originalPayload.size(), this.success.size(), this.errors.size()); + int totalFailed = (int) records.stream().filter(record -> record.getError() != null).count(); + this.summary = new BulkSummary(originalPayload.size(), records.size() - totalFailed, totalFailed); } - public Summary getSummary() { + public BulkSummary getSummary() { return this.summary; } - public List getSuccess() { - return this.success; + public List getRecords() { + return this.records; } - public List getErrors() { - return this.errors; - } - - public ArrayList getRecordsToRetry() { + // Records are guaranteed to be BulkInsertRequestRecord: validateBulkInsertRequest rejects + // any other InsertRequestRecord subtype before the request is ever sent. + public List getRecordsToRetry() { if (recordsToRetry == null) { - recordsToRetry = new ArrayList<>(); - recordsToRetry = errors.stream() - .filter(error -> (error.getCode() >= 500 && error.getCode() <= 599) && error.getCode() != 529) - .map(errorRecord -> originalPayload.get(errorRecord.getIndex())) + // Per-batch responses are built without the original payload; nothing to retry from. + if (originalPayload == null) { + return new ArrayList<>(); + } + recordsToRetry = records.stream() + .filter(record -> record.getHttpCode() >= 500 && record.getHttpCode() <= 599 + && record.getHttpCode() != 529) + .map(record -> (BulkInsertRequestRecord) originalPayload.get(record.getIndex())) .collect(Collectors.toCollection(ArrayList::new)); } return recordsToRetry; @@ -62,7 +55,7 @@ public ArrayList getRecordsToRetry() { @Override public String toString() { - Gson gson = new GsonBuilder().excludeFieldsWithoutExposeAnnotation().create(); + Gson gson = new Gson().newBuilder().serializeNulls().create(); return gson.toJson(this); } } diff --git a/flowvault/src/main/java/com/skyflow/vault/data/BulkInsertResponseRecord.java b/flowvault/src/main/java/com/skyflow/vault/data/BulkInsertResponseRecord.java new file mode 100644 index 00000000..1ea6b757 --- /dev/null +++ b/flowvault/src/main/java/com/skyflow/vault/data/BulkInsertResponseRecord.java @@ -0,0 +1,34 @@ +package com.skyflow.vault.data; + +import com.google.gson.Gson; + +import java.util.Map; + +// Bulk counterpart of InsertResponseRecord. Adds the caller-facing position of the record +// in the submitted payload; all other fields are inherited. +public class BulkInsertResponseRecord extends InsertResponseRecord { + private final int index; + private final String requestId; + + public BulkInsertResponseRecord(int index, String tableName, String skyflowId, + Map fields, Map hashedData, + int httpCode, String error, String requestId) { + super(tableName, skyflowId, fields, hashedData, httpCode, error); + this.index = index; + this.requestId = requestId; + } + + public int getIndex() { + return index; + } + + public String getRequestId(){ + return requestId; + } + + @Override + public String toString() { + Gson gson = new Gson().newBuilder().serializeNulls().create(); + return gson.toJson(this); + } +} diff --git a/flowvault/src/main/java/com/skyflow/vault/data/Summary.java b/flowvault/src/main/java/com/skyflow/vault/data/BulkSummary.java similarity index 85% rename from flowvault/src/main/java/com/skyflow/vault/data/Summary.java rename to flowvault/src/main/java/com/skyflow/vault/data/BulkSummary.java index fb152fb2..f32a14ee 100644 --- a/flowvault/src/main/java/com/skyflow/vault/data/Summary.java +++ b/flowvault/src/main/java/com/skyflow/vault/data/BulkSummary.java @@ -3,7 +3,7 @@ import com.google.gson.Gson; import com.google.gson.annotations.Expose; -public class Summary { +public class BulkSummary { @Expose(serialize = true) private int totalRecords; @Expose(serialize = true) @@ -11,10 +11,10 @@ public class Summary { @Expose(serialize = true) private int totalFailed; - public Summary() { + public BulkSummary() { } - public Summary(int totalRecords, int totalInserted, int totalFailed) { + public BulkSummary(int totalRecords, int totalInserted, int totalFailed) { this.totalRecords = totalRecords; this.totalInserted = totalInserted; this.totalFailed = totalFailed; @@ -24,7 +24,6 @@ public int getTotalRecords() { return totalRecords; } - public int getTotalInserted() { return totalInserted; } @@ -33,7 +32,6 @@ public int getTotalFailed() { return totalFailed; } - @Override public String toString() { Gson gson = new Gson(); diff --git a/flowvault/src/main/java/com/skyflow/vault/data/BulkTokenGroupRedactions.java b/flowvault/src/main/java/com/skyflow/vault/data/BulkTokenGroupRedactions.java deleted file mode 100644 index da894232..00000000 --- a/flowvault/src/main/java/com/skyflow/vault/data/BulkTokenGroupRedactions.java +++ /dev/null @@ -1,39 +0,0 @@ -package com.skyflow.vault.data; - -public class BulkTokenGroupRedactions { - private final BulkTokenGroupRedactionsBuilder builder; - - private BulkTokenGroupRedactions(BulkTokenGroupRedactionsBuilder builder) { - this.builder = builder; - } - public String getTokenGroupName() { - return this.builder.tokenGroupName; - } - - public String getRedaction() { - return this.builder.redaction; - } - - public static BulkTokenGroupRedactionsBuilder builder() { - return new BulkTokenGroupRedactionsBuilder(); - } - - public static final class BulkTokenGroupRedactionsBuilder { - private String tokenGroupName; - private String redaction; - - public BulkTokenGroupRedactionsBuilder tokenGroupName(String tokenGroupName) { - this.tokenGroupName = tokenGroupName; - return this; - } - - public BulkTokenGroupRedactionsBuilder redaction(String redaction) { - this.redaction = redaction; - return this; - } - - public BulkTokenGroupRedactions build() { - return new BulkTokenGroupRedactions(this); - } - } -} diff --git a/flowvault/src/main/java/com/skyflow/vault/data/ColumnRedaction.java b/flowvault/src/main/java/com/skyflow/vault/data/ColumnRedaction.java deleted file mode 100644 index 2325c0ac..00000000 --- a/flowvault/src/main/java/com/skyflow/vault/data/ColumnRedaction.java +++ /dev/null @@ -1,40 +0,0 @@ -package com.skyflow.vault.data; - -public class ColumnRedaction { - private final ColumnRedactionBuilder builder; - - private ColumnRedaction(ColumnRedactionBuilder builder) { - this.builder = builder; - } - - public String getColumnName() { - return this.builder.columnName; - } - - public String getRedaction() { - return this.builder.redaction; - } - - public static ColumnRedactionBuilder builder() { - return new ColumnRedactionBuilder(); - } - - public static final class ColumnRedactionBuilder { - private String columnName; - private String redaction; - - public ColumnRedactionBuilder columnName(String columnName) { - this.columnName = columnName; - return this; - } - - public ColumnRedactionBuilder redaction(String redaction) { - this.redaction = redaction; - return this; - } - - public ColumnRedaction build() { - return new ColumnRedaction(this); - } - } -} diff --git a/flowvault/src/main/java/com/skyflow/vault/data/DeleteTokensRequest.java b/flowvault/src/main/java/com/skyflow/vault/data/DeleteTokensRequest.java deleted file mode 100644 index ea8e9a7a..00000000 --- a/flowvault/src/main/java/com/skyflow/vault/data/DeleteTokensRequest.java +++ /dev/null @@ -1,34 +0,0 @@ -package com.skyflow.vault.data; - -import java.util.List; - -public class DeleteTokensRequest { - private final DeleteTokensRequestBuilder builder; - - private DeleteTokensRequest(DeleteTokensRequestBuilder builder) { - this.builder = builder; - } - - public static DeleteTokensRequestBuilder builder() { - return new DeleteTokensRequestBuilder(); - } - - public List getTokens() { - return this.builder.tokens; - } - - public static final class DeleteTokensRequestBuilder { - private List tokens; - - private DeleteTokensRequestBuilder() {} - - public DeleteTokensRequestBuilder tokens(List tokens) { - this.tokens = tokens; - return this; - } - - public DeleteTokensRequest build() { - return new DeleteTokensRequest(this); - } - } -} diff --git a/flowvault/src/main/java/com/skyflow/vault/data/DeleteTokensResponse.java b/flowvault/src/main/java/com/skyflow/vault/data/DeleteTokensResponse.java deleted file mode 100644 index 7f621d8d..00000000 --- a/flowvault/src/main/java/com/skyflow/vault/data/DeleteTokensResponse.java +++ /dev/null @@ -1,22 +0,0 @@ -package com.skyflow.vault.data; - -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; - -public class DeleteTokensResponse { - private final List tokens; - private final ArrayList> errors; - - public DeleteTokensResponse(List tokens, ArrayList> errors) { - this.tokens = tokens; - this.errors = errors; - } - - public List getTokens() { - return tokens; - } - public ArrayList> getErrors(){ - return this.errors; - } -} diff --git a/flowvault/src/main/java/com/skyflow/vault/data/DetokenizeData.java b/flowvault/src/main/java/com/skyflow/vault/data/DetokenizeData.java deleted file mode 100644 index dcd8a2c8..00000000 --- a/flowvault/src/main/java/com/skyflow/vault/data/DetokenizeData.java +++ /dev/null @@ -1,8 +0,0 @@ -package com.skyflow.vault.data; - - -public class DetokenizeData extends BaseDetokenizeData { - public DetokenizeData(String token) { - super(token); - } -} diff --git a/flowvault/src/main/java/com/skyflow/vault/data/DetokenizeRecordResponse.java b/flowvault/src/main/java/com/skyflow/vault/data/DetokenizeRecordResponse.java deleted file mode 100644 index 27063e8e..00000000 --- a/flowvault/src/main/java/com/skyflow/vault/data/DetokenizeRecordResponse.java +++ /dev/null @@ -1,37 +0,0 @@ -package com.skyflow.vault.data; - - -import com.google.gson.Gson; - -import java.util.Map; - -public class DetokenizeRecordResponse extends BaseDetokenizeRecordResponse { - private String tokenGroupName; - private Map metadata; - private Object value; - - public DetokenizeRecordResponse(String token, Object value, String error, String tokenGroupName, Map metadata) { - super(token, error); - this.value = value; - this.tokenGroupName = tokenGroupName; - this.metadata = metadata; - - } - - public String getTokenGroupName() { - return tokenGroupName; - } - public Map getMetadata() { - return metadata; - } - - public Object getValue(){ - return this.value; - } - - @Override - public String toString() { - Gson gson = new Gson(); - return gson.toJson(this); - } -} \ No newline at end of file diff --git a/flowvault/src/main/java/com/skyflow/vault/data/DetokenizeRequest.java b/flowvault/src/main/java/com/skyflow/vault/data/DetokenizeRequest.java index 67be6140..e833450e 100644 --- a/flowvault/src/main/java/com/skyflow/vault/data/DetokenizeRequest.java +++ b/flowvault/src/main/java/com/skyflow/vault/data/DetokenizeRequest.java @@ -1,12 +1,11 @@ package com.skyflow.vault.data; -import java.util.ArrayList; import java.util.List; -public class DetokenizeRequest extends BaseDetokenizeRequest{ +public class DetokenizeRequest extends BaseDetokenizeRequest { private final DetokenizeRequestBuilder builder; - private DetokenizeRequest(DetokenizeRequestBuilder builder) { + protected DetokenizeRequest(DetokenizeRequestBuilder builder) { this.builder = builder; } @@ -14,19 +13,20 @@ public static DetokenizeRequestBuilder builder() { return new DetokenizeRequestBuilder(); } - public ArrayList getDetokenizeData() { - return this.builder.detokenizeData; + public List getTokens() { + return this.builder.tokens; } + public List getTokenGroupRedactions(){ return this.builder.tokenGroupRedactions; } - public static final class DetokenizeRequestBuilder { - private ArrayList detokenizeData; + public static class DetokenizeRequestBuilder { + private List tokens; private List tokenGroupRedactions; - public DetokenizeRequestBuilder detokenizeData(ArrayList detokenizeData) { - this.detokenizeData = detokenizeData; + public DetokenizeRequestBuilder tokens(List tokens) { + this.tokens = tokens; return this; } public DetokenizeRequestBuilder tokenGroupRedactions(List tokenGroupRedactions){ diff --git a/flowvault/src/main/java/com/skyflow/vault/data/DetokenizeResponse.java b/flowvault/src/main/java/com/skyflow/vault/data/DetokenizeResponse.java index 226b8444..b716abca 100644 --- a/flowvault/src/main/java/com/skyflow/vault/data/DetokenizeResponse.java +++ b/flowvault/src/main/java/com/skyflow/vault/data/DetokenizeResponse.java @@ -2,23 +2,19 @@ import com.google.gson.Gson; -import java.util.ArrayList; +import java.util.List; -public class DetokenizeResponse { - private final ArrayList detokenizedFields; - private final ArrayList errors; +// Response shape for the unary detokenize contract. Retained as published API even though the +// module currently exposes only the bulk operations. +public class DetokenizeResponse extends BaseDetokenizeResponse { + private final List records; - public DetokenizeResponse(ArrayList detokenizedFields, ArrayList errors) { - this.detokenizedFields = detokenizedFields; - this.errors = errors; + public DetokenizeResponse(List records) { + this.records = records; } - public ArrayList getDetokenizedFields() { - return detokenizedFields; - } - - public ArrayList getErrors() { - return errors; + public List getRecords() { + return records; } @Override @@ -26,4 +22,4 @@ public String toString() { Gson gson = new Gson(); return gson.toJson(this); } -} \ No newline at end of file +} diff --git a/flowvault/src/main/java/com/skyflow/vault/data/DetokenizeResponseObject.java b/flowvault/src/main/java/com/skyflow/vault/data/DetokenizeResponseObject.java deleted file mode 100644 index 0e1c57f6..00000000 --- a/flowvault/src/main/java/com/skyflow/vault/data/DetokenizeResponseObject.java +++ /dev/null @@ -1,63 +0,0 @@ -package com.skyflow.vault.data; - -import com.google.gson.Gson; -import com.google.gson.annotations.Expose; - -import java.util.Map; - -public class DetokenizeResponseObject { - @Expose(serialize = true) - private int index; - - @Expose(serialize = true) - private String token; - @Expose(serialize = true) - private Object value; - @Expose(serialize = true) - private String tokenGroupName; - @Expose(serialize = true) - private String error; - - @Expose(serialize = true) - private Map metadata; - - public DetokenizeResponseObject(int index, String token, Object value, String tokenGroupName, String error, Map metadata) { - this.token = token; - this.value = value; - this.tokenGroupName = tokenGroupName; - this.error = error; - this.metadata = metadata; - this.index = index; - } - - public String getToken() { - return token; - } - - public Object getValue() { - return value; - } - - public String getTokenGroupName() { - return tokenGroupName; - } - - public String getError() { - return error; - } - - public int getIndex() { - return index; - } - - public Map getMetadata() { - return metadata; - } - - @Override - public String toString() { - Gson gson = new Gson(); - return gson.toJson(this); - } - -} diff --git a/flowvault/src/main/java/com/skyflow/vault/data/DetokenizeResponseRecord.java b/flowvault/src/main/java/com/skyflow/vault/data/DetokenizeResponseRecord.java new file mode 100644 index 00000000..e0aa8278 --- /dev/null +++ b/flowvault/src/main/java/com/skyflow/vault/data/DetokenizeResponseRecord.java @@ -0,0 +1,36 @@ +package com.skyflow.vault.data; + +import java.util.Map; + +public class DetokenizeResponseRecord extends BaseDetokenizeRecordResponse { + // Passed straight through from V1FlowDetokenizeResponseObject.getValue() (Optional). + private final Object value; + private final String tokenGroupName; + private final Map metadata; + private final int httpCode; + + public DetokenizeResponseRecord(String token, Object value, String tokenGroupName, + Map metadata, int httpCode, String error) { + super(token, error); + this.value = value; + this.tokenGroupName = tokenGroupName; + this.metadata = metadata; + this.httpCode = httpCode; + } + + public Object getValue() { + return value; + } + + public String getTokenGroupName() { + return tokenGroupName; + } + + public Map getMetadata() { + return metadata; + } + + public int getHttpCode() { + return httpCode; + } +} diff --git a/flowvault/src/main/java/com/skyflow/vault/data/GetOptions.java b/flowvault/src/main/java/com/skyflow/vault/data/GetOptions.java deleted file mode 100644 index b4dbc096..00000000 --- a/flowvault/src/main/java/com/skyflow/vault/data/GetOptions.java +++ /dev/null @@ -1,30 +0,0 @@ -package com.skyflow.vault.data; - -public final class GetOptions { - private final RequestInterceptor interceptor; - - private GetOptions(Builder builder) { - this.interceptor = builder.interceptor; - } - - public RequestInterceptor getInterceptor() { - return interceptor; - } - - public static Builder builder() { - return new Builder(); - } - - public static final class Builder { - private RequestInterceptor interceptor; - - public Builder interceptor(RequestInterceptor interceptor) { - this.interceptor = interceptor; - return this; - } - - public GetOptions build() { - return new GetOptions(this); - } - } -} diff --git a/flowvault/src/main/java/com/skyflow/vault/data/GetRecordRequest.java b/flowvault/src/main/java/com/skyflow/vault/data/GetRecordRequest.java deleted file mode 100644 index d2f4c035..00000000 --- a/flowvault/src/main/java/com/skyflow/vault/data/GetRecordRequest.java +++ /dev/null @@ -1,71 +0,0 @@ -package com.skyflow.vault.data; - -import java.util.ArrayList; -import java.util.List; -import java.util.Map; - -/** - * A single table's lookup within a multi-table {@link GetRequest#getRecords()} batch. - * Mirrors the shape of a single-table {@link GetRequest}, minus the request-wide - * {@code limit}/{@code offset}. - */ -public class GetRecordRequest extends BaseGetRequest { - private final GetRecordRequestBuilder builder; - - private GetRecordRequest(GetRecordRequestBuilder builder) { - super(builder); - this.builder = builder; - } - - public static GetRecordRequestBuilder builder() { - return new GetRecordRequestBuilder(); - } - - public List getColumnRedactions() { - return this.builder.columnRedactions; - } - - public List> getUniqueValues() { - return this.builder.uniqueValues; - } - - public static final class GetRecordRequestBuilder extends BaseGetRequestBuilder { - private List columnRedactions; - private List> uniqueValues; - - private GetRecordRequestBuilder() { - } - - @Override - public GetRecordRequestBuilder table(String table) { - super.table(table); - return this; - } - - @Override - public GetRecordRequestBuilder ids(ArrayList ids) { - super.ids(ids); - return this; - } - - @Override - public GetRecordRequestBuilder fields(ArrayList fields) { - super.fields(fields); - return this; - } - - public GetRecordRequestBuilder columnRedactions(List columnRedactions) { - this.columnRedactions = columnRedactions; - return this; - } - - public GetRecordRequestBuilder uniqueValues(List> uniqueValues) { - this.uniqueValues = uniqueValues; - return this; - } - - public GetRecordRequest build() { - return new GetRecordRequest(this); - } - } -} diff --git a/flowvault/src/main/java/com/skyflow/vault/data/GetRequest.java b/flowvault/src/main/java/com/skyflow/vault/data/GetRequest.java deleted file mode 100644 index c4e05c1f..00000000 --- a/flowvault/src/main/java/com/skyflow/vault/data/GetRequest.java +++ /dev/null @@ -1,102 +0,0 @@ -package com.skyflow.vault.data; - -import java.util.ArrayList; -import java.util.List; -import java.util.Map; - -public class GetRequest extends BaseGetRequest { - private final GetRequestBuilder builder; - - private GetRequest(GetRequestBuilder builder) { - super(builder); - this.builder = builder; - } - - public static GetRequestBuilder builder() { - return new GetRequestBuilder(); - } - - public List getColumnRedactions() { - return this.builder.columnRedactions; - } - - public List> getUniqueValues() { - return this.builder.uniqueValues; - } - - public Integer getLimit() { - return this.builder.limit; - } - - public Integer getOffset() { - return this.builder.offset; - } - - /** - * A multi-table lookup batch — when set, this request fetches from each listed - * table in a single call instead of the single-table fields ({@code table}, {@code ids}, - * {@code fields}, {@code uniqueValues}, {@code columnRedactions}) above. The two modes - * are mutually exclusive. - */ - public List getRecords() { - return this.builder.records; - } - - public static final class GetRequestBuilder extends BaseGetRequestBuilder { - private List columnRedactions; - private List> uniqueValues; - private Integer limit; - private Integer offset; - private List records; - - private GetRequestBuilder() { - } - - @Override - public GetRequestBuilder table(String table) { - super.table(table); - return this; - } - - @Override - public GetRequestBuilder ids(ArrayList ids) { - super.ids(ids); - return this; - } - - @Override - public GetRequestBuilder fields(ArrayList fields) { - super.fields(fields); - return this; - } - - public GetRequestBuilder columnRedactions(List columnRedactions) { - this.columnRedactions = columnRedactions; - return this; - } - - public GetRequestBuilder uniqueValues(List> uniqueValues) { - this.uniqueValues = uniqueValues; - return this; - } - - public GetRequestBuilder limit(Integer limit) { - this.limit = limit; - return this; - } - - public GetRequestBuilder offset(Integer offset) { - this.offset = offset; - return this; - } - - public GetRequestBuilder records(List records) { - this.records = records; - return this; - } - - public GetRequest build() { - return new GetRequest(this); - } - } -} diff --git a/flowvault/src/main/java/com/skyflow/vault/data/GetResponse.java b/flowvault/src/main/java/com/skyflow/vault/data/GetResponse.java deleted file mode 100644 index 4267dba3..00000000 --- a/flowvault/src/main/java/com/skyflow/vault/data/GetResponse.java +++ /dev/null @@ -1,10 +0,0 @@ -package com.skyflow.vault.data; - -import java.util.ArrayList; -import java.util.HashMap; - -public class GetResponse extends BaseGetResponse { - public GetResponse(ArrayList> data, ArrayList> errors) { - super(data, errors); - } -} diff --git a/flowvault/src/main/java/com/skyflow/vault/data/InsertRecord.java b/flowvault/src/main/java/com/skyflow/vault/data/InsertRecord.java deleted file mode 100644 index e9cb5c08..00000000 --- a/flowvault/src/main/java/com/skyflow/vault/data/InsertRecord.java +++ /dev/null @@ -1,68 +0,0 @@ -package com.skyflow.vault.data; - -import com.skyflow.enums.UpsertType; - -import java.util.List; -import java.util.Map; - -public class InsertRecord { - private final InsertRecordBuilder builder; - - private InsertRecord(InsertRecordBuilder builder) { - this.builder = builder; - } - - // Getters - public String getTable() { - return this.builder.table; - } - - public Map getData() { - return this.builder.data; - } - - public List getUpsert() { - return this.builder.upsert; - } - - public UpsertType getUpsertType() { - return this.builder.upsertType; - } - - // Builder Class - public static final class InsertRecordBuilder{ - private String table; - private Map data; - private List upsert; - private UpsertType upsertType; - - public InsertRecordBuilder table(String table) { - this.table = table; - return this; - } - - public InsertRecordBuilder data(Map data) { - this.data = data; - return this; - } - - public InsertRecordBuilder upsert(List upsert) { - this.upsert = upsert; - return this; - } - - public InsertRecordBuilder upsertType(UpsertType upsertType) { - this.upsertType = upsertType; - return this; - } - - public InsertRecord build() { - return new InsertRecord(this); - } - } - - // Static entry point for builder - public static InsertRecordBuilder builder() { - return new InsertRecordBuilder(); - } -} diff --git a/flowvault/src/main/java/com/skyflow/vault/data/InsertRequest.java b/flowvault/src/main/java/com/skyflow/vault/data/InsertRequest.java index b4ef6b17..4a2bcb5d 100644 --- a/flowvault/src/main/java/com/skyflow/vault/data/InsertRequest.java +++ b/flowvault/src/main/java/com/skyflow/vault/data/InsertRequest.java @@ -1,14 +1,11 @@ package com.skyflow.vault.data; -import com.skyflow.enums.UpsertType; -import java.util.ArrayList; import java.util.List; public class InsertRequest extends BaseInsertRequest { private final InsertRequestBuilder builder; - private InsertRequest(InsertRequestBuilder builder) { - super(builder); + protected InsertRequest(InsertRequestBuilder builder) { this.builder = builder; } @@ -16,47 +13,38 @@ public static InsertRequestBuilder builder() { return new InsertRequestBuilder(); } - public List getUpsert() { - return this.builder.upsert; - } - - public UpsertType getUpsertType() { - return this.builder.upsertType; + public String getTableName() { + return this.builder.tableName; } - public ArrayList getRecords(){ + public List getRecords() { return this.builder.records; } - public static final class InsertRequestBuilder extends BaseInsertRequestBuilder { - private List upsert; - - private UpsertType upsertType; + public UpsertOptions getUpsert() { + return this.builder.upsert; + } - private ArrayList records; + public static class InsertRequestBuilder { + private String tableName; + private List records; + private UpsertOptions upsert; - private InsertRequestBuilder() { - super(); + protected InsertRequestBuilder() { } - @Override - public InsertRequestBuilder table(String table) { - super.table(table); + public InsertRequestBuilder tableName(String tableName) { + this.tableName = tableName; return this; } - public InsertRequestBuilder upsert(List upsert) { - this.upsert = upsert; - return this; - } - - public InsertRequestBuilder upsertType(UpsertType upsertType) { - this.upsertType = upsertType; + public InsertRequestBuilder records(List records) { + this.records = records; return this; } - public InsertRequestBuilder records(ArrayList records){ - this.records = records; + public InsertRequestBuilder upsert(UpsertOptions upsert) { + this.upsert = upsert; return this; } diff --git a/flowvault/src/main/java/com/skyflow/vault/data/InsertRequestRecord.java b/flowvault/src/main/java/com/skyflow/vault/data/InsertRequestRecord.java new file mode 100644 index 00000000..75747a84 --- /dev/null +++ b/flowvault/src/main/java/com/skyflow/vault/data/InsertRequestRecord.java @@ -0,0 +1,65 @@ +package com.skyflow.vault.data; + +import java.util.Map; + +public class InsertRequestRecord { + private final InsertRequestRecordBuilder builder; + + protected InsertRequestRecord(InsertRequestRecordBuilder builder) { + this.builder = builder; + } + + // Getters + public String getTableName() { + return this.builder.tableName; + } + + public Map getData() { + return this.builder.data; + } + + public Map getTokens() { + return this.builder.tokens; + } + + public UpsertOptions getUpsert() { + return this.builder.upsert; + } + + // Builder Class + public static class InsertRequestRecordBuilder { + private String tableName; + private Map data; + private Map tokens; + private UpsertOptions upsert; + + public InsertRequestRecordBuilder tableName(String tableName) { + this.tableName = tableName; + return this; + } + + public InsertRequestRecordBuilder data(Map data) { + this.data = data; + return this; + } + + public InsertRequestRecordBuilder tokens(Map tokens) { + this.tokens = tokens; + return this; + } + + public InsertRequestRecordBuilder upsert(UpsertOptions upsert) { + this.upsert = upsert; + return this; + } + + public InsertRequestRecord build() { + return new InsertRequestRecord(this); + } + } + + // Static entry point for builder + public static InsertRequestRecordBuilder builder() { + return new InsertRequestRecordBuilder(); + } +} diff --git a/flowvault/src/main/java/com/skyflow/vault/data/InsertResponse.java b/flowvault/src/main/java/com/skyflow/vault/data/InsertResponse.java index 5a969d8d..594e5fd1 100644 --- a/flowvault/src/main/java/com/skyflow/vault/data/InsertResponse.java +++ b/flowvault/src/main/java/com/skyflow/vault/data/InsertResponse.java @@ -1,10 +1,25 @@ package com.skyflow.vault.data; -import java.util.ArrayList; -import java.util.HashMap; +import com.google.gson.Gson; -public class InsertResponse extends BaseInsertResponse{ - public InsertResponse(ArrayList> insertedFields, ArrayList> errors) { - super(insertedFields, errors); +import java.util.List; + +// Response shape for the unary insert contract. Retained as published API even though the +// module currently exposes only the bulk operations. +public class InsertResponse extends BaseInsertResponse { + private final List records; + + public InsertResponse(List records) { + this.records = records; + } + + public List getRecords() { + return records; + } + + @Override + public String toString() { + Gson gson = new Gson().newBuilder().serializeNulls().create(); + return gson.toJson(this); } } diff --git a/flowvault/src/main/java/com/skyflow/vault/data/InsertResponseRecord.java b/flowvault/src/main/java/com/skyflow/vault/data/InsertResponseRecord.java new file mode 100644 index 00000000..442d3749 --- /dev/null +++ b/flowvault/src/main/java/com/skyflow/vault/data/InsertResponseRecord.java @@ -0,0 +1,46 @@ +package com.skyflow.vault.data; + +import java.util.Map; + +public class InsertResponseRecord { + private final String tableName; + private final String skyflowId; + private final Map fields; + private final Map hashedData; + private final int httpCode; + private final String error; + + public InsertResponseRecord(String tableName, String skyflowId, Map fields, + Map hashedData, int httpCode, String error) { + this.tableName = tableName; + this.skyflowId = skyflowId; + this.fields = fields; + this.hashedData = hashedData; + this.httpCode = httpCode; + this.error = error; + } + + public String getTableName() { + return tableName; + } + + public String getSkyflowId() { + return skyflowId; + } + + public Map getFields() { + return fields; + } + + public Map getHashedData() { + return hashedData; + } + + public int getHttpCode() { + return httpCode; + } + + public String getError() { + return error; + } +} diff --git a/flowvault/src/main/java/com/skyflow/vault/data/QueryOptions.java b/flowvault/src/main/java/com/skyflow/vault/data/QueryOptions.java deleted file mode 100644 index 86e5f725..00000000 --- a/flowvault/src/main/java/com/skyflow/vault/data/QueryOptions.java +++ /dev/null @@ -1,30 +0,0 @@ -package com.skyflow.vault.data; - -public final class QueryOptions { - private final RequestInterceptor interceptor; - - private QueryOptions(Builder builder) { - this.interceptor = builder.interceptor; - } - - public RequestInterceptor getInterceptor() { - return interceptor; - } - - public static Builder builder() { - return new Builder(); - } - - public static final class Builder { - private RequestInterceptor interceptor; - - public Builder interceptor(RequestInterceptor interceptor) { - this.interceptor = interceptor; - return this; - } - - public QueryOptions build() { - return new QueryOptions(this); - } - } -} diff --git a/flowvault/src/main/java/com/skyflow/vault/data/QueryRequest.java b/flowvault/src/main/java/com/skyflow/vault/data/QueryRequest.java deleted file mode 100644 index ea27f5d1..00000000 --- a/flowvault/src/main/java/com/skyflow/vault/data/QueryRequest.java +++ /dev/null @@ -1,26 +0,0 @@ -package com.skyflow.vault.data; - -public class QueryRequest extends BaseQueryRequest { - private QueryRequest(QueryRequestBuilder builder) { - super(builder); - } - - public static QueryRequestBuilder builder() { - return new QueryRequestBuilder(); - } - - public static final class QueryRequestBuilder extends BaseQueryRequestBuilder { - private QueryRequestBuilder() { - } - - @Override - public QueryRequestBuilder query(String query) { - super.query(query); - return this; - } - - public QueryRequest build() { - return new QueryRequest(this); - } - } -} diff --git a/flowvault/src/main/java/com/skyflow/vault/data/QueryResponse.java b/flowvault/src/main/java/com/skyflow/vault/data/QueryResponse.java deleted file mode 100644 index 2e242d1a..00000000 --- a/flowvault/src/main/java/com/skyflow/vault/data/QueryResponse.java +++ /dev/null @@ -1,10 +0,0 @@ -package com.skyflow.vault.data; - -import java.util.ArrayList; -import java.util.HashMap; - -public class QueryResponse extends BaseQueryResponse { - public QueryResponse(ArrayList> fields) { - super(fields); - } -} diff --git a/flowvault/src/main/java/com/skyflow/vault/data/Success.java b/flowvault/src/main/java/com/skyflow/vault/data/Success.java deleted file mode 100644 index 59cdf722..00000000 --- a/flowvault/src/main/java/com/skyflow/vault/data/Success.java +++ /dev/null @@ -1,53 +0,0 @@ -package com.skyflow.vault.data; -import com.google.gson.Gson; -import com.google.gson.annotations.Expose; - -import java.util.List; -import java.util.Map; - -public class Success { - @Expose(serialize = true) - private int index; - @Expose(serialize = true) - private String skyflow_id; - @Expose(serialize = true) - private Map> tokens; - @Expose(serialize = true) - private Map data; - @Expose(serialize = true) - private String table; - - public int getIndex() { - return index; - } - - public Success(int index, String skyflow_id, Map> tokens, Map data, String table) { - this.index = index; - this.skyflow_id = skyflow_id; - this.tokens = tokens; - this.data = data; - this.table = table; - } - - public String getSkyflowId() { - return this.skyflow_id; - } - - public Map> getTokens() { - return this.tokens; - } - - public Map getData() { - return this.data; - } - - public String getTable(){ - return this.table; - } - - @Override - public String toString() { - Gson gson = new Gson(); - return gson.toJson(this); - } -} diff --git a/flowvault/src/main/java/com/skyflow/vault/data/Token.java b/flowvault/src/main/java/com/skyflow/vault/data/Token.java deleted file mode 100644 index c50242bb..00000000 --- a/flowvault/src/main/java/com/skyflow/vault/data/Token.java +++ /dev/null @@ -1,23 +0,0 @@ -package com.skyflow.vault.data; - -import com.google.gson.annotations.Expose; - -public class Token { - @Expose(serialize = true) - private String token; - @Expose(serialize = true) - private String tokenGroupName; - - public String getToken() { - return token; - } - - public String getTokenGroupName() { - return tokenGroupName; - } - - public Token(String token, String tokenGroupName) { - this.token = token; - this.tokenGroupName = tokenGroupName; - } -} diff --git a/flowvault/src/main/java/com/skyflow/vault/data/TokenGroupRedactions.java b/flowvault/src/main/java/com/skyflow/vault/data/TokenGroupRedactions.java index 3f89014c..abeabdbe 100644 --- a/flowvault/src/main/java/com/skyflow/vault/data/TokenGroupRedactions.java +++ b/flowvault/src/main/java/com/skyflow/vault/data/TokenGroupRedactions.java @@ -1,6 +1,6 @@ package com.skyflow.vault.data; -public class TokenGroupRedactions { +public class TokenGroupRedactions extends BaseDetokenizeData { private final TokenGroupRedactionsBuilder builder; private TokenGroupRedactions(TokenGroupRedactionsBuilder builder) { diff --git a/flowvault/src/main/java/com/skyflow/vault/data/TokenizeData.java b/flowvault/src/main/java/com/skyflow/vault/data/TokenizeData.java deleted file mode 100644 index 6f04ed8d..00000000 --- a/flowvault/src/main/java/com/skyflow/vault/data/TokenizeData.java +++ /dev/null @@ -1,36 +0,0 @@ -package com.skyflow.vault.data; - -import com.google.gson.Gson; -import com.google.gson.GsonBuilder; - -import java.util.LinkedHashMap; -import java.util.Map; - -public class TokenizeData { - private Object value; - private Map tokens; - private int index; - - public TokenizeData(Object value, int index){ - this.value = value; - this.tokens = new LinkedHashMap<>(); - this.index = index; - } - public void addToken(String tokenGroupName, String token) { - this.tokens.put(tokenGroupName, token); - } - - public Object getValue() { - return value; - } - - public Map getTokens() { - return tokens; - } - - @Override - public String toString() { - Gson gson = new GsonBuilder().create(); - return gson.toJson(this); - } -} diff --git a/flowvault/src/main/java/com/skyflow/vault/data/TokenizeRecord.java b/flowvault/src/main/java/com/skyflow/vault/data/TokenizeRecord.java deleted file mode 100644 index faa676df..00000000 --- a/flowvault/src/main/java/com/skyflow/vault/data/TokenizeRecord.java +++ /dev/null @@ -1,44 +0,0 @@ -package com.skyflow.vault.data; - -import java.util.List; - -public class TokenizeRecord { - private final TokenizeRecordBuilder builder; - - private TokenizeRecord(TokenizeRecordBuilder builder) { - this.builder = builder; - } - - public static TokenizeRecordBuilder builder() { - return new TokenizeRecordBuilder(); - } - - public Object getValue() { - return this.builder.value; - } - - public List getTokenGroupNames() { - return this.builder.tokenGroupNames; - } - - public static final class TokenizeRecordBuilder { - private Object value; - private List tokenGroupNames; - - private TokenizeRecordBuilder() {} - - public TokenizeRecordBuilder value(Object value) { - this.value = value; - return this; - } - - public TokenizeRecordBuilder tokenGroupNames(List tokenGroupNames) { - this.tokenGroupNames = tokenGroupNames; - return this; - } - - public TokenizeRecord build() { - return new TokenizeRecord(this); - } - } -} diff --git a/flowvault/src/main/java/com/skyflow/vault/data/TokenizeRequest.java b/flowvault/src/main/java/com/skyflow/vault/data/TokenizeRequest.java deleted file mode 100644 index a6ef8072..00000000 --- a/flowvault/src/main/java/com/skyflow/vault/data/TokenizeRequest.java +++ /dev/null @@ -1,34 +0,0 @@ -package com.skyflow.vault.data; - -import java.util.ArrayList; - -public class TokenizeRequest { - private final TokenizeRequestBuilder builder; - - private TokenizeRequest(TokenizeRequestBuilder builder) { - this.builder = builder; - } - - public static TokenizeRequestBuilder builder() { - return new TokenizeRequestBuilder(); - } - - public ArrayList getData() { - return this.builder.data; - } - - public static final class TokenizeRequestBuilder { - private ArrayList data; - - private TokenizeRequestBuilder() {} - - public TokenizeRequestBuilder data(ArrayList data) { - this.data = data; - return this; - } - - public TokenizeRequest build() { - return new TokenizeRequest(this); - } - } -} diff --git a/flowvault/src/main/java/com/skyflow/vault/data/TokenizeResponse.java b/flowvault/src/main/java/com/skyflow/vault/data/TokenizeResponse.java deleted file mode 100644 index a4b47351..00000000 --- a/flowvault/src/main/java/com/skyflow/vault/data/TokenizeResponse.java +++ /dev/null @@ -1,26 +0,0 @@ -package com.skyflow.vault.data; - -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; - -public class TokenizeResponse { - private List tokenizedData; - private final ArrayList> errors; - - public TokenizeResponse(ArrayList> errors) { - this.errors = errors; - } - public TokenizeResponse(){ - this.errors = new ArrayList<>(); - } - public List getTokenizedData(){ - return this.tokenizedData; - } - public void setTokenizedData(List tokenizedData) { - this.tokenizedData = tokenizedData; - } - public ArrayList> getErrors(){ - return this.errors; - } -} diff --git a/flowvault/src/main/java/com/skyflow/vault/data/UpsertOptions.java b/flowvault/src/main/java/com/skyflow/vault/data/UpsertOptions.java new file mode 100644 index 00000000..dddaedaf --- /dev/null +++ b/flowvault/src/main/java/com/skyflow/vault/data/UpsertOptions.java @@ -0,0 +1,42 @@ +package com.skyflow.vault.data; + +import java.util.List; + +public class UpsertOptions { + private final UpsertOptionsBuilder builder; + + private UpsertOptions(UpsertOptionsBuilder builder) { + this.builder = builder; + } + + public static UpsertOptionsBuilder builder() { + return new UpsertOptionsBuilder(); + } + + public String getUpdateType() { + return this.builder.updateType; + } + + public List getUniqueColumns() { + return this.builder.uniqueColumns; + } + + public static final class UpsertOptionsBuilder { + private String updateType; + private List uniqueColumns; + + public UpsertOptionsBuilder updateType(String updateType) { + this.updateType = updateType; + return this; + } + + public UpsertOptionsBuilder uniqueColumns(List uniqueColumns) { + this.uniqueColumns = uniqueColumns; + return this; + } + + public UpsertOptions build() { + return new UpsertOptions(this); + } + } +} diff --git a/flowvault/src/test/java/com/skyflow/HttpConfigTests.java b/flowvault/src/test/java/com/skyflow/HttpConfigTests.java new file mode 100644 index 00000000..49ce259c --- /dev/null +++ b/flowvault/src/test/java/com/skyflow/HttpConfigTests.java @@ -0,0 +1,596 @@ +package com.skyflow; + +import com.skyflow.config.VaultConfig; +import com.skyflow.enums.Env; +import com.skyflow.errors.SkyflowException; +import com.skyflow.generated.rest.core.RetryInterceptor; +import com.skyflow.vault.controller.VaultController; +import okhttp3.Interceptor; +import okhttp3.OkHttpClient; +import org.junit.Assert; +import org.junit.Test; + +import java.lang.reflect.Field; +import java.util.List; + +/** + * Covers the HTTP timeout / retry configuration and its three-level resolution: + * vault-level override -> client-wide default -> SDK default. + */ +public class HttpConfigTests { + + // OkHttp's own defaults, applied when we leave a per-attempt timeout unset. + private static final int OKHTTP_DEFAULT_TIMEOUT_MILLIS = 10_000; + private static final int SDK_DEFAULT_CALL_TIMEOUT_MILLIS = 60_000; + + private static VaultConfig buildConfig() { + VaultConfig config = new VaultConfig(); + config.setVaultId("vault1"); + config.setClusterId("cluster1"); + config.setEnv(Env.DEV); + return config; + } + + /** Builds the shared OkHttp client without needing credentials or a live token. */ + private static OkHttpClient httpClientOf(VaultClient client) { + client.updateExecutorInHTTP(); + return client.sharedHttpClient; + } + + /** + * Reads the retry budget back out of the generated RetryInterceptor, which exposes no getter. + * If regeneration changes its internals this fails loudly — which is the point. + */ + private static int maxRetriesOf(RetryInterceptor interceptor) { + try { + Field backoffField = RetryInterceptor.class.getDeclaredField("backoff"); + backoffField.setAccessible(true); + Object backoff = backoffField.get(interceptor); + Field maxField = backoff.getClass().getDeclaredField("maxNumRetries"); + maxField.setAccessible(true); + return (int) maxField.get(backoff); + } catch (ReflectiveOperationException e) { + throw new AssertionError("RetryInterceptor internals changed: " + e.getMessage(), e); + } + } + + private static RetryInterceptor retryInterceptorOf(OkHttpClient http) { + for (Interceptor interceptor : http.interceptors()) { + if (interceptor instanceof RetryInterceptor) { + return (RetryInterceptor) interceptor; + } + } + throw new AssertionError("No RetryInterceptor installed on the HTTP client"); + } + + // ── SDK defaults (neither level configured) ─────────────────────────────── + + @Test + public void testDefaults_callTimeoutIs60Seconds() throws SkyflowException { + OkHttpClient http = httpClientOf(new VaultClient(buildConfig(), null)); + + Assert.assertEquals(SDK_DEFAULT_CALL_TIMEOUT_MILLIS, http.callTimeoutMillis()); + } + + @Test + public void testDefaults_retriesAreOff() throws SkyflowException { + OkHttpClient http = httpClientOf(new VaultClient(buildConfig(), null)); + + Assert.assertEquals(0, maxRetriesOf(retryInterceptorOf(http))); + } + + @Test + public void testDefaults_perAttemptTimeoutsLeftAtOkHttpDefaults() throws SkyflowException { + OkHttpClient http = httpClientOf(new VaultClient(buildConfig(), null)); + + Assert.assertEquals(OKHTTP_DEFAULT_TIMEOUT_MILLIS, http.connectTimeoutMillis()); + Assert.assertEquals(OKHTTP_DEFAULT_TIMEOUT_MILLIS, http.readTimeoutMillis()); + Assert.assertEquals(OKHTTP_DEFAULT_TIMEOUT_MILLIS, http.writeTimeoutMillis()); + } + + // ── Client-wide values only ─────────────────────────────────────────────── + + @Test + public void testClientWideConfig_appliesWhenVaultLevelUnset() throws SkyflowException { + VaultClient client = new VaultClient(buildConfig(), null); + client.setCommonHttpConfig(30, 5, 6, 7, 2); + + OkHttpClient http = httpClientOf(client); + + Assert.assertEquals(30_000, http.callTimeoutMillis()); + Assert.assertEquals(5_000, http.connectTimeoutMillis()); + Assert.assertEquals(6_000, http.readTimeoutMillis()); + Assert.assertEquals(7_000, http.writeTimeoutMillis()); + Assert.assertEquals(2, maxRetriesOf(retryInterceptorOf(http))); + } + + // ── Vault-level values only ─────────────────────────────────────────────── + + @Test + public void testVaultLevelConfig_appliesWhenClientWideUnset() throws SkyflowException { + VaultConfig config = buildConfig(); + config.setTimeout(45); + config.setConnectTimeout(11); + config.setReadTimeout(12); + config.setWriteTimeout(13); + config.setMaxRetries(4); + + OkHttpClient http = httpClientOf(new VaultClient(config, null)); + + Assert.assertEquals(45_000, http.callTimeoutMillis()); + Assert.assertEquals(11_000, http.connectTimeoutMillis()); + Assert.assertEquals(12_000, http.readTimeoutMillis()); + Assert.assertEquals(13_000, http.writeTimeoutMillis()); + Assert.assertEquals(4, maxRetriesOf(retryInterceptorOf(http))); + } + + // ── Precedence: both levels set ─────────────────────────────────────────── + + @Test + public void testPrecedence_vaultLevelBeatsClientWideForEverySetting() throws SkyflowException { + VaultConfig config = buildConfig(); + config.setTimeout(45); + config.setConnectTimeout(11); + config.setReadTimeout(12); + config.setWriteTimeout(13); + config.setMaxRetries(4); + + VaultClient client = new VaultClient(config, null); + client.setCommonHttpConfig(30, 5, 6, 7, 2); + + OkHttpClient http = httpClientOf(client); + + Assert.assertEquals(45_000, http.callTimeoutMillis()); + Assert.assertEquals(11_000, http.connectTimeoutMillis()); + Assert.assertEquals(12_000, http.readTimeoutMillis()); + Assert.assertEquals(13_000, http.writeTimeoutMillis()); + Assert.assertEquals(4, maxRetriesOf(retryInterceptorOf(http))); + } + + @Test + public void testPrecedence_settingsResolveIndependently() throws SkyflowException { + // Vault overrides only the read timeout; everything else falls through to client-wide. + VaultConfig config = buildConfig(); + config.setReadTimeout(12); + + VaultClient client = new VaultClient(config, null); + client.setCommonHttpConfig(30, 5, 6, 7, 2); + + OkHttpClient http = httpClientOf(client); + + Assert.assertEquals(12_000, http.readTimeoutMillis()); + Assert.assertEquals(30_000, http.callTimeoutMillis()); + Assert.assertEquals(5_000, http.connectTimeoutMillis()); + Assert.assertEquals(7_000, http.writeTimeoutMillis()); + Assert.assertEquals(2, maxRetriesOf(retryInterceptorOf(http))); + } + + @Test + public void testPrecedence_sdkDefaultUsedWhenBothLevelsNull() throws SkyflowException { + VaultClient client = new VaultClient(buildConfig(), null); + client.setCommonHttpConfig(null, null, null, null, null); + + OkHttpClient http = httpClientOf(client); + + Assert.assertEquals(SDK_DEFAULT_CALL_TIMEOUT_MILLIS, http.callTimeoutMillis()); + Assert.assertEquals(0, maxRetriesOf(retryInterceptorOf(http))); + Assert.assertEquals(OKHTTP_DEFAULT_TIMEOUT_MILLIS, http.connectTimeoutMillis()); + } + + // ── Cache invalidation ──────────────────────────────────────────────────── + + @Test + public void testSetCommonHttpConfig_rebuildsHttpClientWithNewValues() throws SkyflowException { + VaultClient client = new VaultClient(buildConfig(), null); + OkHttpClient first = httpClientOf(client); + Assert.assertEquals(SDK_DEFAULT_CALL_TIMEOUT_MILLIS, first.callTimeoutMillis()); + + client.setCommonHttpConfig(30, null, null, null, 2); + OkHttpClient second = httpClientOf(client); + + Assert.assertNotSame(first, second); + Assert.assertEquals(30_000, second.callTimeoutMillis()); + Assert.assertEquals(2, maxRetriesOf(retryInterceptorOf(second))); + } + + @Test + public void testUpdateExecutorInHTTP_reusesCachedClientWhenConfigUnchanged() throws SkyflowException { + VaultClient client = new VaultClient(buildConfig(), null); + + OkHttpClient first = httpClientOf(client); + OkHttpClient second = httpClientOf(client); + + Assert.assertSame(first, second); + } + + // ── Interceptor wiring ──────────────────────────────────────────────────── + + @Test + public void testInterceptors_retryIsOuterSoEachAttemptRereadsTheToken() throws SkyflowException { + OkHttpClient http = httpClientOf(new VaultClient(buildConfig(), null)); + + List interceptors = http.interceptors(); + Assert.assertEquals(2, interceptors.size()); + Assert.assertTrue("Retry must be registered first so it wraps the auth interceptor", + interceptors.get(0) instanceof RetryInterceptor); + Assert.assertFalse(interceptors.get(1) instanceof RetryInterceptor); + } + + @Test + public void testConnectionPool_isConfigured() throws SkyflowException { + OkHttpClient http = httpClientOf(new VaultClient(buildConfig(), null)); + + Assert.assertNotNull(http.connectionPool()); + } + + // ── Boxed-Integer semantics ─────────────────────────────────────────────── + + @Test + public void testExplicitZeroTimeout_isAValueNotAnInheritSignal() throws SkyflowException { + // Only null means "inherit". An explicit 0 wins over the client-wide value, and OkHttp + // reads 0 as "no timeout" — so this disables the overall ceiling rather than restoring 60s. + VaultConfig config = buildConfig(); + config.setTimeout(0); + + VaultClient client = new VaultClient(config, null); + client.setCommonHttpConfig(30, null, null, null, null); + + Assert.assertEquals(0, httpClientOf(client).callTimeoutMillis()); + } + + @Test + public void testExplicitZeroMaxRetries_overridesClientWideRetries() throws SkyflowException { + VaultConfig config = buildConfig(); + config.setMaxRetries(0); + + VaultClient client = new VaultClient(config, null); + client.setCommonHttpConfig(null, null, null, null, 5); + + Assert.assertEquals(0, maxRetriesOf(retryInterceptorOf(httpClientOf(client)))); + } + + // ── VaultConfig accessors ───────────────────────────────────────────────── + + @Test + public void testVaultConfig_httpSettingsDefaultToNull() { + VaultConfig config = new VaultConfig(); + + Assert.assertNull(config.getTimeout()); + Assert.assertNull(config.getConnectTimeout()); + Assert.assertNull(config.getReadTimeout()); + Assert.assertNull(config.getWriteTimeout()); + Assert.assertNull(config.getMaxRetries()); + } + + @Test + public void testVaultConfig_settersRoundTrip() { + VaultConfig config = new VaultConfig(); + config.setTimeout(45); + config.setConnectTimeout(11); + config.setReadTimeout(12); + config.setWriteTimeout(13); + config.setMaxRetries(4); + + Assert.assertEquals(Integer.valueOf(45), config.getTimeout()); + Assert.assertEquals(Integer.valueOf(11), config.getConnectTimeout()); + Assert.assertEquals(Integer.valueOf(12), config.getReadTimeout()); + Assert.assertEquals(Integer.valueOf(13), config.getWriteTimeout()); + Assert.assertEquals(Integer.valueOf(4), config.getMaxRetries()); + } + + // ── Skyflow builder wiring ──────────────────────────────────────────────── + + @Test + public void testBuilder_httpConfigSetBeforeAddVaultConfigReachesController() throws SkyflowException { + Skyflow client = Skyflow.builder() + .timeout(30) + .connectTimeout(5) + .readTimeout(6) + .writeTimeout(7) + .maxRetries(2) + .addVaultConfig(buildConfig()) + .build(); + + OkHttpClient http = httpClientOf(client.vault()); + + Assert.assertEquals(30_000, http.callTimeoutMillis()); + Assert.assertEquals(5_000, http.connectTimeoutMillis()); + Assert.assertEquals(6_000, http.readTimeoutMillis()); + Assert.assertEquals(7_000, http.writeTimeoutMillis()); + Assert.assertEquals(2, maxRetriesOf(retryInterceptorOf(http))); + } + + @Test + public void testBuilder_httpConfigSetAfterAddVaultConfigStillReachesController() throws SkyflowException { + // Order independence: propagateHttpConfig() must reach controllers already built. + Skyflow client = Skyflow.builder() + .addVaultConfig(buildConfig()) + .timeout(30) + .maxRetries(2) + .build(); + + OkHttpClient http = httpClientOf(client.vault()); + + Assert.assertEquals(30_000, http.callTimeoutMillis()); + Assert.assertEquals(2, maxRetriesOf(retryInterceptorOf(http))); + } + + @Test + public void testBuilder_vaultConfigOverridesBuilderValueEndToEnd() throws SkyflowException { + VaultConfig config = buildConfig(); + config.setTimeout(45); + + Skyflow client = Skyflow.builder() + .timeout(30) + .addVaultConfig(config) + .build(); + + Assert.assertEquals(45_000, httpClientOf(client.vault()).callTimeoutMillis()); + } + + @Test + public void testBuilder_httpConfigSurvivesUpdateVaultConfig() throws SkyflowException { + Skyflow.SkyflowClientBuilder builder = Skyflow.builder() + .timeout(30) + .maxRetries(2) + .addVaultConfig(buildConfig()); + + VaultConfig updated = buildConfig(); + updated.setClusterId("cluster2"); + Skyflow client = builder.updateVaultConfig(updated).build(); + + OkHttpClient http = httpClientOf(client.vault()); + + Assert.assertEquals(30_000, http.callTimeoutMillis()); + Assert.assertEquals(2, maxRetriesOf(retryInterceptorOf(http))); + } + + @Test + public void testBuilder_httpConfigAppliesToEveryVault() throws SkyflowException { + VaultConfig second = new VaultConfig(); + second.setVaultId("vault2"); + second.setClusterId("cluster2"); + second.setEnv(Env.DEV); + + Skyflow.SkyflowClientBuilder builder = Skyflow.builder() + .addVaultConfig(buildConfig()) + .addVaultConfig(second) + .timeout(30); + + // vault() resolves the first entry, so drop vault1 to reach the second controller. + Assert.assertEquals(30_000, httpClientOf(builder.build().vault()).callTimeoutMillis()); + Assert.assertEquals(30_000, + httpClientOf(builder.removeVaultConfig("vault1").build().vault()).callTimeoutMillis()); + } + + @Test + public void testBuilder_httpConfigMethodsAreFluent() { + Skyflow.SkyflowClientBuilder builder = Skyflow.builder(); + + Assert.assertSame(builder, builder.timeout(30)); + Assert.assertSame(builder, builder.connectTimeout(5)); + Assert.assertSame(builder, builder.readTimeout(6)); + Assert.assertSame(builder, builder.writeTimeout(7)); + Assert.assertSame(builder, builder.maxRetries(2)); + } + + // ── Precedence through the public API: both levels set ──────────────────── + // Each test sets the same setting on Skyflow.builder() AND on VaultConfig, then asserts the + // VaultConfig value is what reaches the wire client. + + @Test + public void testPrecedence_endToEnd_vaultTimeoutBeatsClientLevelTimeout() throws SkyflowException { + VaultConfig config = buildConfig(); + config.setTimeout(45); + + Skyflow client = Skyflow.builder().timeout(30).addVaultConfig(config).build(); + + Assert.assertEquals(45_000, httpClientOf(client.vault()).callTimeoutMillis()); + } + + @Test + public void testPrecedence_endToEnd_vaultConnectTimeoutBeatsClientLevel() throws SkyflowException { + VaultConfig config = buildConfig(); + config.setConnectTimeout(11); + + Skyflow client = Skyflow.builder().connectTimeout(5).addVaultConfig(config).build(); + + Assert.assertEquals(11_000, httpClientOf(client.vault()).connectTimeoutMillis()); + } + + @Test + public void testPrecedence_endToEnd_vaultReadTimeoutBeatsClientLevel() throws SkyflowException { + VaultConfig config = buildConfig(); + config.setReadTimeout(12); + + Skyflow client = Skyflow.builder().readTimeout(6).addVaultConfig(config).build(); + + Assert.assertEquals(12_000, httpClientOf(client.vault()).readTimeoutMillis()); + } + + @Test + public void testPrecedence_endToEnd_vaultWriteTimeoutBeatsClientLevel() throws SkyflowException { + VaultConfig config = buildConfig(); + config.setWriteTimeout(13); + + Skyflow client = Skyflow.builder().writeTimeout(7).addVaultConfig(config).build(); + + Assert.assertEquals(13_000, httpClientOf(client.vault()).writeTimeoutMillis()); + } + + @Test + public void testPrecedence_endToEnd_vaultMaxRetriesBeatsClientLevel() throws SkyflowException { + VaultConfig config = buildConfig(); + config.setMaxRetries(4); + + Skyflow client = Skyflow.builder().maxRetries(2).addVaultConfig(config).build(); + + Assert.assertEquals(4, maxRetriesOf(retryInterceptorOf(httpClientOf(client.vault())))); + } + + @Test + public void testPrecedence_endToEnd_allFiveSetAtBothLevels() throws SkyflowException { + VaultConfig config = buildConfig(); + config.setTimeout(45); + config.setConnectTimeout(11); + config.setReadTimeout(12); + config.setWriteTimeout(13); + config.setMaxRetries(4); + + Skyflow client = Skyflow.builder() + .timeout(30) + .connectTimeout(5) + .readTimeout(6) + .writeTimeout(7) + .maxRetries(2) + .addVaultConfig(config) + .build(); + + OkHttpClient http = httpClientOf(client.vault()); + + Assert.assertEquals(45_000, http.callTimeoutMillis()); + Assert.assertEquals(11_000, http.connectTimeoutMillis()); + Assert.assertEquals(12_000, http.readTimeoutMillis()); + Assert.assertEquals(13_000, http.writeTimeoutMillis()); + Assert.assertEquals(4, maxRetriesOf(retryInterceptorOf(http))); + } + + @Test + public void testPrecedence_endToEnd_partialOverrideTakesEachLevelPerSetting() throws SkyflowException { + // Vault overrides timeout and maxRetries only; the rest come from the client level. + VaultConfig config = buildConfig(); + config.setTimeout(45); + config.setMaxRetries(4); + + Skyflow client = Skyflow.builder() + .timeout(30) + .connectTimeout(5) + .readTimeout(6) + .writeTimeout(7) + .maxRetries(2) + .addVaultConfig(config) + .build(); + + OkHttpClient http = httpClientOf(client.vault()); + + Assert.assertEquals(45_000, http.callTimeoutMillis()); // vault + Assert.assertEquals(4, maxRetriesOf(retryInterceptorOf(http))); // vault + Assert.assertEquals(5_000, http.connectTimeoutMillis()); // client + Assert.assertEquals(6_000, http.readTimeoutMillis()); // client + Assert.assertEquals(7_000, http.writeTimeoutMillis()); // client + } + + @Test + public void testPrecedence_endToEnd_holdsWhenClientLevelIsSetAfterAddVaultConfig() throws SkyflowException { + // Builder call order must not change who wins. + VaultConfig config = buildConfig(); + config.setTimeout(45); + + Skyflow client = Skyflow.builder().addVaultConfig(config).timeout(30).build(); + + Assert.assertEquals(45_000, httpClientOf(client.vault()).callTimeoutMillis()); + } + + @Test + public void testPrecedence_endToEnd_appliesPerVaultNotGlobally() throws SkyflowException { + // vault1 overrides the timeout; vault2 leaves it unset and inherits the client-level value. + VaultConfig overriding = buildConfig(); + overriding.setTimeout(45); + + VaultConfig inheriting = new VaultConfig(); + inheriting.setVaultId("vault2"); + inheriting.setClusterId("cluster2"); + inheriting.setEnv(Env.DEV); + + Skyflow.SkyflowClientBuilder builder = Skyflow.builder() + .timeout(30) + .addVaultConfig(overriding) + .addVaultConfig(inheriting); + + // vault() resolves the first entry, so drop vault1 to reach the second controller. + Assert.assertEquals(45_000, httpClientOf(builder.build().vault()).callTimeoutMillis()); + Assert.assertEquals(30_000, + httpClientOf(builder.removeVaultConfig("vault1").build().vault()).callTimeoutMillis()); + } + + @Test + public void testPrecedence_endToEnd_vaultStillWinsAfterUpdateVaultConfig() throws SkyflowException { + VaultConfig config = buildConfig(); + config.setTimeout(45); + Skyflow.SkyflowClientBuilder builder = Skyflow.builder().timeout(30).addVaultConfig(config); + + VaultConfig update = buildConfig(); + update.setTimeout(90); + + Assert.assertEquals(90_000, + httpClientOf(builder.updateVaultConfig(update).build().vault()).callTimeoutMillis()); + } + + @Test + public void testPrecedence_endToEnd_updateWithoutHttpSettingsKeepsTheExistingOnes() throws SkyflowException { + // A null on the incoming config means "leave as is", matching how the base class merges. + VaultConfig config = buildConfig(); + config.setTimeout(45); + config.setMaxRetries(4); + Skyflow.SkyflowClientBuilder builder = Skyflow.builder().timeout(30).addVaultConfig(config); + + VaultConfig update = buildConfig(); + update.setClusterId("cluster2"); + + OkHttpClient http = httpClientOf(builder.updateVaultConfig(update).build().vault()); + + Assert.assertEquals(45_000, http.callTimeoutMillis()); + Assert.assertEquals(4, maxRetriesOf(retryInterceptorOf(http))); + } + + @Test + public void testPrecedence_endToEnd_updateVaultConfigCarriesAllFiveHttpSettings() throws SkyflowException { + // Regression: BaseSkyflow.mergeVaultConfig() carries only env/clusterId/credentials, so + // without SkyflowClientBuilder.carryHttpOverrides() every value below is silently dropped + // and the vault keeps running on its original settings. + VaultConfig config = buildConfig(); + config.setTimeout(45); + config.setConnectTimeout(11); + config.setReadTimeout(12); + config.setWriteTimeout(13); + config.setMaxRetries(4); + Skyflow.SkyflowClientBuilder builder = Skyflow.builder().addVaultConfig(config); + + VaultConfig update = buildConfig(); + update.setTimeout(90); + update.setConnectTimeout(21); + update.setReadTimeout(22); + update.setWriteTimeout(23); + update.setMaxRetries(8); + + OkHttpClient http = httpClientOf(builder.updateVaultConfig(update).build().vault()); + + Assert.assertEquals(90_000, http.callTimeoutMillis()); + Assert.assertEquals(21_000, http.connectTimeoutMillis()); + Assert.assertEquals(22_000, http.readTimeoutMillis()); + Assert.assertEquals(23_000, http.writeTimeoutMillis()); + Assert.assertEquals(8, maxRetriesOf(retryInterceptorOf(http))); + } + + @Test + public void testPrecedence_endToEnd_updateCanIntroduceAVaultOverride() throws SkyflowException { + // Vault starts with no override (inherits 30), then an update introduces one. + Skyflow.SkyflowClientBuilder builder = Skyflow.builder().timeout(30).addVaultConfig(buildConfig()); + Assert.assertEquals(30_000, httpClientOf(builder.build().vault()).callTimeoutMillis()); + + VaultConfig update = buildConfig(); + update.setTimeout(45); + + Assert.assertEquals(45_000, + httpClientOf(builder.updateVaultConfig(update).build().vault()).callTimeoutMillis()); + } + + @Test + public void testController_isAVaultClientSoItInheritsTheHttpConfig() throws SkyflowException { + Skyflow client = Skyflow.builder().timeout(30).addVaultConfig(buildConfig()).build(); + + VaultController controller = client.vault(); + + Assert.assertEquals(30_000, httpClientOf(controller).callTimeoutMillis()); + } +} diff --git a/flowvault/src/test/java/com/skyflow/SkyflowTests.java b/flowvault/src/test/java/com/skyflow/SkyflowTests.java index fc215e2b..062989cc 100644 --- a/flowvault/src/test/java/com/skyflow/SkyflowTests.java +++ b/flowvault/src/test/java/com/skyflow/SkyflowTests.java @@ -52,6 +52,70 @@ public void testAddVaultConfig_invalidConfigThrows() { } } + // ── updateVaultConfig: flowvault-specific fields ───────────────────────── + // BaseSkyflow.mergeVaultConfig() only carries env/clusterId/credentials, so vaultUrl needs + // SkyflowClientBuilder.carryVaultOverrides() to survive an update. + + @Test + public void testUpdateVaultConfig_changesVaultURL() throws SkyflowException { + VaultConfig config = buildConfig("vault1", "cluster1"); + config.setVaultUrl("https://first.example.com"); + Skyflow.SkyflowClientBuilder builder = Skyflow.builder().addVaultConfig(config); + Assert.assertEquals("https://first.example.com", builder.build().vault().currentVaultURL); + + VaultConfig update = buildConfig("vault1", "cluster1"); + update.setVaultUrl("https://second.example.com"); + + Assert.assertEquals("https://second.example.com", + builder.updateVaultConfig(update).build().vault().currentVaultURL); + } + + @Test + public void testUpdateVaultConfig_storesTheNewVaultURLOnTheConfig() throws SkyflowException { + VaultConfig config = buildConfig("vault1", "cluster1"); + config.setVaultUrl("https://first.example.com"); + Skyflow.SkyflowClientBuilder builder = Skyflow.builder().addVaultConfig(config); + + VaultConfig update = buildConfig("vault1", "cluster1"); + update.setVaultUrl("https://second.example.com"); + + Assert.assertEquals("https://second.example.com", + builder.updateVaultConfig(update).build().getVaultConfig("vault1").getVaultUrl()); + } + + @Test + public void testUpdateVaultConfig_omittingVaultURLKeepsTheExistingOne() throws SkyflowException { + VaultConfig config = buildConfig("vault1", "cluster1"); + config.setVaultUrl("https://first.example.com"); + Skyflow.SkyflowClientBuilder builder = Skyflow.builder().addVaultConfig(config); + + // No vaultUrl on the update: null means "leave as is", as elsewhere in the merge. + Skyflow client = builder.updateVaultConfig(buildConfig("vault1", "cluster2")).build(); + + Assert.assertEquals("https://first.example.com", client.vault().currentVaultURL); + } + + @Test + public void testUpdateVaultConfig_canIntroduceAVaultURLWhereClusterIdWasUsed() throws SkyflowException { + Skyflow.SkyflowClientBuilder builder = Skyflow.builder().addVaultConfig(buildConfig("vault1", "cluster1")); + Assert.assertEquals("https://cluster1.skyvault.skyflowapis.dev", builder.build().vault().currentVaultURL); + + VaultConfig update = buildConfig("vault1", "cluster1"); + update.setVaultUrl("https://explicit.example.com"); + + Assert.assertEquals("https://explicit.example.com", + builder.updateVaultConfig(update).build().vault().currentVaultURL); + } + + @Test + public void testUpdateVaultConfig_clusterIdChangeStillRebuildsTheURL() throws SkyflowException { + Skyflow.SkyflowClientBuilder builder = Skyflow.builder().addVaultConfig(buildConfig("vault1", "cluster1")); + + Skyflow client = builder.updateVaultConfig(buildConfig("vault1", "cluster2")).build(); + + Assert.assertEquals("https://cluster2.skyvault.skyflowapis.dev", client.vault().currentVaultURL); + } + // ── updateVaultConfig ───────────────────────────────────────────────────── @Test @@ -97,6 +161,153 @@ public void testRemoveVaultConfig_nonExistentVaultIdThrows() { } } + // ── Client management lifecycles ───────────────────────────────────────── + // Whole add/update/remove sequences, asserting both the stored config and the controller + // behind vault() stay in step at every stage. + + private static final String NOT_IN_CONFIG_LIST = "VaultId is missing from the config"; + + @Test + public void testLifecycle_addThenRemove() throws SkyflowException { + Skyflow.SkyflowClientBuilder builder = Skyflow.builder().addVaultConfig(buildConfig("vault1", "cluster1")); + + Skyflow afterAdd = builder.build(); + Assert.assertEquals("cluster1", afterAdd.getVaultConfig("vault1").getClusterId()); + Assert.assertNotNull(afterAdd.vault()); + + Skyflow afterRemove = builder.removeVaultConfig("vault1").build(); + + Assert.assertNull(afterRemove.getVaultConfig("vault1")); + try { + afterRemove.vault(); + Assert.fail(EXCEPTION_NOT_THROWN); + } catch (SkyflowException e) { + Assert.assertTrue(e.getMessage().contains(NOT_IN_CONFIG_LIST)); + } + } + + @Test + public void testLifecycle_addThenRemoveThenReAddSameVaultId() throws SkyflowException { + // Removing must clear the id, otherwise re-adding would trip the duplicate check. + Skyflow.SkyflowClientBuilder builder = Skyflow.builder() + .addVaultConfig(buildConfig("vault1", "cluster1")) + .removeVaultConfig("vault1") + .addVaultConfig(buildConfig("vault1", "cluster2")); + + Skyflow client = builder.build(); + + Assert.assertEquals("cluster2", client.getVaultConfig("vault1").getClusterId()); + Assert.assertEquals("https://cluster2.skyvault.skyflowapis.dev", client.vault().currentVaultURL); + } + + @Test + public void testLifecycle_addRemoveThenUpdateFails() throws SkyflowException { + Skyflow.SkyflowClientBuilder builder = Skyflow.builder() + .addVaultConfig(buildConfig("vault1", "cluster1")) + .removeVaultConfig("vault1"); + + try { + builder.updateVaultConfig(buildConfig("vault1", "cluster2")); + Assert.fail(EXCEPTION_NOT_THROWN); + } catch (SkyflowException e) { + Assert.assertTrue(e.getMessage().contains(NOT_IN_CONFIG_LIST)); + } + } + + @Test + public void testLifecycle_addRemoveThenFailedUpdateLeavesNoConfigBehind() throws SkyflowException { + // A rejected update must not resurrect the removed vault. + Skyflow.SkyflowClientBuilder builder = Skyflow.builder() + .addVaultConfig(buildConfig("vault1", "cluster1")) + .removeVaultConfig("vault1"); + try { + builder.updateVaultConfig(buildConfig("vault1", "cluster2")); + Assert.fail(EXCEPTION_NOT_THROWN); + } catch (SkyflowException expected) { + // asserted in testLifecycle_addRemoveThenUpdateFails + } + + Skyflow client = builder.build(); + + Assert.assertNull(client.getVaultConfig("vault1")); + try { + client.vault(); + Assert.fail(EXCEPTION_NOT_THROWN); + } catch (SkyflowException e) { + Assert.assertTrue(e.getMessage().contains(NOT_IN_CONFIG_LIST)); + } + } + + @Test + public void testLifecycle_addUpdateThenRemove() throws SkyflowException { + Skyflow.SkyflowClientBuilder builder = Skyflow.builder().addVaultConfig(buildConfig("vault1", "cluster1")); + + VaultConfig update = buildConfig("vault1", "cluster2"); + update.setEnv(Env.PROD); + Skyflow afterUpdate = builder.updateVaultConfig(update).build(); + + Assert.assertEquals("cluster2", afterUpdate.getVaultConfig("vault1").getClusterId()); + Assert.assertEquals(Env.PROD, afterUpdate.getVaultConfig("vault1").getEnv()); + Assert.assertEquals("https://cluster2.skyvault.skyflowapis.com", afterUpdate.vault().currentVaultURL); + + Skyflow afterRemove = builder.removeVaultConfig("vault1").build(); + + Assert.assertNull(afterRemove.getVaultConfig("vault1")); + try { + afterRemove.vault(); + Assert.fail(EXCEPTION_NOT_THROWN); + } catch (SkyflowException e) { + Assert.assertTrue(e.getMessage().contains(NOT_IN_CONFIG_LIST)); + } + } + + @Test + public void testLifecycle_addUpdateRemoveThenRemoveAgainFails() throws SkyflowException { + Skyflow.SkyflowClientBuilder builder = Skyflow.builder() + .addVaultConfig(buildConfig("vault1", "cluster1")) + .updateVaultConfig(buildConfig("vault1", "cluster2")) + .removeVaultConfig("vault1"); + + try { + builder.removeVaultConfig("vault1"); + Assert.fail(EXCEPTION_NOT_THROWN); + } catch (SkyflowException e) { + Assert.assertTrue(e.getMessage().contains(NOT_IN_CONFIG_LIST)); + } + } + + @Test + public void testLifecycle_removingOneVaultLeavesTheOtherIntact() throws SkyflowException { + Skyflow client = Skyflow.builder() + .addVaultConfig(buildConfig("vault1", "cluster1")) + .addVaultConfig(buildConfig("vault2", "cluster2")) + .removeVaultConfig("vault1") + .build(); + + Assert.assertNull(client.getVaultConfig("vault1")); + Assert.assertEquals("cluster2", client.getVaultConfig("vault2").getClusterId()); + Assert.assertEquals("https://cluster2.skyvault.skyflowapis.dev", client.vault().currentVaultURL); + } + + @Test + public void testLifecycle_onTheBuiltClientRatherThanTheBuilder() throws SkyflowException { + // Skyflow exposes the same add/update/remove surface as the builder; exercise that path too. + Skyflow client = Skyflow.builder().addVaultConfig(buildConfig("vault1", "cluster1")).build(); + + client.updateVaultConfig(buildConfig("vault1", "cluster2")); + Assert.assertEquals("cluster2", client.getVaultConfig("vault1").getClusterId()); + + client.removeVaultConfig("vault1"); + Assert.assertNull(client.getVaultConfig("vault1")); + + try { + client.updateVaultConfig(buildConfig("vault1", "cluster3")); + Assert.fail(EXCEPTION_NOT_THROWN); + } catch (SkyflowException e) { + Assert.assertTrue(e.getMessage().contains(NOT_IN_CONFIG_LIST)); + } + } + // ── addSkyflowCredentials ───────────────────────────────────────────────── @Test diff --git a/flowvault/src/test/java/com/skyflow/UpdatePropagationTests.java b/flowvault/src/test/java/com/skyflow/UpdatePropagationTests.java new file mode 100644 index 00000000..db0d28db --- /dev/null +++ b/flowvault/src/test/java/com/skyflow/UpdatePropagationTests.java @@ -0,0 +1,268 @@ +package com.skyflow; + +import com.skyflow.config.Credentials; +import com.skyflow.config.VaultConfig; +import com.skyflow.enums.Env; +import com.skyflow.errors.SkyflowException; +import com.skyflow.vault.controller.VaultController; +import org.junit.Assert; +import org.junit.Test; + +/** + * Verifies that an update reaches every place the value is actually consumed — the stored config, + * the controller holding it, and everything derived from it (vault URL, HTTP client, bearer token) + * — rather than only the copy in vaultConfigMap. + */ +public class UpdatePropagationTests { + + private static VaultConfig buildConfig(String vaultId, String clusterId) { + VaultConfig config = new VaultConfig(); + config.setVaultId(vaultId); + config.setClusterId(clusterId); + config.setEnv(Env.DEV); + return config; + } + + private static Credentials tokenCredentials(String token) { + Credentials credentials = new Credentials(); + credentials.setToken(token); + return credentials; + } + + // ── updateVaultConfig reaches the controller, not just the stored config ── + + @Test + public void testUpdateVaultConfig_storedConfigAndControllerConfigAgree() throws SkyflowException { + Skyflow.SkyflowClientBuilder builder = Skyflow.builder().addVaultConfig(buildConfig("vault1", "cluster1")); + + VaultConfig update = buildConfig("vault1", "cluster2"); + update.setEnv(Env.PROD); + Skyflow client = builder.updateVaultConfig(update).build(); + + // The copy the user can read back... + Assert.assertEquals("cluster2", client.getVaultConfig("vault1").getClusterId()); + Assert.assertEquals(Env.PROD, client.getVaultConfig("vault1").getEnv()); + // ...and the copy the controller actually builds requests from. + Assert.assertEquals("cluster2", client.vault().getVaultConfig().getClusterId()); + Assert.assertEquals(Env.PROD, client.vault().getVaultConfig().getEnv()); + } + + @Test + public void testUpdateVaultConfig_reconfiguresTheControllerInPlace() throws SkyflowException { + Skyflow.SkyflowClientBuilder builder = Skyflow.builder().addVaultConfig(buildConfig("vault1", "cluster1")); + VaultController before = builder.build().vault(); + + VaultController after = builder.updateVaultConfig(buildConfig("vault1", "cluster2")).build().vault(); + + Assert.assertSame(before, after); + } + + @Test + public void testUpdateVaultConfig_handleHeldAcrossTheUpdateSeesTheNewConfig() throws SkyflowException { + // Replacing the controller instead of reconfiguring it would leave this reference talking + // to the old vault, with no error to say so. + Skyflow.SkyflowClientBuilder builder = Skyflow.builder().addVaultConfig(buildConfig("vault1", "cluster1")); + VaultController held = builder.build().vault(); + Assert.assertEquals("https://cluster1.skyvault.skyflowapis.dev", held.currentVaultURL); + + VaultConfig update = buildConfig("vault1", "cluster2"); + update.setEnv(Env.PROD); + builder.updateVaultConfig(update); + + Assert.assertEquals("cluster2", held.getVaultConfig().getClusterId()); + Assert.assertEquals(Env.PROD, held.getVaultConfig().getEnv()); + Assert.assertEquals("https://cluster2.skyvault.skyflowapis.com", held.currentVaultURL); + } + + @Test + public void testUpdateVaultConfig_handleHeldAcrossTheUpdateSeesNewCredentials() throws SkyflowException { + VaultConfig config = buildConfig("vault1", "cluster1"); + config.setCredentials(tokenCredentials("first-token")); + Skyflow.SkyflowClientBuilder builder = Skyflow.builder().addVaultConfig(config); + VaultController held = builder.build().vault(); + held.setBearerToken(); + Assert.assertEquals("first-token", held.token); + + VaultConfig update = buildConfig("vault1", "cluster1"); + update.setCredentials(tokenCredentials("second-token")); + builder.updateVaultConfig(update); + held.setBearerToken(); + + Assert.assertEquals("second-token", held.token); + } + + @Test + public void testUpdateVaultConfig_handleHeldAcrossTheUpdateSeesNewHttpSettings() throws SkyflowException { + Skyflow.SkyflowClientBuilder builder = Skyflow.builder().addVaultConfig(buildConfig("vault1", "cluster1")); + VaultController held = builder.build().vault(); + held.updateExecutorInHTTP(); + Assert.assertEquals(60_000, held.sharedHttpClient.callTimeoutMillis()); + + VaultConfig update = buildConfig("vault1", "cluster1"); + update.setTimeout(45); + builder.updateVaultConfig(update); + held.updateExecutorInHTTP(); + + Assert.assertEquals(45_000, held.sharedHttpClient.callTimeoutMillis()); + } + + @Test + public void testUpdateVaultConfig_derivedVaultURLIsRebuilt() throws SkyflowException { + Skyflow.SkyflowClientBuilder builder = Skyflow.builder().addVaultConfig(buildConfig("vault1", "cluster1")); + Assert.assertEquals("https://cluster1.skyvault.skyflowapis.dev", builder.build().vault().currentVaultURL); + + VaultConfig update = buildConfig("vault1", "cluster2"); + update.setEnv(Env.PROD); + + Assert.assertEquals("https://cluster2.skyvault.skyflowapis.com", + builder.updateVaultConfig(update).build().vault().currentVaultURL); + } + + @Test + public void testUpdateVaultConfig_newVaultLevelCredentialsReachTheToken() throws SkyflowException { + VaultConfig config = buildConfig("vault1", "cluster1"); + config.setCredentials(tokenCredentials("first-token")); + Skyflow.SkyflowClientBuilder builder = Skyflow.builder().addVaultConfig(config); + + VaultController before = builder.build().vault(); + before.setBearerToken(); + Assert.assertEquals("first-token", before.token); + + VaultConfig update = buildConfig("vault1", "cluster1"); + update.setCredentials(tokenCredentials("second-token")); + VaultController after = builder.updateVaultConfig(update).build().vault(); + after.setBearerToken(); + + Assert.assertEquals("second-token", after.token); + } + + @Test + public void testUpdateVaultConfig_omittingCredentialsKeepsTheExistingOnes() throws SkyflowException { + VaultConfig config = buildConfig("vault1", "cluster1"); + config.setCredentials(tokenCredentials("first-token")); + Skyflow.SkyflowClientBuilder builder = Skyflow.builder().addVaultConfig(config); + + // No credentials on the update: null means "leave as is". + VaultController after = builder.updateVaultConfig(buildConfig("vault1", "cluster2")).build().vault(); + after.setBearerToken(); + + Assert.assertEquals("first-token", after.token); + } + + @Test + public void testUpdateVaultConfig_stillHasTheClientWideCredentialsAfterwards() throws SkyflowException { + Skyflow.SkyflowClientBuilder builder = Skyflow.builder() + .addSkyflowCredentials(tokenCredentials("common-token")) + .addVaultConfig(buildConfig("vault1", "cluster1")); + + // Reconfiguring must not clear the client-wide credentials the controller already holds. + VaultController after = builder.updateVaultConfig(buildConfig("vault1", "cluster2")).build().vault(); + after.setBearerToken(); + + Assert.assertEquals("common-token", after.token); + } + + @Test + public void testUpdateVaultConfig_vaultLevelCredentialsStillBeatClientWide() throws SkyflowException { + VaultConfig config = buildConfig("vault1", "cluster1"); + config.setCredentials(tokenCredentials("vault-token")); + Skyflow.SkyflowClientBuilder builder = Skyflow.builder() + .addSkyflowCredentials(tokenCredentials("common-token")) + .addVaultConfig(config); + + VaultController after = builder.updateVaultConfig(buildConfig("vault1", "cluster2")).build().vault(); + after.setBearerToken(); + + Assert.assertEquals("vault-token", after.token); + } + + // ── Credentials updates reach every controller ─────────────────────────── + + @Test + public void testAddSkyflowCredentials_afterVaultExists_reachesTheController() throws SkyflowException { + Skyflow.SkyflowClientBuilder builder = Skyflow.builder().addVaultConfig(buildConfig("vault1", "cluster1")); + + VaultController controller = builder.addSkyflowCredentials(tokenCredentials("common-token")).build().vault(); + controller.setBearerToken(); + + Assert.assertEquals("common-token", controller.token); + } + + @Test + public void testAddSkyflowCredentials_beforeVaultExists_reachesTheNewController() throws SkyflowException { + Skyflow client = Skyflow.builder() + .addSkyflowCredentials(tokenCredentials("common-token")) + .addVaultConfig(buildConfig("vault1", "cluster1")) + .build(); + + VaultController controller = client.vault(); + controller.setBearerToken(); + + Assert.assertEquals("common-token", controller.token); + } + + @Test + public void testAddSkyflowCredentials_reachesEveryVaultNotJustTheFirst() throws SkyflowException { + Skyflow.SkyflowClientBuilder builder = Skyflow.builder() + .addVaultConfig(buildConfig("vault1", "cluster1")) + .addVaultConfig(buildConfig("vault2", "cluster2")) + .addSkyflowCredentials(tokenCredentials("common-token")); + + VaultController first = builder.build().vault(); + first.setBearerToken(); + Assert.assertEquals("common-token", first.token); + + // vault() resolves the first entry, so drop vault1 to reach the second controller. + VaultController second = builder.removeVaultConfig("vault1").build().vault(); + second.setBearerToken(); + Assert.assertEquals("common-token", second.token); + } + + @Test + public void testUpdateSkyflowCredentials_invalidatesTheCachedToken() throws SkyflowException { + Skyflow client = Skyflow.builder() + .addSkyflowCredentials(tokenCredentials("first-token")) + .addVaultConfig(buildConfig("vault1", "cluster1")) + .build(); + + VaultController controller = client.vault(); + controller.setBearerToken(); + Assert.assertEquals("first-token", controller.token); + + client.updateSkyflowCredentials(tokenCredentials("second-token")); + controller.setBearerToken(); + + Assert.assertEquals("A cached token must not survive a credentials change", + "second-token", controller.token); + } + + @Test + public void testUpdateSkyflowCredentials_doesNotOverrideVaultLevelCredentials() throws SkyflowException { + VaultConfig config = buildConfig("vault1", "cluster1"); + config.setCredentials(tokenCredentials("vault-token")); + Skyflow client = Skyflow.builder().addVaultConfig(config).build(); + + client.updateSkyflowCredentials(tokenCredentials("common-token")); + VaultController controller = client.vault(); + controller.setBearerToken(); + + Assert.assertEquals("vault-token", controller.token); + } + + @Test + public void testUpdateSkyflowCredentials_appliesToTheSameControllerInstance() throws SkyflowException { + // Unlike updateVaultConfig, a credentials change must not swap the controller out — it + // reconfigures the existing one, so a handle the caller already holds stays valid. + Skyflow client = Skyflow.builder() + .addSkyflowCredentials(tokenCredentials("first-token")) + .addVaultConfig(buildConfig("vault1", "cluster1")) + .build(); + VaultController held = client.vault(); + + client.updateSkyflowCredentials(tokenCredentials("second-token")); + + Assert.assertSame(held, client.vault()); + held.setBearerToken(); + Assert.assertEquals("second-token", held.token); + } +} diff --git a/flowvault/src/test/java/com/skyflow/VaultClientTests.java b/flowvault/src/test/java/com/skyflow/VaultClientTests.java index 24e741b5..68f026fb 100644 --- a/flowvault/src/test/java/com/skyflow/VaultClientTests.java +++ b/flowvault/src/test/java/com/skyflow/VaultClientTests.java @@ -20,12 +20,12 @@ private static VaultConfig buildConfig(String vaultId, String clusterId, Credent return config; } - // ── updateVaultURL priority order ──────────────────────────────────────── + // ── updateVaultUrl priority order ──────────────────────────────────────── @Test public void testUpdateVaultURL_usesExplicitVaultURLOverClusterId() throws SkyflowException { VaultConfig config = buildConfig("vault1", "cluster1", null); - config.setVaultURL("https://custom.example.com"); + config.setVaultUrl("https://custom.example.com"); VaultClient client = new VaultClient(config, null); diff --git a/flowvault/src/test/java/com/skyflow/config/VaultConfigTests.java b/flowvault/src/test/java/com/skyflow/config/VaultConfigTests.java index a1c82af5..065848a1 100644 --- a/flowvault/src/test/java/com/skyflow/config/VaultConfigTests.java +++ b/flowvault/src/test/java/com/skyflow/config/VaultConfigTests.java @@ -52,14 +52,14 @@ public void testCredentialsGetterSetter() { @Test public void testVaultURLDefaultsToNull() { VaultConfig config = new VaultConfig(); - Assert.assertNull(config.getVaultURL()); + Assert.assertNull(config.getVaultUrl()); } @Test public void testVaultURLGetterSetter() { VaultConfig config = new VaultConfig(); - config.setVaultURL("https://myvault.example.com"); - Assert.assertEquals("https://myvault.example.com", config.getVaultURL()); + config.setVaultUrl("https://myvault.example.com"); + Assert.assertEquals("https://myvault.example.com", config.getVaultUrl()); } @Test diff --git a/flowvault/src/test/java/com/skyflow/utils/RequestFidelityTests.java b/flowvault/src/test/java/com/skyflow/utils/RequestFidelityTests.java new file mode 100644 index 00000000..de1f3b40 --- /dev/null +++ b/flowvault/src/test/java/com/skyflow/utils/RequestFidelityTests.java @@ -0,0 +1,875 @@ +package com.skyflow.utils; + +import com.skyflow.config.VaultConfig; +import com.skyflow.generated.rest.resources.flowservice.requests.V1FlowDeleteTokenRequest; +import com.skyflow.generated.rest.resources.flowservice.requests.V1FlowDetokenizeRequest; +import com.skyflow.generated.rest.resources.flowservice.requests.V1FlowTokenizeRequest; +import com.skyflow.generated.rest.resources.flowservice.requests.V1InsertRequest; +import com.skyflow.generated.rest.types.FlowEnumUpdateType; +import com.skyflow.generated.rest.types.V1DeleteTokenResponseObject; +import com.skyflow.generated.rest.types.V1FlowDeleteTokenResponse; +import com.skyflow.generated.rest.types.V1FlowDetokenizeResponse; +import com.skyflow.generated.rest.types.V1FlowDetokenizeResponseObject; +import com.skyflow.generated.rest.types.V1FlowTokenizeRequestObject; +import com.skyflow.generated.rest.types.V1InsertRecordData; +import com.skyflow.generated.rest.types.V1InsertResponse; +import com.skyflow.generated.rest.types.V1RecordResponseObject; +import com.skyflow.generated.rest.types.V1TokenGroupRedactions; +import com.skyflow.vault.data.BulkDeleteTokensRequest; +import com.skyflow.vault.data.BulkDeleteTokensResponse; +import com.skyflow.vault.data.BulkDetokenizeRequest; +import com.skyflow.vault.data.BulkDetokenizeResponse; +import com.skyflow.vault.data.BulkInsertRequest; +import com.skyflow.vault.data.BulkInsertRequestRecord; +import com.skyflow.vault.data.BulkInsertResponse; +import com.skyflow.vault.data.BulkTokenizeRecord; +import com.skyflow.vault.data.BulkTokenizeRequest; +import com.skyflow.vault.data.InsertRequestRecord; +import com.skyflow.vault.data.TokenGroupRedactions; +import com.skyflow.vault.data.UpsertOptions; +import org.junit.Assert; +import org.junit.Test; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +/** + * Request-fidelity tests: every value an SDK user sets on a flowvault request object must reach + * the outgoing generated REST request object faithfully — same value, same field, same order — + * including across batching. + * + * These tests deliberately assert on the real mapping code in {@link Utils} rather than on any + * hand-rolled copy of it, and use {@code assertSame} where the SDK should be passing the user's + * own object through untouched (maps, lists, arbitrary tokenize values). + */ +public class RequestFidelityTests { + + private static final String VAULT_ID = "vault123"; + + // Values that are easy to mangle: non-ASCII, embedded spaces, punctuation. + private static final String NON_ASCII_NAME = "日本語 テスト Ω"; + private static final String SPACED_TABLE = "my table name"; + private static final String NON_ASCII_TABLE = "表_日本語"; + + private static VaultConfig vaultConfig() { + VaultConfig config = new VaultConfig(); + config.setVaultId(VAULT_ID); + return config; + } + + private static ArrayList recordList(InsertRequestRecord... records) { + return new ArrayList<>(Arrays.asList(records)); + } + + // ───────────────────────────────────────────────────────────────────────── + // Bulk insert — field fidelity + // ───────────────────────────────────────────────────────────────────────── + + @Test + public void testBulkInsert_everyRecordFieldReachesWire() { + Map data = new LinkedHashMap<>(); + data.put("name", "john"); + Map tokens = new LinkedHashMap<>(); + tokens.put("name", "tok-abc"); + UpsertOptions upsert = UpsertOptions.builder() + .updateType("UPDATE") + .uniqueColumns(Arrays.asList("email", "phone")) + .build(); + + BulkInsertRequestRecord record = BulkInsertRequestRecord.builder() + .tableName("cards") + .data(data) + .tokens(tokens) + .upsert(upsert) + .build(); + BulkInsertRequest request = BulkInsertRequest.builder() + .tableName("cards") + .records(recordList(record)) + .build(); + + V1InsertRequest body = Utils.getBulkInsertRequestBody(request, vaultConfig()); + + Assert.assertEquals(VAULT_ID, body.getVaultId().get()); + Assert.assertEquals("cards", body.getTableName().get()); + V1InsertRecordData wire = body.getRecords().get().get(0); + Assert.assertEquals("cards", wire.getTableName().get()); + // The user's own map instances must be handed to the wire object untouched. + Assert.assertSame(data, wire.getData().get()); + Assert.assertSame(tokens, wire.getTokens().get()); + Assert.assertEquals(FlowEnumUpdateType.UPDATE, wire.getUpsert().get().getUpdateType().get()); + Assert.assertEquals(Arrays.asList("email", "phone"), wire.getUpsert().get().getUniqueColumns().get()); + } + + @Test + public void testBulkInsert_nonAsciiAndSpacedValues_userValueReachesWire() { + Map data = new LinkedHashMap<>(); + data.put("name", NON_ASCII_NAME); + data.put("street address", "12 東京都 千代田区"); + Map tokens = new LinkedHashMap<>(); + tokens.put("name", "tök-ábc 123"); + + BulkInsertRequestRecord record = BulkInsertRequestRecord.builder() + .tableName(NON_ASCII_TABLE) + .data(data) + .tokens(tokens) + .build(); + BulkInsertRequest request = BulkInsertRequest.builder() + .tableName(SPACED_TABLE) + .records(recordList(record)) + .build(); + + V1InsertRequest body = Utils.getBulkInsertRequestBody(request, vaultConfig()); + + Assert.assertEquals(SPACED_TABLE, body.getTableName().get()); + V1InsertRecordData wire = body.getRecords().get().get(0); + Assert.assertEquals(NON_ASCII_TABLE, wire.getTableName().get()); + Assert.assertEquals(NON_ASCII_NAME, wire.getData().get().get("name")); + Assert.assertEquals("12 東京都 千代田区", wire.getData().get().get("street address")); + Assert.assertEquals("tök-ábc 123", wire.getTokens().get().get("name")); + } + + @Test + public void testBulkInsert_nonStringDataValues_userValueReachesWire() { + Map nested = new LinkedHashMap<>(); + nested.put("city", "Paris"); + nested.put("zip", 75001); + List list = Arrays.asList(1, "two", true); + + Map data = new LinkedHashMap<>(); + data.put("age", 42); + data.put("balance", 1234.56d); + data.put("longValue", 9007199254740993L); + data.put("active", Boolean.TRUE); + data.put("address", nested); + data.put("tags", list); + + BulkInsertRequestRecord record = BulkInsertRequestRecord.builder() + .tableName("cards") + .data(data) + .build(); + BulkInsertRequest request = BulkInsertRequest.builder().records(recordList(record)).build(); + + V1InsertRequest body = Utils.getBulkInsertRequestBody(request, vaultConfig()); + Map wireData = body.getRecords().get().get(0).getData().get(); + + Assert.assertSame(data, wireData); + // Object identity/type of every value survives — no toString()-ing, no boxing changes. + Assert.assertEquals(Integer.valueOf(42), wireData.get("age")); + Assert.assertEquals(Double.valueOf(1234.56d), wireData.get("balance")); + Assert.assertEquals(Long.valueOf(9007199254740993L), wireData.get("longValue")); + Assert.assertSame(Boolean.TRUE, wireData.get("active")); + Assert.assertSame(nested, wireData.get("address")); + Assert.assertSame(list, wireData.get("tags")); + Assert.assertEquals(Integer.valueOf(75001), ((Map) wireData.get("address")).get("zip")); + } + + @Test + public void testBulkInsert_recordOrderPreserved() { + ArrayList records = new ArrayList<>(); + for (int i = 0; i < 7; i++) { + Map data = new HashMap<>(); + data.put("pos", i); + records.add(BulkInsertRequestRecord.builder().tableName("cards").data(data).build()); + } + BulkInsertRequest request = BulkInsertRequest.builder().records(records).build(); + + V1InsertRequest body = Utils.getBulkInsertRequestBody(request, vaultConfig()); + + List wireRecords = body.getRecords().get(); + Assert.assertEquals(7, wireRecords.size()); + for (int i = 0; i < 7; i++) { + Assert.assertEquals(Integer.valueOf(i), wireRecords.get(i).getData().get().get("pos")); + } + } + + @Test + public void testBulkInsert_recordOrderPreservedAcrossBatches() { + int total = 7; + int batchSize = 3; + ArrayList records = new ArrayList<>(); + for (int i = 0; i < total; i++) { + Map data = new HashMap<>(); + data.put("pos", i); + records.add(BulkInsertRequestRecord.builder().tableName("cards").data(data).build()); + } + BulkInsertRequest request = BulkInsertRequest.builder().tableName("cards").records(records).build(); + + V1InsertRequest body = Utils.getBulkInsertRequestBody(request, vaultConfig()); + List> batches = Utils.createBulkInsertBatches(body.getRecords().get(), batchSize); + + Assert.assertEquals(3, batches.size()); + int expected = 0; + for (List batch : batches) { + for (V1InsertRecordData wire : batch) { + Assert.assertEquals(Integer.valueOf(expected), wire.getData().get().get("pos")); + // Non-batched per-record fields survive batching on every batch. + Assert.assertEquals("cards", wire.getTableName().get()); + expected++; + } + } + Assert.assertEquals(total, expected); + } + + @Test + public void testBulkInsert_responseIndexMapsToOriginalInputPosition_acrossBatches() { + int total = 7; + int batchSize = 3; + ArrayList records = new ArrayList<>(); + for (int i = 0; i < total; i++) { + Map data = new HashMap<>(); + data.put("pos", i); + records.add(BulkInsertRequestRecord.builder().tableName("cards").data(data).build()); + } + BulkInsertRequest request = BulkInsertRequest.builder().tableName("cards").records(records).build(); + + V1InsertRequest body = Utils.getBulkInsertRequestBody(request, vaultConfig()); + List> batches = Utils.createBulkInsertBatches(body.getRecords().get(), batchSize); + + // Simulate the server echoing one response record per request record in each batch, then + // assert the SDK-assigned index equals the record's position in the ORIGINAL user list. + List indices = new ArrayList<>(); + List skyflowIds = new ArrayList<>(); + for (int batchNumber = 0; batchNumber < batches.size(); batchNumber++) { + List responseRecords = new ArrayList<>(); + for (V1InsertRecordData wire : batches.get(batchNumber)) { + responseRecords.add(V1RecordResponseObject.builder() + .skyflowId("sky-" + wire.getData().get().get("pos")) + .build()); + } + V1InsertResponse response = V1InsertResponse.builder().records(responseRecords).build(); + BulkInsertResponse formatted = Utils.formatBulkInsertResponse(response, batchNumber, batchSize, new HashMap<>()); + formatted.getRecords().forEach(r -> { + indices.add(r.getIndex()); + skyflowIds.add(r.getSkyflowId()); + }); + } + + Assert.assertEquals(total, indices.size()); + for (int i = 0; i < total; i++) { + Assert.assertEquals(Integer.valueOf(i), indices.get(i)); + Assert.assertEquals("sky-" + i, skyflowIds.get(i)); + } + } + + // ── insert: table-name precedence / fallback regressions ───────────────── + + @Test + public void testBulkInsert_blankRecordTableNameFallsBackToRequestLevel() { + // Regression: a blank record-level table name counts as ABSENT (Utils.hasText), so the + // request-level name must be sent — not the literal whitespace string. + Map data = new HashMap<>(); + data.put("name", "john"); + BulkInsertRequestRecord record = BulkInsertRequestRecord.builder() + .tableName(" ") + .data(data) + .build(); + BulkInsertRequest request = BulkInsertRequest.builder() + .tableName("cards") + .records(recordList(record)) + .build(); + + V1InsertRequest body = Utils.getBulkInsertRequestBody(request, vaultConfig()); + + Assert.assertEquals("cards", body.getRecords().get().get(0).getTableName().get()); + Assert.assertEquals("cards", body.getTableName().get()); + } + + @Test + public void testBulkInsert_nullRecordTableNameFallsBackToRequestLevel() { + Map data = new HashMap<>(); + data.put("name", "john"); + BulkInsertRequestRecord record = BulkInsertRequestRecord.builder().data(data).build(); + BulkInsertRequest request = BulkInsertRequest.builder() + .tableName("cards") + .records(recordList(record)) + .build(); + + V1InsertRequest body = Utils.getBulkInsertRequestBody(request, vaultConfig()); + + Assert.assertEquals("cards", body.getRecords().get().get(0).getTableName().get()); + } + + @Test + public void testBulkInsert_emptyStringRecordTableNameFallsBackToRequestLevel() { + Map data = new HashMap<>(); + data.put("name", "john"); + BulkInsertRequestRecord record = BulkInsertRequestRecord.builder().tableName("").data(data).build(); + BulkInsertRequest request = BulkInsertRequest.builder() + .tableName("cards") + .records(recordList(record)) + .build(); + + V1InsertRequest body = Utils.getBulkInsertRequestBody(request, vaultConfig()); + + Assert.assertEquals("cards", body.getRecords().get().get(0).getTableName().get()); + } + + @Test + public void testBulkInsert_recordTableNameOverridesRequestLevel() { + Map data = new HashMap<>(); + data.put("name", "john"); + BulkInsertRequestRecord record = BulkInsertRequestRecord.builder() + .tableName("record_table") + .data(data) + .build(); + BulkInsertRequest request = BulkInsertRequest.builder() + .tableName("request_table") + .records(recordList(record)) + .build(); + + V1InsertRequest body = Utils.getBulkInsertRequestBody(request, vaultConfig()); + + Assert.assertEquals("record_table", body.getRecords().get().get(0).getTableName().get()); + // The request-level name still goes out on the envelope, untouched. + Assert.assertEquals("request_table", body.getTableName().get()); + } + + @Test + public void testBulkInsert_perRecordTableNamesResolveIndependently() { + Map data = new HashMap<>(); + data.put("name", "john"); + BulkInsertRequestRecord withOwn = BulkInsertRequestRecord.builder().tableName("own").data(data).build(); + BulkInsertRequestRecord blank = BulkInsertRequestRecord.builder().tableName(" ").data(data).build(); + BulkInsertRequestRecord missing = BulkInsertRequestRecord.builder().data(data).build(); + BulkInsertRequest request = BulkInsertRequest.builder() + .tableName("fallback") + .records(recordList(withOwn, blank, missing)) + .build(); + + V1InsertRequest body = Utils.getBulkInsertRequestBody(request, vaultConfig()); + + Assert.assertEquals("own", body.getRecords().get().get(0).getTableName().get()); + Assert.assertEquals("fallback", body.getRecords().get().get(1).getTableName().get()); + Assert.assertEquals("fallback", body.getRecords().get().get(2).getTableName().get()); + } + + @Test + public void testBulkInsert_blankRequestLevelTableNameIsOmittedFromEnvelope() { + Map data = new HashMap<>(); + data.put("name", "john"); + BulkInsertRequestRecord record = BulkInsertRequestRecord.builder().tableName("cards").data(data).build(); + BulkInsertRequest request = BulkInsertRequest.builder() + .tableName(" ") + .records(recordList(record)) + .build(); + + V1InsertRequest body = Utils.getBulkInsertRequestBody(request, vaultConfig()); + + Assert.assertFalse(body.getTableName().isPresent()); + Assert.assertEquals("cards", body.getRecords().get().get(0).getTableName().get()); + } + + // ── insert: upsert mapping ─────────────────────────────────────────────── + + @Test + public void testUpsert_updateTypeUpdateAndReplace_userValueReachesWire() { + Assert.assertEquals(FlowEnumUpdateType.UPDATE, upsertWire("UPDATE").getUpdateType().get()); + Assert.assertEquals(FlowEnumUpdateType.REPLACE, upsertWire("REPLACE").getUpdateType().get()); + } + + @Test + public void testUpsert_updateTypeIsMatchedCaseInsensitively() { + Assert.assertEquals(FlowEnumUpdateType.UPDATE, upsertWire("update").getUpdateType().get()); + Assert.assertEquals(FlowEnumUpdateType.UPDATE, upsertWire("UpDaTe").getUpdateType().get()); + Assert.assertEquals(FlowEnumUpdateType.REPLACE, upsertWire("replace").getUpdateType().get()); + Assert.assertEquals(FlowEnumUpdateType.REPLACE, upsertWire("Replace").getUpdateType().get()); + } + + @Test + public void testUpsert_unrecognizedUpdateTypeIsRejectedBeforeMapping() { + // Validations.validateUpsertOptions now rejects anything that is not UPDATE/REPLACE, so + // the mapper can no longer be reached with a value it would silently drop. A null + // updateType stays legal and simply omits the field. + com.skyflow.generated.rest.types.V1Upsert nullType = upsertWire(null); + Assert.assertFalse(nullType.getUpdateType().isPresent()); + Assert.assertEquals(Collections.singletonList("email"), nullType.getUniqueColumns().get()); + } + + @Test + public void testUpsert_uniqueColumnsValuesAndOrderReachWire() { + List uniqueColumns = Arrays.asList("email", "phone number", NON_ASCII_NAME); + Map data = new HashMap<>(); + data.put("name", "john"); + BulkInsertRequestRecord record = BulkInsertRequestRecord.builder() + .tableName("cards") + .data(data) + .upsert(UpsertOptions.builder().updateType("UPDATE").uniqueColumns(uniqueColumns).build()) + .build(); + BulkInsertRequest request = BulkInsertRequest.builder().records(recordList(record)).build(); + + V1InsertRequest body = Utils.getBulkInsertRequestBody(request, vaultConfig()); + + List wireColumns = body.getRecords().get().get(0).getUpsert().get().getUniqueColumns().get(); + Assert.assertSame(uniqueColumns, wireColumns); + Assert.assertEquals(Arrays.asList("email", "phone number", NON_ASCII_NAME), wireColumns); + } + + @Test + public void testUpsert_recordLevelOverridesRequestLevel() { + Map data = new HashMap<>(); + data.put("name", "john"); + BulkInsertRequestRecord record = BulkInsertRequestRecord.builder() + .tableName("cards") + .data(data) + .upsert(UpsertOptions.builder() + .updateType("REPLACE") + .uniqueColumns(Collections.singletonList("record_col")) + .build()) + .build(); + BulkInsertRequest request = BulkInsertRequest.builder() + .records(recordList(record)) + .upsert(UpsertOptions.builder() + .updateType("UPDATE") + .uniqueColumns(Collections.singletonList("request_col")) + .build()) + .build(); + + V1InsertRequest body = Utils.getBulkInsertRequestBody(request, vaultConfig()); + + com.skyflow.generated.rest.types.V1Upsert wire = body.getRecords().get().get(0).getUpsert().get(); + Assert.assertEquals(FlowEnumUpdateType.REPLACE, wire.getUpdateType().get()); + Assert.assertEquals(Collections.singletonList("record_col"), wire.getUniqueColumns().get()); + } + + @Test + public void testUpsert_requestLevelAppliesToEveryRecord() { + Map data = new HashMap<>(); + data.put("name", "john"); + BulkInsertRequest request = BulkInsertRequest.builder() + .tableName("cards") + .records(recordList( + BulkInsertRequestRecord.builder().data(data).build(), + BulkInsertRequestRecord.builder().data(data).build(), + BulkInsertRequestRecord.builder().data(data).build())) + .upsert(UpsertOptions.builder() + .updateType("UPDATE") + .uniqueColumns(Collections.singletonList("email")) + .build()) + .build(); + + V1InsertRequest body = Utils.getBulkInsertRequestBody(request, vaultConfig()); + + for (V1InsertRecordData wire : body.getRecords().get()) { + Assert.assertTrue(wire.getUpsert().isPresent()); + Assert.assertEquals(FlowEnumUpdateType.UPDATE, wire.getUpsert().get().getUpdateType().get()); + Assert.assertEquals(Collections.singletonList("email"), wire.getUpsert().get().getUniqueColumns().get()); + } + } + + @Test + public void testUpsert_requestLevelUpsertNeverReachesEnvelope_knownGap() { + // KNOWN GAP: Utils.getInsertRequestBody projects the request-level upsert onto each + // record but never sets it on the V1InsertRequest envelope. VaultController#insertBatchFutures + // reads insertRequest.getUpsert() to re-apply it to every batch request, so that read is + // always empty. Harmless today (each record already carries it), but the envelope-level + // upsert is dead wiring. + Map data = new HashMap<>(); + data.put("name", "john"); + BulkInsertRequest request = BulkInsertRequest.builder() + .tableName("cards") + .records(recordList(BulkInsertRequestRecord.builder().data(data).build())) + .upsert(UpsertOptions.builder() + .updateType("UPDATE") + .uniqueColumns(Collections.singletonList("email")) + .build()) + .build(); + + V1InsertRequest body = Utils.getBulkInsertRequestBody(request, vaultConfig()); + + Assert.assertFalse(body.getUpsert().isPresent()); + Assert.assertTrue(body.getRecords().get().get(0).getUpsert().isPresent()); + } + + @Test + public void testUpsert_emptyUniqueColumnsMeansNoUpsertOnWire() { + Map data = new HashMap<>(); + data.put("name", "john"); + BulkInsertRequestRecord record = BulkInsertRequestRecord.builder() + .tableName("cards") + .data(data) + .upsert(UpsertOptions.builder() + .updateType("UPDATE") + .uniqueColumns(new ArrayList<>()) + .build()) + .build(); + BulkInsertRequest request = BulkInsertRequest.builder().records(recordList(record)).build(); + + V1InsertRequest body = Utils.getBulkInsertRequestBody(request, vaultConfig()); + + Assert.assertFalse(body.getRecords().get().get(0).getUpsert().isPresent()); + } + + private static com.skyflow.generated.rest.types.V1Upsert upsertWire(String updateType) { + Map data = new HashMap<>(); + data.put("name", "john"); + BulkInsertRequestRecord record = BulkInsertRequestRecord.builder() + .tableName("cards") + .data(data) + .upsert(UpsertOptions.builder() + .updateType(updateType) + .uniqueColumns(Collections.singletonList("email")) + .build()) + .build(); + BulkInsertRequest request = BulkInsertRequest.builder().records(recordList(record)).build(); + return Utils.getBulkInsertRequestBody(request, vaultConfig()) + .getRecords().get().get(0).getUpsert().get(); + } + + // ── insert: tokens map ─────────────────────────────────────────────────── + + @Test + public void testBulkInsert_emptyTokensMapIsOmittedFromWire_knownGap() { + // KNOWN GAP: an explicitly-set-but-empty tokens map is dropped rather than sent as {}. + // Pinning current behavior — a caller cannot distinguish "no tokens" from "empty tokens". + Map data = new HashMap<>(); + data.put("name", "john"); + BulkInsertRequestRecord record = BulkInsertRequestRecord.builder() + .tableName("cards") + .data(data) + .tokens(new HashMap<>()) + .build(); + BulkInsertRequest request = BulkInsertRequest.builder().records(recordList(record)).build(); + + V1InsertRequest body = Utils.getBulkInsertRequestBody(request, vaultConfig()); + + Assert.assertFalse(body.getRecords().get().get(0).getTokens().isPresent()); + } + + @Test + public void testBulkInsert_nullTokensMapIsOmittedFromWire() { + Map data = new HashMap<>(); + data.put("name", "john"); + BulkInsertRequestRecord record = BulkInsertRequestRecord.builder() + .tableName("cards") + .data(data) + .build(); + BulkInsertRequest request = BulkInsertRequest.builder().records(recordList(record)).build(); + + V1InsertRequest body = Utils.getBulkInsertRequestBody(request, vaultConfig()); + + Assert.assertFalse(body.getRecords().get().get(0).getTokens().isPresent()); + } + + @Test + public void testBulkInsert_multiValueTokensMapReachesWireVerbatim() { + Map data = new HashMap<>(); + data.put("name", "john"); + Map tokens = new LinkedHashMap<>(); + tokens.put("name", "tok-1"); + tokens.put("ssn", "tok-2"); + tokens.put("nested", Collections.singletonMap("group", "tok-3")); + BulkInsertRequestRecord record = BulkInsertRequestRecord.builder() + .tableName("cards") + .data(data) + .tokens(tokens) + .build(); + BulkInsertRequest request = BulkInsertRequest.builder().records(recordList(record)).build(); + + V1InsertRequest body = Utils.getBulkInsertRequestBody(request, vaultConfig()); + + Map wireTokens = body.getRecords().get().get(0).getTokens().get(); + Assert.assertSame(tokens, wireTokens); + Assert.assertEquals("tok-1", wireTokens.get("name")); + Assert.assertEquals("tok-2", wireTokens.get("ssn")); + Assert.assertEquals(Collections.singletonMap("group", "tok-3"), wireTokens.get("nested")); + } + + // ───────────────────────────────────────────────────────────────────────── + // Bulk detokenize + // ───────────────────────────────────────────────────────────────────────── + + @Test + public void testBulkDetokenize_everyFieldReachesWire() { + List tokens = Arrays.asList("token-1", "token 2", "トークン-3"); + TokenGroupRedactions groupA = TokenGroupRedactions.builder() + .tokenGroupName("group one") + .redaction("MASKED") + .build(); + TokenGroupRedactions groupB = TokenGroupRedactions.builder() + .tokenGroupName(NON_ASCII_NAME) + .redaction("PLAIN_TEXT") + .build(); + BulkDetokenizeRequest request = BulkDetokenizeRequest.builder() + .tokens(tokens) + .tokenGroupRedactions(Arrays.asList(groupA, groupB)) + .build(); + + V1FlowDetokenizeRequest body = Utils.getBulkDetokenizeRequestBody(request, VAULT_ID); + + Assert.assertEquals(VAULT_ID, body.getVaultId().get()); + Assert.assertSame(tokens, body.getTokens().get()); + Assert.assertEquals(Arrays.asList("token-1", "token 2", "トークン-3"), body.getTokens().get()); + + List wireGroups = body.getTokenGroupRedactions().get(); + Assert.assertEquals(2, wireGroups.size()); + Assert.assertEquals("group one", wireGroups.get(0).getTokenGroupName().get()); + Assert.assertEquals("MASKED", wireGroups.get(0).getRedaction().get()); + Assert.assertEquals(NON_ASCII_NAME, wireGroups.get(1).getTokenGroupName().get()); + Assert.assertEquals("PLAIN_TEXT", wireGroups.get(1).getRedaction().get()); + } + + @Test + public void testBulkDetokenize_emptyTokenGroupRedactionsListIsOmitted() { + BulkDetokenizeRequest request = BulkDetokenizeRequest.builder() + .tokens(Collections.singletonList("token-1")) + .tokenGroupRedactions(new ArrayList<>()) + .build(); + + V1FlowDetokenizeRequest body = Utils.getBulkDetokenizeRequestBody(request, VAULT_ID); + + Assert.assertFalse(body.getTokenGroupRedactions().isPresent()); + } + + @Test + public void testBulkDetokenize_tokenOrderPreservedAcrossBatches() { + int total = 7; + int batchSize = 3; + List tokens = new ArrayList<>(); + for (int i = 0; i < total; i++) { + tokens.add("token-" + i); + } + BulkDetokenizeRequest request = BulkDetokenizeRequest.builder().tokens(tokens).build(); + + V1FlowDetokenizeRequest body = Utils.getBulkDetokenizeRequestBody(request, VAULT_ID); + List batches = Utils.createBulkDetokenizeBatches(body, batchSize); + + Assert.assertEquals(3, batches.size()); + List flattened = new ArrayList<>(); + for (V1FlowDetokenizeRequest batch : batches) { + flattened.addAll(batch.getTokens().get()); + } + Assert.assertEquals(tokens, flattened); + } + + @Test + public void testBulkDetokenize_vaultIdAndRedactionsPresentOnEveryBatch() { + int total = 7; + int batchSize = 3; + List tokens = new ArrayList<>(); + for (int i = 0; i < total; i++) { + tokens.add("token-" + i); + } + TokenGroupRedactions group = TokenGroupRedactions.builder() + .tokenGroupName("group one") + .redaction("MASKED") + .build(); + BulkDetokenizeRequest request = BulkDetokenizeRequest.builder() + .tokens(tokens) + .tokenGroupRedactions(Collections.singletonList(group)) + .build(); + + V1FlowDetokenizeRequest body = Utils.getBulkDetokenizeRequestBody(request, VAULT_ID); + List batches = Utils.createBulkDetokenizeBatches(body, batchSize); + + Assert.assertEquals(3, batches.size()); + for (V1FlowDetokenizeRequest batch : batches) { + Assert.assertEquals(VAULT_ID, batch.getVaultId().get()); + Assert.assertTrue(batch.getTokenGroupRedactions().isPresent()); + Assert.assertEquals(1, batch.getTokenGroupRedactions().get().size()); + Assert.assertEquals("group one", batch.getTokenGroupRedactions().get().get(0).getTokenGroupName().get()); + Assert.assertEquals("MASKED", batch.getTokenGroupRedactions().get().get(0).getRedaction().get()); + } + } + + @Test + public void testBulkDetokenize_responseIndexMapsToOriginalInputPosition_acrossBatches() { + int total = 7; + int batchSize = 3; + List tokens = new ArrayList<>(); + for (int i = 0; i < total; i++) { + tokens.add("token-" + i); + } + BulkDetokenizeRequest request = BulkDetokenizeRequest.builder().tokens(tokens).build(); + V1FlowDetokenizeRequest body = Utils.getBulkDetokenizeRequestBody(request, VAULT_ID); + List batches = Utils.createBulkDetokenizeBatches(body, batchSize); + + List indices = new ArrayList<>(); + List echoedTokens = new ArrayList<>(); + for (int batchNumber = 0; batchNumber < batches.size(); batchNumber++) { + List responseRecords = new ArrayList<>(); + for (String token : batches.get(batchNumber).getTokens().get()) { + responseRecords.add(V1FlowDetokenizeResponseObject.builder().token(token).build()); + } + V1FlowDetokenizeResponse response = V1FlowDetokenizeResponse.builder().response(responseRecords).build(); + BulkDetokenizeResponse formatted = + Utils.formatBulkDetokenizeResponse(response, batchNumber, batchSize, new HashMap<>()); + formatted.getRecords().forEach(r -> { + indices.add(r.getIndex()); + echoedTokens.add(r.getToken()); + }); + } + + Assert.assertEquals(total, indices.size()); + for (int i = 0; i < total; i++) { + Assert.assertEquals(Integer.valueOf(i), indices.get(i)); + Assert.assertEquals(tokens.get(i), echoedTokens.get(i)); + } + } + + // ───────────────────────────────────────────────────────────────────────── + // Bulk tokenize + // ───────────────────────────────────────────────────────────────────────── + + @Test + public void testBulkTokenize_everyFieldReachesWire() { + List groupNames = Arrays.asList("group one", NON_ASCII_NAME); + BulkTokenizeRecord record = BulkTokenizeRecord.builder() + .value(NON_ASCII_NAME) + .tokenGroupNames(groupNames) + .build(); + ArrayList data = new ArrayList<>(Collections.singletonList(record)); + BulkTokenizeRequest request = BulkTokenizeRequest.builder().data(data).build(); + + V1FlowTokenizeRequest body = Utils.getBulkTokenizeRequestBody(request, VAULT_ID); + + Assert.assertEquals(VAULT_ID, body.getVaultId().get()); + Assert.assertEquals(1, body.getData().get().size()); + V1FlowTokenizeRequestObject wire = body.getData().get().get(0); + Assert.assertEquals(NON_ASCII_NAME, wire.getValue().get()); + Assert.assertSame(groupNames, wire.getTokenGroupNames().get()); + Assert.assertEquals(Arrays.asList("group one", NON_ASCII_NAME), wire.getTokenGroupNames().get()); + } + + @Test + public void testBulkTokenize_nonStringValues_objectIdentitySurvives() { + Map nested = new LinkedHashMap<>(); + nested.put("city", "Paris"); + nested.put("zip", 75001); + List listValue = Arrays.asList(1, 2, 3); + + ArrayList data = new ArrayList<>(Arrays.asList( + BulkTokenizeRecord.builder().value(42).build(), + BulkTokenizeRecord.builder().value(3.14d).build(), + BulkTokenizeRecord.builder().value(Boolean.FALSE).build(), + BulkTokenizeRecord.builder().value(nested).build(), + BulkTokenizeRecord.builder().value(listValue).build())); + BulkTokenizeRequest request = BulkTokenizeRequest.builder().data(data).build(); + + V1FlowTokenizeRequest body = Utils.getBulkTokenizeRequestBody(request, VAULT_ID); + List wire = body.getData().get(); + + Assert.assertEquals(Integer.valueOf(42), wire.get(0).getValue().get()); + Assert.assertEquals(Double.valueOf(3.14d), wire.get(1).getValue().get()); + Assert.assertSame(Boolean.FALSE, wire.get(2).getValue().get()); + Assert.assertSame(nested, wire.get(3).getValue().get()); + Assert.assertSame(listValue, wire.get(4).getValue().get()); + } + + @Test + public void testBulkTokenize_nullTokenGroupNamesIsOmitted() { + ArrayList data = new ArrayList<>( + Collections.singletonList(BulkTokenizeRecord.builder().value("v1").build())); + BulkTokenizeRequest request = BulkTokenizeRequest.builder().data(data).build(); + + V1FlowTokenizeRequest body = Utils.getBulkTokenizeRequestBody(request, VAULT_ID); + + Assert.assertFalse(body.getData().get().get(0).getTokenGroupNames().isPresent()); + } + + @Test + public void testBulkTokenize_recordOrderPreservedAcrossBatches() { + int total = 7; + int batchSize = 3; + ArrayList data = new ArrayList<>(); + for (int i = 0; i < total; i++) { + data.add(BulkTokenizeRecord.builder().value("value-" + i).build()); + } + BulkTokenizeRequest request = BulkTokenizeRequest.builder().data(data).build(); + + V1FlowTokenizeRequest body = Utils.getBulkTokenizeRequestBody(request, VAULT_ID); + List batches = Utils.createBulkTokenizeBatches(body, batchSize); + + Assert.assertEquals(3, batches.size()); + List flattened = new ArrayList<>(); + for (V1FlowTokenizeRequest batch : batches) { + // vaultId is a non-batched field and must be re-applied on every batch. + Assert.assertEquals(VAULT_ID, batch.getVaultId().get()); + for (V1FlowTokenizeRequestObject obj : batch.getData().get()) { + flattened.add(obj.getValue().get()); + } + } + Assert.assertEquals(total, flattened.size()); + for (int i = 0; i < total; i++) { + Assert.assertEquals("value-" + i, flattened.get(i)); + } + } + + // ───────────────────────────────────────────────────────────────────────── + // Bulk delete tokens + // ───────────────────────────────────────────────────────────────────────── + + @Test + public void testBulkDeleteTokens_everyFieldReachesWire() { + List tokens = Arrays.asList("token-1", "token 2", "トークン-3"); + BulkDeleteTokensRequest request = BulkDeleteTokensRequest.builder().tokens(tokens).build(); + + V1FlowDeleteTokenRequest body = Utils.getBulkDeleteTokensRequestBody(request, VAULT_ID); + + Assert.assertEquals(VAULT_ID, body.getVaultId().get()); + Assert.assertSame(tokens, body.getTokens().get()); + Assert.assertEquals(Arrays.asList("token-1", "token 2", "トークン-3"), body.getTokens().get()); + } + + @Test + public void testBulkDeleteTokens_tokenOrderPreservedAndVaultIdOnEveryBatch() { + int total = 7; + int batchSize = 3; + List tokens = new ArrayList<>(); + for (int i = 0; i < total; i++) { + tokens.add("token-" + i); + } + BulkDeleteTokensRequest request = BulkDeleteTokensRequest.builder().tokens(tokens).build(); + + V1FlowDeleteTokenRequest body = Utils.getBulkDeleteTokensRequestBody(request, VAULT_ID); + List batches = Utils.createBulkDeleteTokensBatches(body, batchSize); + + Assert.assertEquals(3, batches.size()); + List flattened = new ArrayList<>(); + for (V1FlowDeleteTokenRequest batch : batches) { + Assert.assertEquals(VAULT_ID, batch.getVaultId().get()); + flattened.addAll(batch.getTokens().get()); + } + Assert.assertEquals(tokens, flattened); + } + + @Test + public void testBulkDeleteTokens_responseIndexMapsToOriginalInputPosition_acrossBatches() { + int total = 7; + int batchSize = 3; + List tokens = new ArrayList<>(); + for (int i = 0; i < total; i++) { + tokens.add("token-" + i); + } + BulkDeleteTokensRequest request = BulkDeleteTokensRequest.builder().tokens(tokens).build(); + V1FlowDeleteTokenRequest body = Utils.getBulkDeleteTokensRequestBody(request, VAULT_ID); + List batches = Utils.createBulkDeleteTokensBatches(body, batchSize); + + List indices = new ArrayList<>(); + List echoed = new ArrayList<>(); + for (int batchNumber = 0; batchNumber < batches.size(); batchNumber++) { + List responseRecords = new ArrayList<>(); + for (String token : batches.get(batchNumber).getTokens().get()) { + responseRecords.add(V1DeleteTokenResponseObject.builder().value(token).build()); + } + V1FlowDeleteTokenResponse response = + V1FlowDeleteTokenResponse.builder().tokens(responseRecords).build(); + BulkDeleteTokensResponse formatted = + Utils.formatBulkDeleteTokensResponse(response, batchNumber, batchSize, new HashMap<>()); + formatted.getSuccess().forEach(s -> { + indices.add(s.getIndex()); + echoed.add(s.getToken()); + }); + } + + Assert.assertEquals(total, indices.size()); + for (int i = 0; i < total; i++) { + Assert.assertEquals(Integer.valueOf(i), indices.get(i)); + Assert.assertEquals(tokens.get(i), echoed.get(i)); + } + } +} diff --git a/flowvault/src/test/java/com/skyflow/utils/UtilsTests.java b/flowvault/src/test/java/com/skyflow/utils/UtilsTests.java index dbfdfbf4..d2624bbc 100644 --- a/flowvault/src/test/java/com/skyflow/utils/UtilsTests.java +++ b/flowvault/src/test/java/com/skyflow/utils/UtilsTests.java @@ -4,7 +4,6 @@ import com.skyflow.config.Credentials; import com.skyflow.config.VaultConfig; import com.skyflow.enums.Env; -import com.skyflow.enums.UpsertType; import com.skyflow.errors.SkyflowException; import com.skyflow.generated.rest.core.ApiClientApiException; import com.skyflow.generated.rest.resources.flowservice.requests.V1FlowDeleteTokenRequest; @@ -26,28 +25,19 @@ import com.skyflow.vault.data.BulkDeleteTokensResponse; import com.skyflow.vault.data.BulkDetokenizeRequest; import com.skyflow.vault.data.BulkDetokenizeResponse; -import com.skyflow.vault.data.BulkInsertRecord; +import com.skyflow.vault.data.BulkDetokenizeResponseRecord; +import com.skyflow.vault.data.BulkInsertRequestRecord; import com.skyflow.vault.data.BulkInsertRequest; import com.skyflow.vault.data.BulkInsertResponse; -import com.skyflow.vault.data.BulkTokenGroupRedactions; +import com.skyflow.vault.data.BulkInsertResponseRecord; import com.skyflow.vault.data.BulkTokenizeRecord; import com.skyflow.vault.data.BulkTokenizeRequest; import com.skyflow.vault.data.BulkTokenizeResponse; -import com.skyflow.vault.data.DeleteTokensRequest; -import com.skyflow.vault.data.DeleteTokensResponse; -import com.skyflow.vault.data.DetokenizeData; -import com.skyflow.vault.data.DetokenizeRecordResponse; -import com.skyflow.vault.data.DetokenizeRequest; -import com.skyflow.vault.data.DetokenizeResponse; import com.skyflow.vault.data.ErrorRecord; -import com.skyflow.vault.data.InsertRecord; +import com.skyflow.vault.data.InsertRequestRecord; import com.skyflow.vault.data.InsertRequest; -import com.skyflow.vault.data.InsertResponse; import com.skyflow.vault.data.TokenGroupRedactions; -import com.skyflow.vault.data.TokenizeData; -import com.skyflow.vault.data.TokenizeRecord; -import com.skyflow.vault.data.TokenizeRequest; -import com.skyflow.vault.data.TokenizeResponse; +import com.skyflow.vault.data.UpsertOptions; import org.junit.After; import org.junit.Assert; import org.junit.Before; @@ -85,29 +75,29 @@ public void restoreEnvFile() throws IOException { } } - // ── getVaultURL ─────────────────────────────────────────────────────────── + // ── getVaultUrl ─────────────────────────────────────────────────────────── @Test public void testGetVaultURL_prodEnv() { - String url = Utils.getVaultURL("cluster1", Env.PROD); + String url = Utils.getVaultUrl("cluster1", Env.PROD); Assert.assertEquals("https://cluster1.skyvault.skyflowapis.com", url); } @Test public void testGetVaultURL_devEnv() { - String url = Utils.getVaultURL("cluster1", Env.DEV); + String url = Utils.getVaultUrl("cluster1", Env.DEV); Assert.assertEquals("https://cluster1.skyvault.skyflowapis.dev", url); } @Test public void testGetVaultURL_stageEnv() { - String url = Utils.getVaultURL("cluster1", Env.STAGE); + String url = Utils.getVaultUrl("cluster1", Env.STAGE); Assert.assertEquals("https://cluster1.skyvault.skyflowapis.tech", url); } @Test public void testGetVaultURL_sandboxEnv() { - String url = Utils.getVaultURL("cluster1", Env.SANDBOX); + String url = Utils.getVaultUrl("cluster1", Env.SANDBOX); Assert.assertEquals("https://cluster1.skyvault.skyflowapis-preview.com", url); } @@ -121,12 +111,12 @@ public void testGetMetrics_containsSdkVersion() { Assert.assertTrue(sdkVersionMetric.startsWith(Constants.SDK_METRIC_NAME_VERSION_PREFIX)); } - // ── getEnvVaultURL ──────────────────────────────────────────────────────── + // ── getEnvVaultUrl ──────────────────────────────────────────────────────── @Test public void testGetEnvVaultURL_doesNotThrowUnexpectedException() { try { - Utils.getEnvVaultURL(); + Utils.getEnvVaultUrl(); } catch (SkyflowException e) { // acceptable if this environment happens to have an invalid VAULT_URL set Assert.assertNotNull(e.getMessage()); @@ -142,7 +132,7 @@ public void testGetEnvVaultURL_emptyValueThrowsEmptyVaultUrl() throws Exception } try { - Utils.getEnvVaultURL(); + Utils.getEnvVaultUrl(); Assert.fail("Should have thrown SkyflowException for empty VAULT_URL"); } catch (SkyflowException e) { Assert.assertEquals(com.skyflow.errors.ErrorMessage.EmptyVaultUrl.getMessage(), e.getMessage()); @@ -156,33 +146,33 @@ public void testGetEnvVaultURL_invalidFormatThrowsInvalidVaultUrlFormat() throws } try { - Utils.getEnvVaultURL(); + Utils.getEnvVaultUrl(); Assert.fail("Should have thrown SkyflowException for invalid VAULT_URL format"); } catch (SkyflowException e) { Assert.assertEquals(com.skyflow.errors.ErrorMessage.InvalidVaultUrlFormat.getMessage(), e.getMessage()); } } - // ── isValidURL ──────────────────────────────────────────────────────────── + // ── isValidUrl ──────────────────────────────────────────────────────────── @Test public void testIsValidURL_validHttpsUrl() { - Assert.assertTrue(Utils.isValidURL("https://example.com")); + Assert.assertTrue(Utils.isValidUrl("https://example.com")); } @Test public void testIsValidURL_httpUrlIsInvalid() { - Assert.assertFalse(Utils.isValidURL("http://example.com")); + Assert.assertFalse(Utils.isValidUrl("http://example.com")); } @Test public void testIsValidURL_malformedUrl() { - Assert.assertFalse(Utils.isValidURL("not a url")); + Assert.assertFalse(Utils.isValidUrl("not a url")); } @Test public void testIsValidURL_httpsUrlWithEmptyHostIsInvalid() { - Assert.assertFalse(Utils.isValidURL("https:///path")); + Assert.assertFalse(Utils.isValidUrl("https:///path")); } // ── generateBearerToken ─────────────────────────────────────────────────── @@ -223,423 +213,116 @@ public void testGenerateBearerToken_withNonExistentPathThrows() { } } - // ── getBulkInsertRequestBody ────────────────────────────────────────────── + // ── getInsertRequestBody (unary) ────────────────────────────────────────── @Test - public void testGetBulkInsertRequestBody_buildsCorrectRequest() { + public void testGetInsertRequestBody_buildsCorrectRequest() { Map data = new HashMap<>(); data.put("name", "john"); - InsertRecord record = InsertRecord.builder().data(data).build(); - ArrayList records = new ArrayList<>(); + InsertRequestRecord record = InsertRequestRecord.builder().tableName("table1").data(data).build(); + ArrayList records = new ArrayList<>(); records.add(record); - InsertRequest request = InsertRequest.builder().table("table1").records(records).build(); + InsertRequest request = InsertRequest.builder().records(records).build(); VaultConfig config = new VaultConfig(); config.setVaultId("vault123"); - V1InsertRequest body = Utils.getBulkInsertRequestBody(request, config); + V1InsertRequest body = Utils.getInsertRequestBody(request, config); Assert.assertEquals("vault123", body.getVaultId().get()); - Assert.assertEquals("table1", body.getTableName().get()); Assert.assertEquals(1, body.getRecords().get().size()); + Assert.assertEquals("table1", body.getRecords().get().get(0).getTableName().get()); Assert.assertEquals(data, body.getRecords().get().get(0).getData().get()); } @Test - public void testGetBulkInsertRequestBody_withUpsertAtRequestLevel() { + public void testGetInsertRequestBody_fallsBackToRequestLevelTableName() { Map data = new HashMap<>(); data.put("name", "john"); - InsertRecord record = InsertRecord.builder().data(data).build(); - ArrayList records = new ArrayList<>(); + InsertRequestRecord record = InsertRequestRecord.builder().data(data).build(); + ArrayList records = new ArrayList<>(); records.add(record); - InsertRequest request = InsertRequest.builder() - .table("table1") - .records(records) - .upsert(Collections.singletonList("email")) - .upsertType(UpsertType.UPDATE) - .build(); + InsertRequest request = InsertRequest.builder().tableName("table1").records(records).build(); VaultConfig config = new VaultConfig(); config.setVaultId("vault123"); - V1InsertRequest body = Utils.getBulkInsertRequestBody(request, config); + V1InsertRequest body = Utils.getInsertRequestBody(request, config); - Assert.assertTrue(body.getUpsert().isPresent()); - Assert.assertEquals(Collections.singletonList("email"), body.getUpsert().get().getUniqueColumns().get()); - Assert.assertEquals(FlowEnumUpdateType.UPDATE, body.getUpsert().get().getUpdateType().get()); + Assert.assertEquals("table1", body.getTableName().get()); + Assert.assertEquals("table1", body.getRecords().get().get(0).getTableName().get()); } @Test - public void testGetBulkInsertRequestBody_withUpsertAtRecordLevel() { + public void testGetInsertRequestBody_withTokens() { Map data = new HashMap<>(); data.put("name", "john"); - InsertRecord record = InsertRecord.builder() - .table("table1") - .data(data) - .upsert(Collections.singletonList("email")) - .upsertType(UpsertType.REPLACE) - .build(); - ArrayList records = new ArrayList<>(); + Map tokens = new HashMap<>(); + tokens.put("name", "tok-abc"); + InsertRequestRecord record = InsertRequestRecord.builder().tableName("table1").data(data).tokens(tokens).build(); + ArrayList records = new ArrayList<>(); records.add(record); InsertRequest request = InsertRequest.builder().records(records).build(); VaultConfig config = new VaultConfig(); config.setVaultId("vault123"); - V1InsertRequest body = Utils.getBulkInsertRequestBody(request, config); - - Assert.assertEquals("table1", body.getRecords().get().get(0).getTableName().get()); - Assert.assertTrue(body.getRecords().get().get(0).getUpsert().isPresent()); - Assert.assertEquals(FlowEnumUpdateType.REPLACE, body.getRecords().get().get(0).getUpsert().get().getUpdateType().get()); - } - - // ── buildInsertResponse ─────────────────────────────────────────────────── - - @Test - public void testBuildInsertResponse_success() { - Map tokens = new HashMap<>(); - tokens.put("name", "tok-abc"); - V1RecordResponseObject record = V1RecordResponseObject.builder() - .skyflowId("sky-id-1") - .tokens(tokens) - .build(); - V1InsertResponse res = V1InsertResponse.builder().records(Collections.singletonList(record)).build(); - - InsertResponse response = Utils.buildInsertResponse(res); - - Assert.assertEquals(1, response.getInsertedFields().size()); - Assert.assertEquals("sky-id-1", response.getInsertedFields().get(0).get("skyflowId")); - Assert.assertEquals("tok-abc", response.getInsertedFields().get(0).get("name")); - Assert.assertTrue(response.getErrors().isEmpty()); - } - - @Test - public void testBuildInsertResponse_error() { - V1RecordResponseObject record = V1RecordResponseObject.builder() - .skyflowId("sky-id-1") - .tableName("table1") - .error("insert failed") - .httpCode(400) - .build(); - V1InsertResponse res = V1InsertResponse.builder().records(Collections.singletonList(record)).build(); - - InsertResponse response = Utils.buildInsertResponse(res); - - Assert.assertTrue(response.getInsertedFields().isEmpty()); - Assert.assertEquals(1, response.getErrors().size()); - Assert.assertEquals("insert failed", response.getErrors().get(0).get("error")); - Assert.assertEquals("table1", response.getErrors().get(0).get("tableName")); - Assert.assertEquals(400, response.getErrors().get(0).get("httpCode")); - } - - @Test - public void testBuildInsertResponse_nullResponse() { - InsertResponse response = Utils.buildInsertResponse(null); - Assert.assertTrue(response.getInsertedFields().isEmpty()); - Assert.assertTrue(response.getErrors().isEmpty()); - } - - @Test - public void testBuildInsertResponse_emptyResponse() { - V1InsertResponse res = V1InsertResponse.builder().build(); - InsertResponse response = Utils.buildInsertResponse(res); - Assert.assertTrue(response.getInsertedFields().isEmpty()); - Assert.assertTrue(response.getErrors().isEmpty()); - } - - // ── getDetokenizeRequestBody ────────────────────────────────────────────── - - @Test - public void testGetDetokenizeRequestBody_buildsCorrectRequest() { - ArrayList data = new ArrayList<>(); - data.add(new DetokenizeData("token1")); - DetokenizeRequest request = DetokenizeRequest.builder().detokenizeData(data).build(); - - V1FlowDetokenizeRequest body = Utils.getDetokenizeRequestBody(request, "vault123"); - - Assert.assertEquals("vault123", body.getVaultId().get()); - Assert.assertEquals(Collections.singletonList("token1"), body.getTokens().get()); - Assert.assertFalse(body.getTokenGroupRedactions().isPresent()); - } - - @Test - public void testGetDetokenizeRequestBody_withTokenGroupRedactions() { - ArrayList data = new ArrayList<>(); - data.add(new DetokenizeData("token1")); - List groupRedactions = Collections.singletonList( - TokenGroupRedactions.builder().tokenGroupName("group1").redaction("MASKED").build()); - DetokenizeRequest request = DetokenizeRequest.builder() - .detokenizeData(data) - .tokenGroupRedactions(groupRedactions) - .build(); - - V1FlowDetokenizeRequest body = Utils.getDetokenizeRequestBody(request, "vault123"); - - Assert.assertTrue(body.getTokenGroupRedactions().isPresent()); - Assert.assertEquals("group1", body.getTokenGroupRedactions().get().get(0).getTokenGroupName().get()); - Assert.assertEquals("MASKED", body.getTokenGroupRedactions().get().get(0).getRedaction().get()); - } - - // ── buildDetokenizeResponse ─────────────────────────────────────────────── - - @Test - public void testBuildDetokenizeResponse_success() { - V1FlowDetokenizeResponseObject record = V1FlowDetokenizeResponseObject.builder() - .token("token1") - .value("secret-value") - .tokenGroupName("group1") - .build(); - V1FlowDetokenizeResponse res = V1FlowDetokenizeResponse.builder() - .response(Collections.singletonList(record)) - .build(); - - DetokenizeResponse response = Utils.buildDetokenizeResponse(res); - - Assert.assertEquals(1, response.getDetokenizedFields().size()); - DetokenizeRecordResponse detokenizedField = response.getDetokenizedFields().get(0); - Assert.assertEquals("token1", detokenizedField.getToken()); - Assert.assertEquals("secret-value", detokenizedField.getValue()); - Assert.assertTrue(response.getErrors().isEmpty()); - } - - @Test - public void testBuildDetokenizeResponse_error() { - V1FlowDetokenizeResponseObject record = V1FlowDetokenizeResponseObject.builder() - .token("token1") - .error("detokenize failed") - .build(); - V1FlowDetokenizeResponse res = V1FlowDetokenizeResponse.builder() - .response(Collections.singletonList(record)) - .build(); - - DetokenizeResponse response = Utils.buildDetokenizeResponse(res); - - Assert.assertTrue(response.getDetokenizedFields().isEmpty()); - Assert.assertEquals(1, response.getErrors().size()); - Assert.assertEquals("detokenize failed", response.getErrors().get(0).getError()); - } - - @Test - public void testBuildDetokenizeResponse_nullResponse() { - DetokenizeResponse response = Utils.buildDetokenizeResponse(null); - Assert.assertTrue(response.getDetokenizedFields().isEmpty()); - Assert.assertTrue(response.getErrors().isEmpty()); - } - - // ── getTokenizeRequestBody ──────────────────────────────────────────────── - - @Test - public void testGetTokenizeRequestBody_buildsCorrectRequest() { - ArrayList records = new ArrayList<>(); - records.add(TokenizeRecord.builder() - .value("value1") - .tokenGroupNames(Collections.singletonList("group1")) - .build()); - TokenizeRequest request = TokenizeRequest.builder().data(records).build(); - - V1FlowTokenizeRequest body = Utils.getTokenizeRequestBody(request, "vault123"); - - Assert.assertEquals("vault123", body.getVaultId().get()); - Assert.assertTrue(body.getData().isPresent()); - Assert.assertEquals(1, body.getData().get().size()); - Assert.assertEquals("value1", body.getData().get().get(0).getValue().get()); - Assert.assertEquals(Collections.singletonList("group1"), body.getData().get().get(0).getTokenGroupNames().get()); - } - - // ── buildTokenizeResponse ───────────────────────────────────────────────── - - @Test - public void testBuildTokenizeResponse_success() { - FlowTokenizeResponseObjectToken token = FlowTokenizeResponseObjectToken.builder() - .tokenGroupName("group1") - .token("tok-abc") - .build(); - V1FlowTokenizeResponseObject responseObject = V1FlowTokenizeResponseObject.builder() - .value("value1") - .tokens(Collections.singletonList(token)) - .build(); - V1FlowTokenizeResponse res = V1FlowTokenizeResponse.builder() - .response(Collections.singletonList(responseObject)) - .build(); - - TokenizeResponse response = Utils.buildTokenizeResponse(res, new HashMap<>(), 1); - - Assert.assertEquals(1, response.getTokenizedData().size()); - TokenizeData data = response.getTokenizedData().get(0); - Assert.assertEquals("value1", data.getValue()); - Assert.assertEquals("tok-abc", data.getTokens().get("group1")); - Assert.assertTrue(response.getErrors().isEmpty()); - } - - @Test - public void testBuildTokenizeResponse_error() { - FlowTokenizeResponseObjectToken token = FlowTokenizeResponseObjectToken.builder() - .tokenGroupName("group1") - .error("tokenization failed") - .httpCode(400) - .build(); - V1FlowTokenizeResponseObject responseObject = V1FlowTokenizeResponseObject.builder() - .value("value1") - .tokens(Collections.singletonList(token)) - .build(); - V1FlowTokenizeResponse res = V1FlowTokenizeResponse.builder() - .response(Collections.singletonList(responseObject)) - .build(); - - TokenizeResponse response = Utils.buildTokenizeResponse(res, new HashMap<>(), 1); - - Assert.assertTrue(response.getTokenizedData().isEmpty()); - Assert.assertEquals(1, response.getErrors().size()); - Map errorRecord = response.getErrors().get(0); - Assert.assertEquals("tokenization failed", errorRecord.get("error")); - Assert.assertEquals(400, errorRecord.get("httpCode")); - Assert.assertEquals("group1", errorRecord.get("tokenGroupName")); - Assert.assertEquals(0, errorRecord.get("index")); - } - - @Test - public void testBuildTokenizeResponse_mixedSuccessAndError() { - FlowTokenizeResponseObjectToken successToken = FlowTokenizeResponseObjectToken.builder() - .tokenGroupName("group1") - .token("tok-abc") - .build(); - FlowTokenizeResponseObjectToken errorToken = FlowTokenizeResponseObjectToken.builder() - .tokenGroupName("group2") - .error("group2 failed") - .httpCode(400) - .build(); - V1FlowTokenizeResponseObject responseObject = V1FlowTokenizeResponseObject.builder() - .value("value1") - .tokens(java.util.Arrays.asList(successToken, errorToken)) - .build(); - V1FlowTokenizeResponse res = V1FlowTokenizeResponse.builder() - .response(Collections.singletonList(responseObject)) - .build(); - - TokenizeResponse response = Utils.buildTokenizeResponse(res, new HashMap<>(), 1); - - Assert.assertEquals(1, response.getTokenizedData().size()); - Assert.assertEquals("tok-abc", response.getTokenizedData().get(0).getTokens().get("group1")); - Assert.assertEquals(1, response.getErrors().size()); - Assert.assertEquals("group2 failed", response.getErrors().get(0).get("error")); - } - - @Test - public void testBuildTokenizeResponse_emptyResponse() { - V1FlowTokenizeResponse res = V1FlowTokenizeResponse.builder().build(); - - TokenizeResponse response = Utils.buildTokenizeResponse(res, new HashMap<>(), 1); - - Assert.assertTrue(response.getTokenizedData().isEmpty()); - Assert.assertTrue(response.getErrors().isEmpty()); - } - - @Test - public void testBuildTokenizeResponse_nullResponse() { - TokenizeResponse response = Utils.buildTokenizeResponse(null, new HashMap<>(), 1); - - Assert.assertTrue(response.getTokenizedData().isEmpty()); - Assert.assertTrue(response.getErrors().isEmpty()); - } - - @Test - public void testBuildTokenizeResponse_incompleteResponseDoesNotThrow() { - FlowTokenizeResponseObjectToken token = FlowTokenizeResponseObjectToken.builder() - .tokenGroupName("group1") - .token("tok-abc") - .build(); - V1FlowTokenizeResponseObject responseObject = V1FlowTokenizeResponseObject.builder() - .value("value1") - .tokens(Collections.singletonList(token)) - .build(); - V1FlowTokenizeResponse res = V1FlowTokenizeResponse.builder() - .response(Collections.singletonList(responseObject)) - .build(); - - // requestedRecordCount=2 but only 1 record came back — should just warn, not throw - TokenizeResponse response = Utils.buildTokenizeResponse(res, new HashMap<>(), 2); - - Assert.assertEquals(1, response.getTokenizedData().size()); - } - - // ── getDeleteTokensRequestBody ──────────────────────────────────────────── - - @Test - public void testGetDeleteTokensRequestBody_buildsCorrectRequest() { - DeleteTokensRequest request = DeleteTokensRequest.builder() - .tokens(Collections.singletonList("token1")) - .build(); - - V1FlowDeleteTokenRequest body = Utils.getDeleteTokensRequestBody(request, "vault123"); + V1InsertRequest body = Utils.getInsertRequestBody(request, config); - Assert.assertEquals("vault123", body.getVaultId().get()); - Assert.assertEquals(Collections.singletonList("token1"), body.getTokens().get()); + Assert.assertEquals(tokens, body.getRecords().get().get(0).getTokens().get()); } - // ── buildDeleteTokensResponse ───────────────────────────────────────────── - @Test - public void testBuildDeleteTokensResponse_success() { - V1DeleteTokenResponseObject record = V1DeleteTokenResponseObject.builder() - .value("token1") + public void testGetInsertRequestBody_withUpsertAtRequestLevel() { + Map data = new HashMap<>(); + data.put("name", "john"); + InsertRequestRecord record = InsertRequestRecord.builder().tableName("table1").data(data).build(); + ArrayList records = new ArrayList<>(); + records.add(record); + UpsertOptions upsert = UpsertOptions.builder() + .uniqueColumns(Collections.singletonList("email")) + .updateType("UPDATE") .build(); - V1FlowDeleteTokenResponse res = V1FlowDeleteTokenResponse.builder() - .tokens(Collections.singletonList(record)) + InsertRequest request = InsertRequest.builder() + .records(records) + .upsert(upsert) .build(); + VaultConfig config = new VaultConfig(); + config.setVaultId("vault123"); - DeleteTokensResponse response = Utils.buildDeleteTokensResponse(res, new HashMap<>(), 1); + V1InsertRequest body = Utils.getInsertRequestBody(request, config); - Assert.assertEquals(Collections.singletonList("token1"), response.getTokens()); - Assert.assertTrue(response.getErrors().isEmpty()); + Assert.assertTrue(body.getRecords().get().get(0).getUpsert().isPresent()); + Assert.assertEquals(Collections.singletonList("email"), body.getRecords().get().get(0).getUpsert().get().getUniqueColumns().get()); + Assert.assertEquals(FlowEnumUpdateType.UPDATE, body.getRecords().get().get(0).getUpsert().get().getUpdateType().get()); } @Test - public void testBuildDeleteTokensResponse_error() { - V1DeleteTokenResponseObject record = V1DeleteTokenResponseObject.builder() - .error("token not found") - .httpCode(404) + public void testGetInsertRequestBody_withUpsertAtRecordLevel() { + Map data = new HashMap<>(); + data.put("name", "john"); + UpsertOptions upsert = UpsertOptions.builder() + .uniqueColumns(Collections.singletonList("email")) + .updateType("REPLACE") .build(); - V1FlowDeleteTokenResponse res = V1FlowDeleteTokenResponse.builder() - .tokens(Collections.singletonList(record)) + InsertRequestRecord record = InsertRequestRecord.builder() + .tableName("table1") + .data(data) + .upsert(upsert) .build(); + ArrayList records = new ArrayList<>(); + records.add(record); + InsertRequest request = InsertRequest.builder().records(records).build(); + VaultConfig config = new VaultConfig(); + config.setVaultId("vault123"); - DeleteTokensResponse response = Utils.buildDeleteTokensResponse(res, new HashMap<>(), 1); - - Assert.assertTrue(response.getTokens().isEmpty()); - Assert.assertEquals(1, response.getErrors().size()); - Assert.assertEquals("token not found", response.getErrors().get(0).get("error")); - Assert.assertEquals(404, response.getErrors().get(0).get("httpCode")); - } - - @Test - public void testBuildDeleteTokensResponse_emptyResponse() { - V1FlowDeleteTokenResponse res = V1FlowDeleteTokenResponse.builder().build(); - - DeleteTokensResponse response = Utils.buildDeleteTokensResponse(res, new HashMap<>(), 1); - - Assert.assertTrue(response.getTokens().isEmpty()); - Assert.assertTrue(response.getErrors().isEmpty()); - } - - @Test - public void testBuildDeleteTokensResponse_nullResponse() { - DeleteTokensResponse response = Utils.buildDeleteTokensResponse(null, new HashMap<>(), 1); + V1InsertRequest body = Utils.getInsertRequestBody(request, config); - Assert.assertTrue(response.getTokens().isEmpty()); - Assert.assertTrue(response.getErrors().isEmpty()); + Assert.assertEquals("table1", body.getRecords().get().get(0).getTableName().get()); + Assert.assertTrue(body.getRecords().get().get(0).getUpsert().isPresent()); + Assert.assertEquals(FlowEnumUpdateType.REPLACE, body.getRecords().get().get(0).getUpsert().get().getUpdateType().get()); } - @Test - public void testBuildDeleteTokensResponse_incompleteResponseDoesNotThrow() { - V1DeleteTokenResponseObject record = V1DeleteTokenResponseObject.builder() - .value("token1") - .build(); - V1FlowDeleteTokenResponse res = V1FlowDeleteTokenResponse.builder() - .tokens(Collections.singletonList(record)) - .build(); - - // requestedTokenCount=2 but only 1 record came back — should just warn, not throw - DeleteTokensResponse response = Utils.buildDeleteTokensResponse(res, new HashMap<>(), 2); - - Assert.assertEquals(1, response.getTokens().size()); - } + // Tests for buildInsertResponse / getDetokenizeRequestBody / buildDetokenizeResponse / + // getTokenizeRequestBody / buildTokenizeResponse / getDeleteTokensRequestBody / + // buildDeleteTokensResponse were removed: those unary Utils helpers no longer exist (bulk-only module). // ── getBulkInsertRequestBody (bulk overload) ────────────────────────────── @@ -647,10 +330,10 @@ public void testBuildDeleteTokensResponse_incompleteResponseDoesNotThrow() { public void testGetBulkInsertRequestBody_bulk_buildsCorrectRequest() { Map data = new HashMap<>(); data.put("name", "john"); - BulkInsertRecord record = BulkInsertRecord.builder().data(data).build(); - ArrayList records = new ArrayList<>(); + BulkInsertRequestRecord record = BulkInsertRequestRecord.builder().data(data).build(); + ArrayList records = new ArrayList<>(); records.add(record); - BulkInsertRequest request = BulkInsertRequest.builder().table("table1").records(records).build(); + BulkInsertRequest request = BulkInsertRequest.builder().tableName("table1").records(records).build(); VaultConfig config = new VaultConfig(); config.setVaultId("vault123"); @@ -659,6 +342,7 @@ public void testGetBulkInsertRequestBody_bulk_buildsCorrectRequest() { Assert.assertEquals("vault123", body.getVaultId().get()); Assert.assertEquals("table1", body.getTableName().get()); Assert.assertEquals(1, body.getRecords().get().size()); + Assert.assertEquals("table1", body.getRecords().get().get(0).getTableName().get()); Assert.assertEquals(data, body.getRecords().get().get(0).getData().get()); } @@ -666,36 +350,40 @@ public void testGetBulkInsertRequestBody_bulk_buildsCorrectRequest() { public void testGetBulkInsertRequestBody_bulk_withUpsertAtRequestLevel() { Map data = new HashMap<>(); data.put("name", "john"); - BulkInsertRecord record = BulkInsertRecord.builder().data(data).build(); - ArrayList records = new ArrayList<>(); + BulkInsertRequestRecord record = BulkInsertRequestRecord.builder().data(data).build(); + ArrayList records = new ArrayList<>(); records.add(record); BulkInsertRequest request = BulkInsertRequest.builder() - .table("table1") + .tableName("table1") .records(records) - .upsert(Collections.singletonList("email")) - .upsertType(UpsertType.UPDATE) + .upsert(UpsertOptions.builder() + .uniqueColumns(Collections.singletonList("email")) + .updateType("UPDATE") + .build()) .build(); VaultConfig config = new VaultConfig(); config.setVaultId("vault123"); V1InsertRequest body = Utils.getBulkInsertRequestBody(request, config); - Assert.assertTrue(body.getUpsert().isPresent()); - Assert.assertEquals(Collections.singletonList("email"), body.getUpsert().get().getUniqueColumns().get()); - Assert.assertEquals(FlowEnumUpdateType.UPDATE, body.getUpsert().get().getUpdateType().get()); + Assert.assertTrue(body.getRecords().get().get(0).getUpsert().isPresent()); + Assert.assertEquals(Collections.singletonList("email"), body.getRecords().get().get(0).getUpsert().get().getUniqueColumns().get()); + Assert.assertEquals(FlowEnumUpdateType.UPDATE, body.getRecords().get().get(0).getUpsert().get().getUpdateType().get()); } @Test public void testGetBulkInsertRequestBody_bulk_withUpsertAtRecordLevel() { Map data = new HashMap<>(); data.put("name", "john"); - BulkInsertRecord record = BulkInsertRecord.builder() - .table("table1") + BulkInsertRequestRecord record = BulkInsertRequestRecord.builder() + .tableName("table1") .data(data) - .upsert(Collections.singletonList("email")) - .upsertType(UpsertType.REPLACE) + .upsert(UpsertOptions.builder() + .uniqueColumns(Collections.singletonList("email")) + .updateType("REPLACE") + .build()) .build(); - ArrayList records = new ArrayList<>(); + ArrayList records = new ArrayList<>(); records.add(record); BulkInsertRequest request = BulkInsertRequest.builder().records(records).build(); VaultConfig config = new VaultConfig(); @@ -725,7 +413,7 @@ public void testGetBulkDetokenizeRequestBody_buildsCorrectRequest() { @Test public void testGetBulkDetokenizeRequestBody_withTokenGroupRedactions() { - BulkTokenGroupRedactions redaction = BulkTokenGroupRedactions.builder() + TokenGroupRedactions redaction = TokenGroupRedactions.builder() .tokenGroupName("group1") .redaction("MASKED") .build(); @@ -947,10 +635,10 @@ public void testHandleBulkInsertBatchException_apiExceptionWithRecordsBody() { RuntimeException wrapper = new RuntimeException(apiEx); List batch = Collections.singletonList(V1InsertRecordData.builder().data(new HashMap<>()).build()); - List errors = Utils.handleBulkInsertBatchException(wrapper, batch, 0, 50); + List errors = Utils.handleBulkInsertBatchException(wrapper, batch, 0, 50); Assert.assertEquals(1, errors.size()); - Assert.assertEquals(409, errors.get(0).getCode()); + Assert.assertEquals(409, errors.get(0).getHttpCode()); Assert.assertEquals("duplicate", errors.get(0).getError()); } @@ -962,10 +650,10 @@ public void testHandleBulkInsertBatchException_apiExceptionWithNoParsableBody() List batch = Arrays.asList( V1InsertRecordData.builder().data(new HashMap<>()).build(), V1InsertRecordData.builder().data(new HashMap<>()).build()); - List errors = Utils.handleBulkInsertBatchException(wrapper, batch, 0, 50); + List errors = Utils.handleBulkInsertBatchException(wrapper, batch, 0, 50); Assert.assertEquals(2, errors.size()); - Assert.assertEquals(401, errors.get(0).getCode()); + Assert.assertEquals(401, errors.get(0).getHttpCode()); Assert.assertEquals("insert failed", errors.get(0).getError()); } @@ -977,10 +665,10 @@ public void testHandleBulkInsertBatchException_apiExceptionWithErrorKeyBody() { RuntimeException wrapper = new RuntimeException(apiEx); List batch = Collections.singletonList(V1InsertRecordData.builder().data(new HashMap<>()).build()); - List errors = Utils.handleBulkInsertBatchException(wrapper, batch, 0, 50); + List errors = Utils.handleBulkInsertBatchException(wrapper, batch, 0, 50); Assert.assertEquals(1, errors.size()); - Assert.assertEquals(401, errors.get(0).getCode()); + Assert.assertEquals(401, errors.get(0).getHttpCode()); Assert.assertEquals("top level auth error", errors.get(0).getError()); } @@ -989,35 +677,482 @@ public void testHandleBulkInsertBatchException_genericException() { RuntimeException ex = new RuntimeException("boom"); List batch = Collections.singletonList(V1InsertRecordData.builder().data(new HashMap<>()).build()); - List errors = Utils.handleBulkInsertBatchException(ex, batch, 1, 2); + List errors = Utils.handleBulkInsertBatchException(ex, batch, 1, 2); Assert.assertEquals(1, errors.size()); - Assert.assertEquals(500, errors.get(0).getCode()); + Assert.assertEquals(500, errors.get(0).getHttpCode()); Assert.assertEquals("boom", errors.get(0).getError()); Assert.assertEquals(2, errors.get(0).getIndex()); + // Projected error records carry no table/id/field data. + Assert.assertNull(errors.get(0).getTableName()); + Assert.assertNull(errors.get(0).getSkyflowId()); + Assert.assertNull(errors.get(0).getFields()); + Assert.assertNull(errors.get(0).getHashedData()); } - // ── handleBulkDetokenizeBatchException ──────────────────────────────────── + @Test + public void testHandleBulkInsertBatchException_recordsBodyCarriesTableAndSkyflowId() { + // createInsertErrorRecord now builds a BulkInsertResponseRecord directly, so per-record + // table/id data from the error body survives instead of being nulled out. + Map recordMap = new HashMap<>(); + recordMap.put("tableName", "cards"); + recordMap.put("error", "duplicate"); + recordMap.put("httpCode", 409); + Map body = new HashMap<>(); + body.put("records", Collections.singletonList(recordMap)); + ApiClientApiException apiEx = new ApiClientApiException("insert failed", 409, body); + RuntimeException wrapper = new RuntimeException(apiEx); + + List batch = Collections.singletonList( + V1InsertRecordData.builder().data(new HashMap<>()).build()); + List records = Utils.handleBulkInsertBatchException(wrapper, batch, 0, 50); + + Assert.assertEquals(1, records.size()); + Assert.assertEquals("cards", records.get(0).getTableName()); + Assert.assertEquals("duplicate", records.get(0).getError()); + Assert.assertEquals(409, records.get(0).getHttpCode()); + } @Test - public void testHandleBulkDetokenizeBatchException_apiExceptionWithResponseBody() { - Map errorRecordMap = new HashMap<>(); - errorRecordMap.put("error", "token not found"); - errorRecordMap.put("httpCode", 404); + public void testHandleBulkInsertBatchException_recordsBodyFallsBackToUnknownError() { + // An entry with no error/message key still produces a record rather than being skipped. + Map recordMap = new HashMap<>(); + recordMap.put("httpCode", 500); Map body = new HashMap<>(); - body.put("response", Collections.singletonList(errorRecordMap)); - ApiClientApiException apiEx = new ApiClientApiException("detokenize failed", 404, body); + body.put("records", Collections.singletonList(recordMap)); + ApiClientApiException apiEx = new ApiClientApiException("insert failed", 500, body); RuntimeException wrapper = new RuntimeException(apiEx); - V1FlowDetokenizeRequest batch = V1FlowDetokenizeRequest.builder() - .vaultId("vault123") - .tokens(Collections.singletonList("t1")) - .build(); - List errors = Utils.handleBulkDetokenizeBatchException(wrapper, batch, 0, 50); + List batch = Collections.singletonList( + V1InsertRecordData.builder().data(new HashMap<>()).build()); + List records = Utils.handleBulkInsertBatchException(wrapper, batch, 0, 50); - Assert.assertEquals(1, errors.size()); - Assert.assertEquals(404, errors.get(0).getCode()); - Assert.assertEquals("token not found", errors.get(0).getError()); + Assert.assertEquals(1, records.size()); + Assert.assertEquals("Unknown error", records.get(0).getError()); + Assert.assertEquals(500, records.get(0).getHttpCode()); + } + + @Test + public void testHandleBulkInsertBatchException_indexOffsetAcrossBatches() { + Map body = new HashMap<>(); + body.put("error", "auth error"); + ApiClientApiException apiEx = new ApiClientApiException("insert failed", 401, body); + RuntimeException wrapper = new RuntimeException(apiEx); + + List batch = Arrays.asList( + V1InsertRecordData.builder().data(new HashMap<>()).build(), + V1InsertRecordData.builder().data(new HashMap<>()).build()); + List records = Utils.handleBulkInsertBatchException(wrapper, batch, 2, 50); + + Assert.assertEquals(100, records.get(0).getIndex()); + Assert.assertEquals(101, records.get(1).getIndex()); + } + + @Test + public void testHandleBulkInsertBatchException_genericExceptionHasNoRequestId() { + // A non-API failure has no response headers to read a request id from. + RuntimeException ex = new RuntimeException("boom"); + List batch = Collections.singletonList( + V1InsertRecordData.builder().data(new HashMap<>()).build()); + + List records = Utils.handleBulkInsertBatchException(ex, batch, 0, 50); + + Assert.assertEquals(1, records.size()); + Assert.assertNull(records.get(0).getRequestId()); + Assert.assertEquals("boom", records.get(0).getError()); + } + + // ── createInsertErrorRecord / createDetokenizeErrorRecord branch coverage ─ + + @Test + public void testCreateInsertErrorRecord_nullRecordMapReturnsNull() { + Assert.assertNull(Utils.createInsertErrorRecord(null, 0, "req-1")); + Assert.assertNull(Utils.createDetokenizeErrorRecord(null, 0, "req-1")); + } + + @Test + public void testCreateErrorRecords_snakeCaseHttpCodeKey() { + Map recordMap = new HashMap<>(); + recordMap.put("http_code", 409); + recordMap.put("error", "duplicate"); + + Assert.assertEquals(409, Utils.createInsertErrorRecord(recordMap, 0, null).getHttpCode()); + Assert.assertEquals(409, Utils.createDetokenizeErrorRecord(recordMap, 0, null).getHttpCode()); + } + + @Test + public void testCreateErrorRecords_statusCodeKey() { + Map recordMap = new HashMap<>(); + recordMap.put("statusCode", 422); + recordMap.put("error", "unprocessable"); + + Assert.assertEquals(422, Utils.createInsertErrorRecord(recordMap, 0, null).getHttpCode()); + Assert.assertEquals(422, Utils.createDetokenizeErrorRecord(recordMap, 0, null).getHttpCode()); + } + + @Test + public void testCreateErrorRecords_noHttpCodeKeyDefaultsTo500() { + Map recordMap = new HashMap<>(); + recordMap.put("error", "no code supplied"); + + Assert.assertEquals(500, Utils.createInsertErrorRecord(recordMap, 0, null).getHttpCode()); + Assert.assertEquals(500, Utils.createDetokenizeErrorRecord(recordMap, 0, null).getHttpCode()); + } + + @Test + public void testCreateErrorRecords_messageKeyIsUsedWhenErrorKeyAbsent() { + Map recordMap = new HashMap<>(); + recordMap.put("message", "vault not found"); + recordMap.put("httpCode", 404); + + Assert.assertEquals("vault not found", Utils.createInsertErrorRecord(recordMap, 0, null).getError()); + Assert.assertEquals("vault not found", Utils.createDetokenizeErrorRecord(recordMap, 0, null).getError()); + } + + @Test + public void testCreateInsertErrorRecord_readsSkyflowIdUsingWireCasing() { + // The API returns the id as "skyflowID" — matching @JsonProperty("skyflowID") on the + // generated V1RecordResponseObject — even though the SDK exposes it as getSkyflowId(). + Map recordMap = new HashMap<>(); + recordMap.put("skyflowID", "id-1"); + recordMap.put("tableName", "cards"); + recordMap.put("error", "duplicate"); + + BulkInsertResponseRecord record = Utils.createInsertErrorRecord(recordMap, 0, null); + + Assert.assertEquals("id-1", record.getSkyflowId()); + Assert.assertEquals("cards", record.getTableName()); + } + + @Test + public void testCreateErrorRecords_requestIdIsCarried() { + Map recordMap = new HashMap<>(); + recordMap.put("error", "boom"); + + Assert.assertEquals("req-7", Utils.createInsertErrorRecord(recordMap, 3, "req-7").getRequestId()); + Assert.assertEquals("req-7", Utils.createDetokenizeErrorRecord(recordMap, 3, "req-7").getRequestId()); + Assert.assertEquals(3, Utils.createInsertErrorRecord(recordMap, 3, "req-7").getIndex()); + } + + // ── handleBulkInsertBatchException, remaining branches ──────────────────── + + @Test + public void testHandleBulkInsertBatchException_errorFieldAsObjectUsesHelper() { + // The API's structured error envelope: {"error": {message, httpCode, ...}} + Map errorObject = new HashMap<>(); + errorObject.put("message", "vault not found"); + errorObject.put("httpCode", 404); + Map body = new HashMap<>(); + body.put("error", errorObject); + ApiClientApiException apiEx = new ApiClientApiException("insert failed", 404, body); + RuntimeException wrapper = new RuntimeException(apiEx); + + List batch = Arrays.asList( + V1InsertRecordData.builder().data(new HashMap<>()).build(), + V1InsertRecordData.builder().data(new HashMap<>()).build()); + List records = Utils.handleBulkInsertBatchException(wrapper, batch, 0, 50); + + Assert.assertEquals(2, records.size()); + for (BulkInsertResponseRecord record : records) { + Assert.assertEquals("vault not found", record.getError()); + Assert.assertEquals(404, record.getHttpCode()); + } + } + + @Test + public void testHandleBulkInsertBatchException_errorFieldNeitherMapNorStringUsesApiMessage() { + Map body = new HashMap<>(); + body.put("error", 500); + ApiClientApiException apiEx = new ApiClientApiException("insert failed", 500, body); + RuntimeException wrapper = new RuntimeException(apiEx); + + List batch = Collections.singletonList( + V1InsertRecordData.builder().data(new HashMap<>()).build()); + List records = Utils.handleBulkInsertBatchException(wrapper, batch, 0, 50); + + Assert.assertEquals(1, records.size()); + Assert.assertEquals("insert failed", records.get(0).getError()); + } + + @Test + public void testHandleBulkInsertBatchException_recordsNotAListFallsBackToBatchWideError() { + Map body = new HashMap<>(); + body.put("records", "not-a-list"); + ApiClientApiException apiEx = new ApiClientApiException("insert failed", 400, body); + RuntimeException wrapper = new RuntimeException(apiEx); + + List batch = Collections.singletonList( + V1InsertRecordData.builder().data(new HashMap<>()).build()); + List records = Utils.handleBulkInsertBatchException(wrapper, batch, 0, 50); + + Assert.assertEquals(1, records.size()); + Assert.assertEquals("insert failed", records.get(0).getError()); + Assert.assertEquals(400, records.get(0).getHttpCode()); + } + + @Test + public void testHandleBulkInsertBatchException_nonMapEntriesAreSkipped() { + Map body = new HashMap<>(); + body.put("records", Arrays.asList("not-a-map", null)); + ApiClientApiException apiEx = new ApiClientApiException("insert failed", 400, body); + RuntimeException wrapper = new RuntimeException(apiEx); + + List batch = Collections.singletonList( + V1InsertRecordData.builder().data(new HashMap<>()).build()); + List records = Utils.handleBulkInsertBatchException(wrapper, batch, 0, 50); + + // No entry parsed, so the batch-wide fallback fires instead. + Assert.assertEquals(1, records.size()); + Assert.assertEquals("insert failed", records.get(0).getError()); + } + + @Test + public void testHandleBulkInsertBatchException_bodyWithNeitherRecordsNorErrorKey() { + // A map body that matches neither branch falls through to the batch-wide fallback. + Map body = new HashMap<>(); + body.put("unexpected", "shape"); + ApiClientApiException apiEx = new ApiClientApiException("insert failed", 503, body); + RuntimeException wrapper = new RuntimeException(apiEx); + + List batch = Collections.singletonList( + V1InsertRecordData.builder().data(new HashMap<>()).build()); + List records = Utils.handleBulkInsertBatchException(wrapper, batch, 0, 50); + + Assert.assertEquals(1, records.size()); + Assert.assertEquals("insert failed", records.get(0).getError()); + Assert.assertEquals(503, records.get(0).getHttpCode()); + } + + // ── handleBulkDetokenizeBatchException ──────────────────────────────────── + + @Test + public void testHandleBulkDetokenizeBatchException_apiExceptionWithResponseBody() { + Map errorRecordMap = new HashMap<>(); + errorRecordMap.put("error", "token not found"); + errorRecordMap.put("httpCode", 404); + Map body = new HashMap<>(); + body.put("response", Collections.singletonList(errorRecordMap)); + ApiClientApiException apiEx = new ApiClientApiException("detokenize failed", 404, body); + RuntimeException wrapper = new RuntimeException(apiEx); + + V1FlowDetokenizeRequest batch = V1FlowDetokenizeRequest.builder() + .vaultId("vault123") + .tokens(Collections.singletonList("t1")) + .build(); + List errors = Utils.handleBulkDetokenizeBatchException(wrapper, batch, 0, 50); + + Assert.assertEquals(1, errors.size()); + Assert.assertEquals(404, errors.get(0).getHttpCode()); + Assert.assertEquals("token not found", errors.get(0).getError()); + Assert.assertEquals(0, errors.get(0).getIndex()); + // This entry carried no token/group of its own, so those stay null. + Assert.assertNull(errors.get(0).getToken()); + Assert.assertNull(errors.get(0).getTokenGroupName()); + Assert.assertNull(errors.get(0).getMetadata()); + } + + @Test + public void testHandleBulkDetokenizeBatchException_responseBodyCarriesTokenAndGroup() { + // createDetokenizeErrorRecord builds a BulkDetokenizeResponseRecord directly, so the + // failing token echoed back by the API survives instead of being nulled out. + Map errorRecordMap = new HashMap<>(); + errorRecordMap.put("token", "tok-bad"); + errorRecordMap.put("tokenGroupName", "email_group"); + errorRecordMap.put("error", "token not found"); + errorRecordMap.put("httpCode", 404); + Map body = new HashMap<>(); + body.put("response", Collections.singletonList(errorRecordMap)); + ApiClientApiException apiEx = new ApiClientApiException("detokenize failed", 404, body); + RuntimeException wrapper = new RuntimeException(apiEx); + + V1FlowDetokenizeRequest batch = V1FlowDetokenizeRequest.builder() + .vaultId("vault123") + .tokens(Collections.singletonList("tok-bad")) + .build(); + List records = Utils.handleBulkDetokenizeBatchException(wrapper, batch, 0, 50); + + Assert.assertEquals(1, records.size()); + Assert.assertEquals("tok-bad", records.get(0).getToken()); + Assert.assertEquals("email_group", records.get(0).getTokenGroupName()); + Assert.assertEquals("token not found", records.get(0).getError()); + Assert.assertEquals(404, records.get(0).getHttpCode()); + } + + @Test + public void testHandleBulkDetokenizeBatchException_responseBodyFallsBackToUnknownError() { + Map errorRecordMap = new HashMap<>(); + errorRecordMap.put("httpCode", 500); + Map body = new HashMap<>(); + body.put("response", Collections.singletonList(errorRecordMap)); + ApiClientApiException apiEx = new ApiClientApiException("detokenize failed", 500, body); + RuntimeException wrapper = new RuntimeException(apiEx); + + V1FlowDetokenizeRequest batch = V1FlowDetokenizeRequest.builder() + .vaultId("vault123") + .tokens(Collections.singletonList("t1")) + .build(); + List records = Utils.handleBulkDetokenizeBatchException(wrapper, batch, 0, 50); + + Assert.assertEquals(1, records.size()); + Assert.assertEquals("Unknown error", records.get(0).getError()); + Assert.assertEquals(500, records.get(0).getHttpCode()); + } + + @Test + public void testHandleBulkDetokenizeBatchException_topLevelErrorAppliesToEveryToken() { + Map body = new HashMap<>(); + body.put("error", "top level auth error"); + ApiClientApiException apiEx = new ApiClientApiException("detokenize failed", 401, body); + RuntimeException wrapper = new RuntimeException(apiEx); + + V1FlowDetokenizeRequest batch = V1FlowDetokenizeRequest.builder() + .vaultId("vault123") + .tokens(Arrays.asList("t1", "t2")) + .build(); + List records = Utils.handleBulkDetokenizeBatchException(wrapper, batch, 2, 50); + + Assert.assertEquals(2, records.size()); + Assert.assertEquals("top level auth error", records.get(0).getError()); + Assert.assertEquals(401, records.get(0).getHttpCode()); + // index continues from the batch offset + Assert.assertEquals(100, records.get(0).getIndex()); + Assert.assertEquals(101, records.get(1).getIndex()); + } + + @Test + public void testHandleBulkDetokenizeBatchException_errorFieldAsObjectUsesHelper() { + Map errorObject = new HashMap<>(); + errorObject.put("message", "vault not found"); + errorObject.put("httpCode", 404); + Map body = new HashMap<>(); + body.put("error", errorObject); + ApiClientApiException apiEx = new ApiClientApiException("detokenize failed", 404, body); + RuntimeException wrapper = new RuntimeException(apiEx); + + V1FlowDetokenizeRequest batch = V1FlowDetokenizeRequest.builder() + .vaultId("vault123") + .tokens(Arrays.asList("t1", "t2")) + .build(); + List records = Utils.handleBulkDetokenizeBatchException(wrapper, batch, 0, 50); + + Assert.assertEquals(2, records.size()); + for (BulkDetokenizeResponseRecord record : records) { + Assert.assertEquals("vault not found", record.getError()); + Assert.assertEquals(404, record.getHttpCode()); + } + } + + @Test + public void testHandleBulkDetokenizeBatchException_errorFieldNeitherMapNorStringUsesApiMessage() { + Map body = new HashMap<>(); + body.put("error", 500); + ApiClientApiException apiEx = new ApiClientApiException("detokenize failed", 500, body); + RuntimeException wrapper = new RuntimeException(apiEx); + + V1FlowDetokenizeRequest batch = V1FlowDetokenizeRequest.builder() + .vaultId("vault123") + .tokens(Collections.singletonList("t1")) + .build(); + List records = Utils.handleBulkDetokenizeBatchException(wrapper, batch, 0, 50); + + Assert.assertEquals(1, records.size()); + Assert.assertEquals("detokenize failed", records.get(0).getError()); + } + + @Test + public void testHandleBulkDetokenizeBatchException_responseNotAListFallsBackToBatchWideError() { + Map body = new HashMap<>(); + body.put("response", "not-a-list"); + ApiClientApiException apiEx = new ApiClientApiException("detokenize failed", 400, body); + RuntimeException wrapper = new RuntimeException(apiEx); + + V1FlowDetokenizeRequest batch = V1FlowDetokenizeRequest.builder() + .vaultId("vault123") + .tokens(Collections.singletonList("t1")) + .build(); + List records = Utils.handleBulkDetokenizeBatchException(wrapper, batch, 0, 50); + + Assert.assertEquals(1, records.size()); + Assert.assertEquals("detokenize failed", records.get(0).getError()); + } + + @Test + public void testHandleBulkDetokenizeBatchException_nonMapEntriesAreSkipped() { + Map body = new HashMap<>(); + body.put("response", Arrays.asList("not-a-map", null)); + ApiClientApiException apiEx = new ApiClientApiException("detokenize failed", 400, body); + RuntimeException wrapper = new RuntimeException(apiEx); + + V1FlowDetokenizeRequest batch = V1FlowDetokenizeRequest.builder() + .vaultId("vault123") + .tokens(Collections.singletonList("t1")) + .build(); + List records = Utils.handleBulkDetokenizeBatchException(wrapper, batch, 0, 50); + + Assert.assertEquals(1, records.size()); + Assert.assertEquals("detokenize failed", records.get(0).getError()); + } + + @Test + public void testHandleBulkDetokenizeBatchException_batchWithNoTokensProducesNoRecords() { + Map body = new HashMap<>(); + body.put("error", "auth error"); + ApiClientApiException apiEx = new ApiClientApiException("detokenize failed", 401, body); + RuntimeException wrapper = new RuntimeException(apiEx); + + V1FlowDetokenizeRequest batch = V1FlowDetokenizeRequest.builder().vaultId("vault123").build(); + List records = Utils.handleBulkDetokenizeBatchException(wrapper, batch, 0, 50); + + Assert.assertTrue(records.isEmpty()); + } + + @Test + public void testHandleBulkDetokenizeBatchException_bodyWithNeitherResponseNorErrorKey() { + Map body = new HashMap<>(); + body.put("unexpected", "shape"); + ApiClientApiException apiEx = new ApiClientApiException("detokenize failed", 503, body); + RuntimeException wrapper = new RuntimeException(apiEx); + + V1FlowDetokenizeRequest batch = V1FlowDetokenizeRequest.builder() + .vaultId("vault123") + .tokens(Collections.singletonList("t1")) + .build(); + List records = Utils.handleBulkDetokenizeBatchException(wrapper, batch, 0, 50); + + Assert.assertEquals(1, records.size()); + Assert.assertEquals("detokenize failed", records.get(0).getError()); + Assert.assertEquals(503, records.get(0).getHttpCode()); + } + + // ── formatBulk*Response, empty/absent bodies ────────────────────────────── + + @Test + public void testFormatBulkResponses_nullResponseReturnsNull() { + Assert.assertNull(Utils.formatBulkInsertResponse(null, 0, 50, null)); + Assert.assertNull(Utils.formatBulkDetokenizeResponse(null, 0, 50, null)); + } + + @Test + public void testFormatBulkResponses_absentRecordsReturnsNull() { + Assert.assertNull(Utils.formatBulkInsertResponse( + V1InsertResponse.builder().build(), 0, 50, null)); + Assert.assertNull(Utils.formatBulkDetokenizeResponse( + V1FlowDetokenizeResponse.builder().build(), 0, 50, null)); + } + + @Test + public void testHandleBulkDetokenizeBatchException_genericExceptionHasNoRequestId() { + RuntimeException ex = new RuntimeException("boom"); + V1FlowDetokenizeRequest batch = V1FlowDetokenizeRequest.builder() + .vaultId("vault123") + .tokens(Collections.singletonList("t1")) + .build(); + + List records = Utils.handleBulkDetokenizeBatchException(ex, batch, 0, 50); + + Assert.assertEquals(1, records.size()); + Assert.assertNull(records.get(0).getRequestId()); + Assert.assertEquals("boom", records.get(0).getError()); } @Test @@ -1028,10 +1163,11 @@ public void testHandleBulkDetokenizeBatchException_genericException() { .tokens(Collections.singletonList("t1")) .build(); - List errors = Utils.handleBulkDetokenizeBatchException(ex, batch, 0, 50); + List errors = Utils.handleBulkDetokenizeBatchException(ex, batch, 0, 50); Assert.assertEquals(1, errors.size()); - Assert.assertEquals(500, errors.get(0).getCode()); + Assert.assertEquals(500, errors.get(0).getHttpCode()); + Assert.assertEquals("boom", errors.get(0).getError()); } // ── handleBulkDeleteTokensBatchException ────────────────────────────────── @@ -1140,9 +1276,23 @@ public void testFormatBulkInsertResponse_success() { BulkInsertResponse result = Utils.formatBulkInsertResponse(response, 0, 50, new HashMap<>()); - Assert.assertEquals(1, result.getSuccess().size()); - Assert.assertEquals("sky-id-1", result.getSuccess().get(0).getSkyflowId()); - Assert.assertTrue(result.getErrors().isEmpty()); + Assert.assertEquals(1, result.getRecords().size()); + BulkInsertResponseRecord inserted = result.getRecords().get(0); + Assert.assertEquals("sky-id-1", inserted.getSkyflowId()); + Assert.assertEquals(tokens, inserted.getFields()); + Assert.assertEquals(0, inserted.getIndex()); + Assert.assertEquals(200, inserted.getHttpCode()); + Assert.assertNull(inserted.getError()); + } + + @Test + public void testFormatBulkInsertResponse_indexOffsetByBatchNumber() { + V1RecordResponseObject record = V1RecordResponseObject.builder().skyflowId("sky-id-1").build(); + V1InsertResponse response = V1InsertResponse.builder().records(Collections.singletonList(record)).build(); + + BulkInsertResponse result = Utils.formatBulkInsertResponse(response, 2, 50, new HashMap<>()); + + Assert.assertEquals(100, result.getRecords().get(0).getIndex()); } @Test @@ -1154,9 +1304,9 @@ public void testFormatBulkInsertResponse_errorWithMissingHttpCodeDefaultsTo500() BulkInsertResponse result = Utils.formatBulkInsertResponse(response, 0, 50, new HashMap<>()); - Assert.assertEquals(1, result.getErrors().size()); - Assert.assertEquals(500, result.getErrors().get(0).getCode()); - Assert.assertEquals("insert failed", result.getErrors().get(0).getError()); + Assert.assertEquals(1, result.getRecords().size()); + Assert.assertEquals(500, result.getRecords().get(0).getHttpCode()); + Assert.assertEquals("insert failed", result.getRecords().get(0).getError()); } @Test @@ -1178,9 +1328,29 @@ public void testFormatBulkDetokenizeResponse_success() { BulkDetokenizeResponse result = Utils.formatBulkDetokenizeResponse(response, 0, 50, new HashMap<>()); - Assert.assertEquals(1, result.getSuccess().size()); - Assert.assertEquals("secret-value", result.getSuccess().get(0).getValue()); - Assert.assertTrue(result.getErrors().isEmpty()); + Assert.assertEquals(1, result.getRecords().size()); + BulkDetokenizeResponseRecord detokenized = result.getRecords().get(0); + Assert.assertEquals("token1", detokenized.getToken()); + Assert.assertEquals(0, detokenized.getIndex()); + // Success records default to httpCode 200 and carry no error. + Assert.assertEquals(200, detokenized.getHttpCode()); + Assert.assertNull(detokenized.getError()); + // Per-batch responses leave the summary unset. + Assert.assertNull(result.getSummary()); + } + + @Test + public void testFormatBulkDetokenizeResponse_indexOffsetByBatchNumber() { + V1FlowDetokenizeResponseObject record = V1FlowDetokenizeResponseObject.builder() + .token("token1") + .build(); + V1FlowDetokenizeResponse response = V1FlowDetokenizeResponse.builder() + .response(Collections.singletonList(record)) + .build(); + + BulkDetokenizeResponse result = Utils.formatBulkDetokenizeResponse(response, 2, 50, new HashMap<>()); + + Assert.assertEquals(100, result.getRecords().get(0).getIndex()); } @Test @@ -1195,8 +1365,9 @@ public void testFormatBulkDetokenizeResponse_errorWithMissingHttpCodeDefaultsTo5 BulkDetokenizeResponse result = Utils.formatBulkDetokenizeResponse(response, 0, 50, new HashMap<>()); - Assert.assertEquals(1, result.getErrors().size()); - Assert.assertEquals(500, result.getErrors().get(0).getCode()); + Assert.assertEquals(1, result.getRecords().size()); + Assert.assertEquals(500, result.getRecords().get(0).getHttpCode()); + Assert.assertEquals("token not found", result.getRecords().get(0).getError()); } @Test @@ -1322,243 +1493,7 @@ public void testFormatBulkTokenizeResponse_emptyResponseReturnsNull() { Assert.assertNull(Utils.formatBulkTokenizeResponse(response, batchRequest, 0, 50, new HashMap<>())); } - // ── getQueryRequestBody ──────────────────────────────────────────────────── - - @Test - public void testGetQueryRequestBody_buildsCorrectRequest() { - com.skyflow.vault.data.QueryRequest request = com.skyflow.vault.data.QueryRequest.builder() - .query("SELECT * FROM table1") - .build(); - - com.skyflow.generated.rest.resources.records.requests.V1ExecuteQueryRequest body = - Utils.getQueryRequestBody(request, "vault123"); - - Assert.assertEquals("vault123", body.getVaultId().get()); - Assert.assertEquals("SELECT * FROM table1", body.getQuery().get()); - } - - // ── buildQueryResponse ───────────────────────────────────────────────────── - - @Test - public void testBuildQueryResponse_success() { - Map data = new HashMap<>(); - data.put("skyflowId", "sky-id-1"); - data.put("name", "john"); - com.skyflow.generated.rest.types.V1ExecuteQueryRecordResponse record = - com.skyflow.generated.rest.types.V1ExecuteQueryRecordResponse.builder().data(data).build(); - com.skyflow.generated.rest.types.V1ExecuteQueryResponse response = - com.skyflow.generated.rest.types.V1ExecuteQueryResponse.builder() - .records(Collections.singletonList(record)) - .build(); - - com.skyflow.vault.data.QueryResponse result = Utils.buildQueryResponse(response); - - Assert.assertEquals(1, result.getFields().size()); - Assert.assertEquals("sky-id-1", result.getFields().get(0).get("skyflowId")); - Assert.assertEquals("john", result.getFields().get(0).get("name")); - Assert.assertNull(result.getErrors()); - } - - @Test - public void testBuildQueryResponse_emptyResponse() { - com.skyflow.generated.rest.types.V1ExecuteQueryResponse response = - com.skyflow.generated.rest.types.V1ExecuteQueryResponse.builder().build(); - - com.skyflow.vault.data.QueryResponse result = Utils.buildQueryResponse(response); - - Assert.assertTrue(result.getFields().isEmpty()); - } - - @Test - public void testBuildQueryResponse_nullResponse() { - com.skyflow.vault.data.QueryResponse result = Utils.buildQueryResponse(null); - - Assert.assertTrue(result.getFields().isEmpty()); - } - - // ── getGetRequestBody ────────────────────────────────────────────────────── - - @Test - public void testGetGetRequestBody_withIds() { - ArrayList ids = new ArrayList<>(); - ids.add("id1"); - com.skyflow.vault.data.GetRequest request = com.skyflow.vault.data.GetRequest.builder() - .table("table1") - .ids(ids) - .build(); - - com.skyflow.generated.rest.resources.flowservice.requests.V1GetRequest body = - Utils.getGetRequestBody(request, "vault123"); - - Assert.assertEquals("vault123", body.getVaultId().get()); - Assert.assertEquals("table1", body.getTableName().get()); - Assert.assertEquals(ids, body.getSkyflowIDs().get()); - Assert.assertFalse(body.getUniqueValues().isPresent()); - Assert.assertFalse(body.getColumnRedactions().isPresent()); - } - - @Test - public void testGetGetRequestBody_withUniqueValuesAndColumnRedactions() { - Map uniqueValue = new HashMap<>(); - uniqueValue.put("email", "john@example.com"); - List> uniqueValues = Collections.singletonList(uniqueValue); - - com.skyflow.vault.data.ColumnRedaction redaction = com.skyflow.vault.data.ColumnRedaction.builder() - .columnName("email") - .redaction("MASKED") - .build(); + // Tests for getQueryRequestBody / buildQueryResponse / getGetRequestBody / buildGetResponse + // were removed: get and query Utils helpers no longer exist (bulk-only module). - com.skyflow.vault.data.GetRequest request = com.skyflow.vault.data.GetRequest.builder() - .table("table1") - .uniqueValues(uniqueValues) - .columnRedactions(Collections.singletonList(redaction)) - .fields(new ArrayList<>(Collections.singletonList("name"))) - .limit(10) - .offset(0) - .build(); - - com.skyflow.generated.rest.resources.flowservice.requests.V1GetRequest body = - Utils.getGetRequestBody(request, "vault123"); - - Assert.assertTrue(body.getUniqueValues().isPresent()); - Assert.assertEquals(uniqueValue, body.getUniqueValues().get().get(0).getData().get()); - Assert.assertTrue(body.getColumnRedactions().isPresent()); - Assert.assertEquals("email", body.getColumnRedactions().get().get(0).getColumnName().get()); - Assert.assertEquals("MASKED", body.getColumnRedactions().get().get(0).getRedaction().get()); - Assert.assertEquals(Collections.singletonList("name"), body.getColumns().get()); - Assert.assertEquals(Integer.valueOf(10), body.getLimit().get()); - Assert.assertEquals(Integer.valueOf(0), body.getOffset().get()); - } - - // ── buildGetResponse ─────────────────────────────────────────────────────── - - @Test - public void testBuildGetResponse_success() { - Map data = new HashMap<>(); - data.put("name", "john"); - V1RecordResponseObject record = V1RecordResponseObject.builder() - .skyflowId("sky-id-1") - .data(data) - .build(); - com.skyflow.generated.rest.types.V1GetResponse response = - com.skyflow.generated.rest.types.V1GetResponse.builder() - .records(Collections.singletonList(record)) - .build(); - - com.skyflow.vault.data.GetResponse result = Utils.buildGetResponse(response); - - Assert.assertEquals(1, result.getData().size()); - Assert.assertEquals("sky-id-1", result.getData().get(0).get("skyflowId")); - Assert.assertEquals("john", result.getData().get(0).get("name")); - Assert.assertTrue(result.getErrors().isEmpty()); - } - - @Test - public void testBuildGetResponse_error() { - V1RecordResponseObject record = V1RecordResponseObject.builder() - .skyflowId("sky-id-1") - .error("not found") - .httpCode(404) - .build(); - com.skyflow.generated.rest.types.V1GetResponse response = - com.skyflow.generated.rest.types.V1GetResponse.builder() - .records(Collections.singletonList(record)) - .build(); - - com.skyflow.vault.data.GetResponse result = Utils.buildGetResponse(response); - - Assert.assertTrue(result.getData().isEmpty()); - Assert.assertEquals(1, result.getErrors().size()); - Assert.assertEquals("not found", result.getErrors().get(0).get("error")); - Assert.assertEquals(404, result.getErrors().get(0).get("httpCode")); - } - - @Test - public void testBuildGetResponse_nullResponse() { - com.skyflow.vault.data.GetResponse result = Utils.buildGetResponse(null); - - Assert.assertTrue(result.getData().isEmpty()); - Assert.assertTrue(result.getErrors().isEmpty()); - } - - @Test - public void testBuildGetResponse_successIncludesTableName() { - Map data = new HashMap<>(); - data.put("name", "john"); - V1RecordResponseObject record = V1RecordResponseObject.builder() - .skyflowId("sky-id-1") - .tableName("table1") - .data(data) - .build(); - com.skyflow.generated.rest.types.V1GetResponse response = - com.skyflow.generated.rest.types.V1GetResponse.builder() - .records(Collections.singletonList(record)) - .build(); - - com.skyflow.vault.data.GetResponse result = Utils.buildGetResponse(response); - - Assert.assertEquals("table1", result.getData().get(0).get("tableName")); - } - - // ── getGetRequestBody (multi-table records) ─────────────────────────────── - - @Test - public void testGetGetRequestBody_withRecords_buildsMultiTableRequest() { - com.skyflow.vault.data.GetRecordRequest record1 = com.skyflow.vault.data.GetRecordRequest.builder() - .table("table1") - .ids(new ArrayList<>(Collections.singletonList("id1"))) - .build(); - Map uniqueValue = new HashMap<>(); - uniqueValue.put("email", "john@example.com"); - com.skyflow.vault.data.ColumnRedaction redaction = com.skyflow.vault.data.ColumnRedaction.builder() - .columnName("email") - .redaction("MASKED") - .build(); - com.skyflow.vault.data.GetRecordRequest record2 = com.skyflow.vault.data.GetRecordRequest.builder() - .table("table2") - .uniqueValues(Collections.singletonList(uniqueValue)) - .columnRedactions(Collections.singletonList(redaction)) - .build(); - - com.skyflow.vault.data.GetRequest request = com.skyflow.vault.data.GetRequest.builder() - .records(java.util.Arrays.asList(record1, record2)) - .build(); - - com.skyflow.generated.rest.resources.flowservice.requests.V1GetRequest body = - Utils.getGetRequestBody(request, "vault123"); - - Assert.assertEquals("vault123", body.getVaultId().get()); - Assert.assertFalse(body.getTableName().isPresent()); - Assert.assertTrue(body.getRecords().isPresent()); - Assert.assertEquals(2, body.getRecords().get().size()); - - com.skyflow.generated.rest.types.V1GetRequestData data1 = body.getRecords().get().get(0); - Assert.assertEquals("table1", data1.getTableName().get()); - Assert.assertEquals(Collections.singletonList("id1"), data1.getSkyflowIDs().get()); - - com.skyflow.generated.rest.types.V1GetRequestData data2 = body.getRecords().get().get(1); - Assert.assertEquals("table2", data2.getTableName().get()); - Assert.assertEquals(uniqueValue, data2.getUniqueValues().get().get(0).getData().get()); - Assert.assertEquals("email", data2.getColumnRedactions().get().get(0).getColumnName().get()); - Assert.assertEquals("MASKED", data2.getColumnRedactions().get().get(0).getRedaction().get()); - } - - @Test - public void testGetGetRequestBody_withRecords_topLevelLimitOffsetStillApply() { - com.skyflow.vault.data.GetRecordRequest record = com.skyflow.vault.data.GetRecordRequest.builder() - .table("table1") - .ids(new ArrayList<>(Collections.singletonList("id1"))) - .build(); - com.skyflow.vault.data.GetRequest request = com.skyflow.vault.data.GetRequest.builder() - .records(Collections.singletonList(record)) - .limit(5) - .offset(1) - .build(); - - com.skyflow.generated.rest.resources.flowservice.requests.V1GetRequest body = - Utils.getGetRequestBody(request, "vault123"); - - Assert.assertEquals(Integer.valueOf(5), body.getLimit().get()); - Assert.assertEquals(Integer.valueOf(1), body.getOffset().get()); - } } diff --git a/flowvault/src/test/java/com/skyflow/utils/validations/ValidationsTests.java b/flowvault/src/test/java/com/skyflow/utils/validations/ValidationsTests.java index 0980e08c..c9e8dacd 100644 --- a/flowvault/src/test/java/com/skyflow/utils/validations/ValidationsTests.java +++ b/flowvault/src/test/java/com/skyflow/utils/validations/ValidationsTests.java @@ -3,22 +3,19 @@ import com.skyflow.config.Credentials; import com.skyflow.config.VaultConfig; import com.skyflow.enums.Env; +import com.skyflow.errors.ErrorMessage; import com.skyflow.errors.SkyflowException; import com.skyflow.vault.data.BulkDeleteTokensRequest; -import com.skyflow.vault.data.BulkInsertRecord; +import com.skyflow.vault.data.BulkInsertRequestRecord; import com.skyflow.vault.data.BulkInsertRequest; import com.skyflow.vault.data.BulkDetokenizeRequest; -import com.skyflow.vault.data.BulkTokenGroupRedactions; import com.skyflow.vault.data.BulkTokenizeRecord; import com.skyflow.vault.data.BulkTokenizeRequest; -import com.skyflow.vault.data.DeleteTokensRequest; -import com.skyflow.vault.data.DetokenizeData; import com.skyflow.vault.data.DetokenizeRequest; -import com.skyflow.vault.data.InsertRecord; +import com.skyflow.vault.data.InsertRequestRecord; import com.skyflow.vault.data.InsertRequest; import com.skyflow.vault.data.TokenGroupRedactions; -import com.skyflow.vault.data.TokenizeRecord; -import com.skyflow.vault.data.TokenizeRequest; +import com.skyflow.vault.data.UpsertOptions; import org.junit.Assert; import org.junit.Test; @@ -34,225 +31,8 @@ public class ValidationsTests { private static final String EXCEPTION_NOT_THROWN = "Should have thrown an exception"; private static final String INVALID_EXCEPTION_THROWN = "Should not have thrown any exception"; - // ── validateTokenizeRequest ────────────────────────────────────────────── - - @Test - public void testValidateTokenizeRequest_nullRequest() { - try { - Validations.validateTokenizeRequest(null); - Assert.fail(EXCEPTION_NOT_THROWN); - } catch (SkyflowException e) { - Assert.assertNotNull(e.getMessage()); - } - } - - @Test - public void testValidateTokenizeRequest_nullData() { - TokenizeRequest request = TokenizeRequest.builder().data(null).build(); - try { - Validations.validateTokenizeRequest(request); - Assert.fail(EXCEPTION_NOT_THROWN); - } catch (SkyflowException e) { - Assert.assertNotNull(e.getMessage()); - } - } - - @Test - public void testValidateTokenizeRequest_emptyData() { - TokenizeRequest request = TokenizeRequest.builder().data(new ArrayList<>()).build(); - try { - Validations.validateTokenizeRequest(request); - Assert.fail(EXCEPTION_NOT_THROWN); - } catch (SkyflowException e) { - Assert.assertNotNull(e.getMessage()); - } - } - - @Test - public void testValidateTokenizeRequest_largeValidListDoesNotThrow() { - ArrayList records = new ArrayList<>(); - for (int i = 0; i < 100; i++) { - records.add(TokenizeRecord.builder() - .value("value" + i) - .tokenGroupNames(Collections.singletonList("group1")) - .build()); - } - TokenizeRequest request = TokenizeRequest.builder().data(records).build(); - try { - Validations.validateTokenizeRequest(request); - } catch (SkyflowException e) { - Assert.fail(INVALID_EXCEPTION_THROWN); - } - } - - @Test - public void testValidateTokenizeRequest_nullRecordInList() { - ArrayList records = new ArrayList<>(); - records.add(null); - TokenizeRequest request = TokenizeRequest.builder().data(records).build(); - try { - Validations.validateTokenizeRequest(request); - Assert.fail(EXCEPTION_NOT_THROWN); - } catch (SkyflowException e) { - Assert.assertNotNull(e.getMessage()); - } - } - - @Test - public void testValidateTokenizeRequest_nullValue() { - ArrayList records = new ArrayList<>(); - records.add(TokenizeRecord.builder().value(null).tokenGroupNames(Collections.singletonList("group1")).build()); - TokenizeRequest request = TokenizeRequest.builder().data(records).build(); - try { - Validations.validateTokenizeRequest(request); - Assert.fail(EXCEPTION_NOT_THROWN); - } catch (SkyflowException e) { - Assert.assertNotNull(e.getMessage()); - } - } - - @Test - public void testValidateTokenizeRequest_blankStringValue() { - ArrayList records = new ArrayList<>(); - records.add(TokenizeRecord.builder().value(" ").tokenGroupNames(Collections.singletonList("group1")).build()); - TokenizeRequest request = TokenizeRequest.builder().data(records).build(); - try { - Validations.validateTokenizeRequest(request); - Assert.fail(EXCEPTION_NOT_THROWN); - } catch (SkyflowException e) { - Assert.assertNotNull(e.getMessage()); - } - } - - @Test - public void testValidateTokenizeRequest_nullTokenGroupNames() { - ArrayList records = new ArrayList<>(); - records.add(TokenizeRecord.builder().value("value1").tokenGroupNames(null).build()); - TokenizeRequest request = TokenizeRequest.builder().data(records).build(); - try { - Validations.validateTokenizeRequest(request); - Assert.fail(EXCEPTION_NOT_THROWN); - } catch (SkyflowException e) { - Assert.assertNotNull(e.getMessage()); - } - } - - @Test - public void testValidateTokenizeRequest_emptyTokenGroupNames() { - ArrayList records = new ArrayList<>(); - records.add(TokenizeRecord.builder().value("value1").tokenGroupNames(new ArrayList<>()).build()); - TokenizeRequest request = TokenizeRequest.builder().data(records).build(); - try { - Validations.validateTokenizeRequest(request); - Assert.fail(EXCEPTION_NOT_THROWN); - } catch (SkyflowException e) { - Assert.assertNotNull(e.getMessage()); - } - } - - @Test - public void testValidateTokenizeRequest_blankTokenGroupNameAtIndex() { - ArrayList records = new ArrayList<>(); - records.add(TokenizeRecord.builder() - .value("value1") - .tokenGroupNames(Arrays.asList("group1", " ")) - .build()); - TokenizeRequest request = TokenizeRequest.builder().data(records).build(); - try { - Validations.validateTokenizeRequest(request); - Assert.fail(EXCEPTION_NOT_THROWN); - } catch (SkyflowException e) { - Assert.assertNotNull(e.getMessage()); - } - } - - @Test - public void testValidateTokenizeRequest_validRequest() { - ArrayList records = new ArrayList<>(); - records.add(TokenizeRecord.builder() - .value("value1") - .tokenGroupNames(Collections.singletonList("group1")) - .build()); - TokenizeRequest request = TokenizeRequest.builder().data(records).build(); - try { - Validations.validateTokenizeRequest(request); - } catch (SkyflowException e) { - Assert.fail(INVALID_EXCEPTION_THROWN); - } - } - - // ── validateDeleteTokensRequest ────────────────────────────────────────── - - @Test - public void testValidateDeleteTokensRequest_nullRequest() { - try { - Validations.validateDeleteTokensRequest(null); - Assert.fail(EXCEPTION_NOT_THROWN); - } catch (SkyflowException e) { - Assert.assertNotNull(e.getMessage()); - } - } - - @Test - public void testValidateDeleteTokensRequest_nullTokens() { - DeleteTokensRequest request = DeleteTokensRequest.builder().tokens(null).build(); - try { - Validations.validateDeleteTokensRequest(request); - Assert.fail(EXCEPTION_NOT_THROWN); - } catch (SkyflowException e) { - Assert.assertNotNull(e.getMessage()); - } - } - - @Test - public void testValidateDeleteTokensRequest_emptyTokens() { - DeleteTokensRequest request = DeleteTokensRequest.builder().tokens(new ArrayList<>()).build(); - try { - Validations.validateDeleteTokensRequest(request); - Assert.fail(EXCEPTION_NOT_THROWN); - } catch (SkyflowException e) { - Assert.assertNotNull(e.getMessage()); - } - } - - @Test - public void testValidateDeleteTokensRequest_largeValidListDoesNotThrow() { - List tokens = new ArrayList<>(); - for (int i = 0; i < 100; i++) { - tokens.add("token" + i); - } - DeleteTokensRequest request = DeleteTokensRequest.builder().tokens(tokens).build(); - try { - Validations.validateDeleteTokensRequest(request); - } catch (SkyflowException e) { - Assert.fail(INVALID_EXCEPTION_THROWN); - } - } - - @Test - public void testValidateDeleteTokensRequest_blankTokenAtIndex() { - DeleteTokensRequest request = DeleteTokensRequest.builder() - .tokens(Arrays.asList("token1", " ")) - .build(); - try { - Validations.validateDeleteTokensRequest(request); - Assert.fail(EXCEPTION_NOT_THROWN); - } catch (SkyflowException e) { - Assert.assertNotNull(e.getMessage()); - } - } - - @Test - public void testValidateDeleteTokensRequest_validRequest() { - DeleteTokensRequest request = DeleteTokensRequest.builder() - .tokens(Collections.singletonList("token1")) - .build(); - try { - Validations.validateDeleteTokensRequest(request); - } catch (SkyflowException e) { - Assert.fail(INVALID_EXCEPTION_THROWN); - } - } + // Tests for validateTokenizeRequest / validateDeleteTokensRequest were removed: + // those unary validators no longer exist (bulk-only module). // ── validateCredentials ─────────────────────────────────────────────────── @@ -492,7 +272,7 @@ public void testValidateVaultConfiguration_emptyVaultUrl() { VaultConfig config = new VaultConfig(); config.setVaultId("vault123"); config.setClusterId("cluster1"); - config.setVaultURL(" "); + config.setVaultUrl(" "); try { Validations.validateVaultConfiguration(config); Assert.fail(EXCEPTION_NOT_THROWN); @@ -505,7 +285,7 @@ public void testValidateVaultConfiguration_emptyVaultUrl() { public void testValidateVaultConfiguration_invalidVaultUrlFormat() { VaultConfig config = new VaultConfig(); config.setVaultId("vault123"); - config.setVaultURL("http://not-https.example.com"); + config.setVaultUrl("http://not-https.example.com"); try { Validations.validateVaultConfiguration(config); Assert.fail(EXCEPTION_NOT_THROWN); @@ -531,7 +311,7 @@ public void testValidateVaultConfiguration_validWithClusterId() { public void testValidateVaultConfiguration_validWithVaultUrl() { VaultConfig config = new VaultConfig(); config.setVaultId("vault123"); - config.setVaultURL("https://myvault.example.com"); + config.setVaultUrl("https://myvault.example.com"); try { Validations.validateVaultConfiguration(config); } catch (SkyflowException e) { @@ -569,7 +349,7 @@ public void testValidateInsertRequest_nullRequest() { @Test public void testValidateInsertRequest_nullRecords() { - InsertRequest request = InsertRequest.builder().table("table1").records(null).build(); + InsertRequest request = InsertRequest.builder().records(null).build(); try { Validations.validateInsertRequest(request); Assert.fail(EXCEPTION_NOT_THROWN); @@ -580,7 +360,7 @@ public void testValidateInsertRequest_nullRecords() { @Test public void testValidateInsertRequest_emptyRecords() { - InsertRequest request = InsertRequest.builder().table("table1").records(new ArrayList<>()).build(); + InsertRequest request = InsertRequest.builder().records(new ArrayList<>()).build(); try { Validations.validateInsertRequest(request); Assert.fail(EXCEPTION_NOT_THROWN); @@ -591,9 +371,9 @@ public void testValidateInsertRequest_emptyRecords() { @Test public void testValidateInsertRequest_nullRecordInList() { - ArrayList records = new ArrayList<>(); + ArrayList records = new ArrayList<>(); records.add(null); - InsertRequest request = InsertRequest.builder().table("table1").records(records).build(); + InsertRequest request = InsertRequest.builder().records(records).build(); try { Validations.validateInsertRequest(request); Assert.fail(EXCEPTION_NOT_THROWN); @@ -602,13 +382,20 @@ public void testValidateInsertRequest_nullRecordInList() { } } + // NOTE: a table name may now be supplied at either the record level or the request level. + // EmptyTable is thrown only when it is missing from both. Specifying it at both levels is + // currently accepted (no conflict error) — that decision is deliberately deferred. Likewise + // the old "upsert present at record/request level requires table at the other level" checks are + // gone, since upsert is no longer coupled to where the table is specified — it's just an + // UpsertOptions object validated the same way at either level (see the upsert tests below). + @Test - public void testValidateInsertRequest_tableSpecifiedAtBothPlaces() { + public void testValidateInsertRequest_missingTableNameInRecordThrowsEmptyTable() { Map data = new HashMap<>(); data.put("name", "john"); - ArrayList records = new ArrayList<>(); - records.add(InsertRecord.builder().table("recordTable").data(data).build()); - InsertRequest request = InsertRequest.builder().table("requestTable").records(records).build(); + ArrayList records = new ArrayList<>(); + records.add(InsertRequestRecord.builder().data(data).build()); + InsertRequest request = InsertRequest.builder().records(records).build(); try { Validations.validateInsertRequest(request); Assert.fail(EXCEPTION_NOT_THROWN); @@ -618,11 +405,11 @@ public void testValidateInsertRequest_tableSpecifiedAtBothPlaces() { } @Test - public void testValidateInsertRequest_tableMissingAtBothPlaces() { + public void testValidateInsertRequest_emptyTableNameInRecordThrowsEmptyTable() { Map data = new HashMap<>(); data.put("name", "john"); - ArrayList records = new ArrayList<>(); - records.add(InsertRecord.builder().data(data).build()); + ArrayList records = new ArrayList<>(); + records.add(InsertRequestRecord.builder().tableName(" ").data(data).build()); InsertRequest request = InsertRequest.builder().records(records).build(); try { Validations.validateInsertRequest(request); @@ -633,32 +420,29 @@ public void testValidateInsertRequest_tableMissingAtBothPlaces() { } @Test - public void testValidateInsertRequest_upsertAtRecordLevelWhenTableAtRequestLevel() { + public void testValidateInsertRequest_requestLevelTableNameSatisfiesRecordsWithoutOne() { Map data = new HashMap<>(); data.put("name", "john"); - ArrayList records = new ArrayList<>(); - records.add(InsertRecord.builder().data(data).upsert(Collections.singletonList("email")).build()); - InsertRequest request = InsertRequest.builder() - .table("table1") - .records(records) - .build(); + ArrayList records = new ArrayList<>(); + records.add(InsertRequestRecord.builder().data(data).build()); + InsertRequest request = InsertRequest.builder().tableName("table1").records(records).build(); try { Validations.validateInsertRequest(request); - Assert.fail(EXCEPTION_NOT_THROWN); } catch (SkyflowException e) { - Assert.assertNotNull(e.getMessage()); + Assert.fail(INVALID_EXCEPTION_THROWN); } } @Test - public void testValidateInsertRequest_upsertAtRequestLevelWhenNoTableAtRequestLevel() { + public void testValidateInsertRequest_emptyUpsertAtRequestLevel() { Map data = new HashMap<>(); data.put("name", "john"); - ArrayList records = new ArrayList<>(); - records.add(InsertRecord.builder().table("table1").data(data).build()); + ArrayList records = new ArrayList<>(); + records.add(InsertRequestRecord.builder().tableName("table1").data(data).build()); + UpsertOptions upsert = UpsertOptions.builder().uniqueColumns(new ArrayList<>()).build(); InsertRequest request = InsertRequest.builder() .records(records) - .upsert(Collections.singletonList("email")) + .upsert(upsert) .build(); try { Validations.validateInsertRequest(request); @@ -669,16 +453,13 @@ public void testValidateInsertRequest_upsertAtRequestLevelWhenNoTableAtRequestLe } @Test - public void testValidateInsertRequest_emptyUpsertAtRequestLevel() { + public void testValidateInsertRequest_emptyUpsertAtRecordLevel() { Map data = new HashMap<>(); data.put("name", "john"); - ArrayList records = new ArrayList<>(); - records.add(InsertRecord.builder().data(data).build()); - InsertRequest request = InsertRequest.builder() - .table("table1") - .records(records) - .upsert(new ArrayList<>()) - .build(); + UpsertOptions upsert = UpsertOptions.builder().uniqueColumns(new ArrayList<>()).build(); + ArrayList records = new ArrayList<>(); + records.add(InsertRequestRecord.builder().tableName("table1").data(data).upsert(upsert).build()); + InsertRequest request = InsertRequest.builder().records(records).build(); try { Validations.validateInsertRequest(request); Assert.fail(EXCEPTION_NOT_THROWN); @@ -691,8 +472,8 @@ public void testValidateInsertRequest_emptyUpsertAtRequestLevel() { public void testValidateInsertRequest_emptyOrNullKeyInData() { Map data = new HashMap<>(); data.put("", "john"); - ArrayList records = new ArrayList<>(); - records.add(InsertRecord.builder().table("table1").data(data).build()); + ArrayList records = new ArrayList<>(); + records.add(InsertRequestRecord.builder().tableName("table1").data(data).build()); InsertRequest request = InsertRequest.builder().records(records).build(); try { Validations.validateInsertRequest(request); @@ -706,8 +487,8 @@ public void testValidateInsertRequest_emptyOrNullKeyInData() { public void testValidateInsertRequest_emptyOrNullValueInData() { Map data = new HashMap<>(); data.put("name", ""); - ArrayList records = new ArrayList<>(); - records.add(InsertRecord.builder().table("table1").data(data).build()); + ArrayList records = new ArrayList<>(); + records.add(InsertRequestRecord.builder().tableName("table1").data(data).build()); InsertRequest request = InsertRequest.builder().records(records).build(); try { Validations.validateInsertRequest(request); @@ -721,8 +502,62 @@ public void testValidateInsertRequest_emptyOrNullValueInData() { public void testValidateInsertRequest_validRequest() { Map data = new HashMap<>(); data.put("name", "john"); - ArrayList records = new ArrayList<>(); - records.add(InsertRecord.builder().table("table1").data(data).build()); + ArrayList records = new ArrayList<>(); + records.add(InsertRequestRecord.builder().tableName("table1").data(data).build()); + InsertRequest request = InsertRequest.builder().records(records).build(); + try { + Validations.validateInsertRequest(request); + } catch (SkyflowException e) { + Assert.fail(INVALID_EXCEPTION_THROWN); + } + } + + @Test + public void testValidateInsertRequest_validRequestWithUpsertAtRequestLevel() { + Map data = new HashMap<>(); + data.put("name", "john"); + ArrayList records = new ArrayList<>(); + records.add(InsertRequestRecord.builder().data(data).build()); + UpsertOptions upsert = UpsertOptions.builder() + .uniqueColumns(Collections.singletonList("email")) + .updateType("UPDATE") + .build(); + InsertRequest request = InsertRequest.builder() + .tableName("table1") + .records(records) + .upsert(upsert) + .build(); + try { + Validations.validateInsertRequest(request); + } catch (SkyflowException e) { + Assert.fail(INVALID_EXCEPTION_THROWN); + } + } + + @Test + public void testValidateInsertRequest_validRequestWithUpsertAtRecordLevel() { + Map data = new HashMap<>(); + data.put("name", "john"); + UpsertOptions upsert = UpsertOptions.builder() + .uniqueColumns(Collections.singletonList("email")) + .updateType("REPLACE") + .build(); + ArrayList records = new ArrayList<>(); + records.add(InsertRequestRecord.builder().tableName("table1").data(data).upsert(upsert).build()); + InsertRequest request = InsertRequest.builder().records(records).build(); + try { + Validations.validateInsertRequest(request); + } catch (SkyflowException e) { + Assert.fail(INVALID_EXCEPTION_THROWN); + } + } + + @Test + public void testValidateInsertRequest_validRequestWithTokens() { + Map tokens = new HashMap<>(); + tokens.put("name", "tok-abc"); + ArrayList records = new ArrayList<>(); + records.add(InsertRequestRecord.builder().tableName("table1").tokens(tokens).build()); InsertRequest request = InsertRequest.builder().records(records).build(); try { Validations.validateInsertRequest(request); @@ -744,8 +579,8 @@ public void testValidateDetokenizeRequest_nullRequest() { } @Test - public void testValidateDetokenizeRequest_nullDetokenizeData() { - DetokenizeRequest request = DetokenizeRequest.builder().detokenizeData(null).build(); + public void testValidateDetokenizeRequest_nullTokens() { + DetokenizeRequest request = DetokenizeRequest.builder().tokens(null).build(); try { Validations.validateDetokenizeRequest(request); Assert.fail(EXCEPTION_NOT_THROWN); @@ -755,8 +590,8 @@ public void testValidateDetokenizeRequest_nullDetokenizeData() { } @Test - public void testValidateDetokenizeRequest_emptyDetokenizeData() { - DetokenizeRequest request = DetokenizeRequest.builder().detokenizeData(new ArrayList<>()).build(); + public void testValidateDetokenizeRequest_emptyTokens() { + DetokenizeRequest request = DetokenizeRequest.builder().tokens(new ArrayList<>()).build(); try { Validations.validateDetokenizeRequest(request); Assert.fail(EXCEPTION_NOT_THROWN); @@ -766,10 +601,10 @@ public void testValidateDetokenizeRequest_emptyDetokenizeData() { } @Test - public void testValidateDetokenizeRequest_nullEntryInDetokenizeData() { - ArrayList data = new ArrayList<>(); - data.add(null); - DetokenizeRequest request = DetokenizeRequest.builder().detokenizeData(data).build(); + public void testValidateDetokenizeRequest_nullTokenInList() { + List tokens = new ArrayList<>(); + tokens.add(null); + DetokenizeRequest request = DetokenizeRequest.builder().tokens(tokens).build(); try { Validations.validateDetokenizeRequest(request); Assert.fail(EXCEPTION_NOT_THROWN); @@ -779,10 +614,10 @@ public void testValidateDetokenizeRequest_nullEntryInDetokenizeData() { } @Test - public void testValidateDetokenizeRequest_blankTokenInDetokenizeData() { - ArrayList data = new ArrayList<>(); - data.add(new DetokenizeData(" ")); - DetokenizeRequest request = DetokenizeRequest.builder().detokenizeData(data).build(); + public void testValidateDetokenizeRequest_blankTokenInList() { + DetokenizeRequest request = DetokenizeRequest.builder() + .tokens(Collections.singletonList(" ")) + .build(); try { Validations.validateDetokenizeRequest(request); Assert.fail(EXCEPTION_NOT_THROWN); @@ -793,12 +628,10 @@ public void testValidateDetokenizeRequest_blankTokenInDetokenizeData() { @Test public void testValidateDetokenizeRequest_nullTokenGroupRedactionInList() { - ArrayList data = new ArrayList<>(); - data.add(new DetokenizeData("token1")); List groupRedactions = new ArrayList<>(); groupRedactions.add(null); DetokenizeRequest request = DetokenizeRequest.builder() - .detokenizeData(data) + .tokens(Collections.singletonList("token1")) .tokenGroupRedactions(groupRedactions) .build(); try { @@ -811,12 +644,10 @@ public void testValidateDetokenizeRequest_nullTokenGroupRedactionInList() { @Test public void testValidateDetokenizeRequest_blankTokenGroupNameInRedaction() { - ArrayList data = new ArrayList<>(); - data.add(new DetokenizeData("token1")); List groupRedactions = Collections.singletonList( TokenGroupRedactions.builder().tokenGroupName(" ").redaction("MASKED").build()); DetokenizeRequest request = DetokenizeRequest.builder() - .detokenizeData(data) + .tokens(Collections.singletonList("token1")) .tokenGroupRedactions(groupRedactions) .build(); try { @@ -829,12 +660,10 @@ public void testValidateDetokenizeRequest_blankTokenGroupNameInRedaction() { @Test public void testValidateDetokenizeRequest_blankRedactionInGroup() { - ArrayList data = new ArrayList<>(); - data.add(new DetokenizeData("token1")); List groupRedactions = Collections.singletonList( TokenGroupRedactions.builder().tokenGroupName("group1").redaction(" ").build()); DetokenizeRequest request = DetokenizeRequest.builder() - .detokenizeData(data) + .tokens(Collections.singletonList("token1")) .tokenGroupRedactions(groupRedactions) .build(); try { @@ -847,9 +676,9 @@ public void testValidateDetokenizeRequest_blankRedactionInGroup() { @Test public void testValidateDetokenizeRequest_validRequest() { - ArrayList data = new ArrayList<>(); - data.add(new DetokenizeData("token1")); - DetokenizeRequest request = DetokenizeRequest.builder().detokenizeData(data).build(); + DetokenizeRequest request = DetokenizeRequest.builder() + .tokens(Collections.singletonList("token1")) + .build(); try { Validations.validateDetokenizeRequest(request); } catch (SkyflowException e) { @@ -859,12 +688,10 @@ public void testValidateDetokenizeRequest_validRequest() { @Test public void testValidateDetokenizeRequest_validRequestWithTokenGroupRedactions() { - ArrayList data = new ArrayList<>(); - data.add(new DetokenizeData("token1")); List groupRedactions = Collections.singletonList( TokenGroupRedactions.builder().tokenGroupName("group1").redaction("MASKED").build()); DetokenizeRequest request = DetokenizeRequest.builder() - .detokenizeData(data) + .tokens(Collections.singletonList("token1")) .tokenGroupRedactions(groupRedactions) .build(); try { @@ -910,7 +737,7 @@ public void testValidateBulkInsertRequest_emptyRecords() { @Test public void testValidateBulkInsertRequest_nullRecordInList() { - ArrayList records = new ArrayList<>(); + ArrayList records = new ArrayList<>(); records.add(null); BulkInsertRequest request = BulkInsertRequest.builder().records(records).build(); try { @@ -921,69 +748,94 @@ public void testValidateBulkInsertRequest_nullRecordInList() { } } + // Removed: testValidateBulkInsertRequest_tableSpecifiedAtBothPlaces — bulk insert now + // delegates to validateInsertRequest, which deliberately accepts a table name at both + // the request and record level (TableSpecifiedInRequestAndRecordObject is no longer thrown). + + // Removed: testValidateBulkInsertRequest_upsertAtRecordLevelWhenTableAtRequestLevel and + // testValidateBulkInsertRequest_upsertAtRequestLevelWhenNoTableAtRequestLevel — upsert is + // now an UpsertOptions object accepted at either level, so UpsertTableRequestAtRecordLevel / + // UpsertTableRequestAtRequestLevel are no longer thrown for insert. + @Test - public void testValidateBulkInsertRequest_tableSpecifiedAtBothPlaces() { + public void testValidateBulkInsertRequest_tableNotSpecifiedAnywhereThrowsEmptyTable() { Map data = new HashMap<>(); data.put("name", "john"); - ArrayList records = new ArrayList<>(); - records.add(BulkInsertRecord.builder().table("table1").data(data).build()); - BulkInsertRequest request = BulkInsertRequest.builder().table("table1").records(records).build(); + ArrayList records = new ArrayList<>(); + records.add(BulkInsertRequestRecord.builder().data(data).build()); + BulkInsertRequest request = BulkInsertRequest.builder().records(records).build(); try { Validations.validateBulkInsertRequest(request); Assert.fail(EXCEPTION_NOT_THROWN); } catch (SkyflowException e) { - Assert.assertNotNull(e.getMessage()); + Assert.assertEquals(ErrorMessage.TableNotSpecifiedInRequestAndRecordObject.getMessage(), e.getMessage()); } } @Test - public void testValidateBulkInsertRequest_tableNotSpecifiedAnywhere() { + public void testValidateBulkInsertRequest_requestLevelTableNameSatisfiesRecordsWithoutOne() { Map data = new HashMap<>(); data.put("name", "john"); - ArrayList records = new ArrayList<>(); - records.add(BulkInsertRecord.builder().data(data).build()); - BulkInsertRequest request = BulkInsertRequest.builder().records(records).build(); + ArrayList records = new ArrayList<>(); + records.add(BulkInsertRequestRecord.builder().data(data).build()); + BulkInsertRequest request = BulkInsertRequest.builder().tableName("table1").records(records).build(); + try { + Validations.validateBulkInsertRequest(request); + } catch (SkyflowException e) { + Assert.fail(INVALID_EXCEPTION_THROWN); + } + } + + @Test + public void testValidateBulkInsertRequest_tableNameAtBothLevelsThrows() { + // Table name must live at exactly one level, never both. + Map data = new HashMap<>(); + data.put("name", "john"); + ArrayList records = new ArrayList<>(); + records.add(BulkInsertRequestRecord.builder().tableName("table1").data(data).build()); + BulkInsertRequest request = BulkInsertRequest.builder().tableName("table1").records(records).build(); try { Validations.validateBulkInsertRequest(request); Assert.fail(EXCEPTION_NOT_THROWN); } catch (SkyflowException e) { - Assert.assertNotNull(e.getMessage()); + Assert.assertEquals(ErrorMessage.TableSpecifiedInRequestAndRecordObject.getMessage(), e.getMessage()); } } @Test - public void testValidateBulkInsertRequest_upsertAtRecordLevelWhenTableAtRequestLevel() { + public void testValidateBulkInsertRequest_emptyUpsertAtRecordLevel() { Map data = new HashMap<>(); data.put("name", "john"); - ArrayList records = new ArrayList<>(); - records.add(BulkInsertRecord.builder().data(data).upsert(Collections.singletonList("email")).build()); - BulkInsertRequest request = BulkInsertRequest.builder() - .table("table1") - .records(records) - .build(); + ArrayList records = new ArrayList<>(); + records.add(BulkInsertRequestRecord.builder() + .tableName("table1") + .data(data) + .upsert(UpsertOptions.builder().updateType("UPDATE").build()) + .build()); + BulkInsertRequest request = BulkInsertRequest.builder().records(records).build(); try { Validations.validateBulkInsertRequest(request); Assert.fail(EXCEPTION_NOT_THROWN); } catch (SkyflowException e) { - Assert.assertNotNull(e.getMessage()); + Assert.assertEquals(ErrorMessage.EmptyUpsertValues.getMessage(), e.getMessage()); } } @Test - public void testValidateBulkInsertRequest_upsertAtRequestLevelWhenNoTableAtRequestLevel() { + public void testValidateBulkInsertRequest_emptyUpsertAtRequestLevel() { Map data = new HashMap<>(); data.put("name", "john"); - ArrayList records = new ArrayList<>(); - records.add(BulkInsertRecord.builder().table("table1").data(data).build()); + ArrayList records = new ArrayList<>(); + records.add(BulkInsertRequestRecord.builder().tableName("table1").data(data).build()); BulkInsertRequest request = BulkInsertRequest.builder() .records(records) - .upsert(Collections.singletonList("email")) + .upsert(UpsertOptions.builder().uniqueColumns(new ArrayList<>()).build()) .build(); try { Validations.validateBulkInsertRequest(request); Assert.fail(EXCEPTION_NOT_THROWN); } catch (SkyflowException e) { - Assert.assertNotNull(e.getMessage()); + Assert.assertEquals(ErrorMessage.EmptyUpsertValues.getMessage(), e.getMessage()); } } @@ -991,8 +843,8 @@ public void testValidateBulkInsertRequest_upsertAtRequestLevelWhenNoTableAtReque public void testValidateBulkInsertRequest_emptyKeyInData() { Map data = new HashMap<>(); data.put("", "john"); - ArrayList records = new ArrayList<>(); - records.add(BulkInsertRecord.builder().table("table1").data(data).build()); + ArrayList records = new ArrayList<>(); + records.add(BulkInsertRequestRecord.builder().tableName("table1").data(data).build()); BulkInsertRequest request = BulkInsertRequest.builder().records(records).build(); try { Validations.validateBulkInsertRequest(request); @@ -1006,8 +858,8 @@ public void testValidateBulkInsertRequest_emptyKeyInData() { public void testValidateBulkInsertRequest_emptyValueInData() { Map data = new HashMap<>(); data.put("name", ""); - ArrayList records = new ArrayList<>(); - records.add(BulkInsertRecord.builder().table("table1").data(data).build()); + ArrayList records = new ArrayList<>(); + records.add(BulkInsertRequestRecord.builder().tableName("table1").data(data).build()); BulkInsertRequest request = BulkInsertRequest.builder().records(records).build(); try { Validations.validateBulkInsertRequest(request); @@ -1021,8 +873,8 @@ public void testValidateBulkInsertRequest_emptyValueInData() { public void testValidateBulkInsertRequest_validRequest() { Map data = new HashMap<>(); data.put("name", "john"); - ArrayList records = new ArrayList<>(); - records.add(BulkInsertRecord.builder().table("table1").data(data).build()); + ArrayList records = new ArrayList<>(); + records.add(BulkInsertRequestRecord.builder().tableName("table1").data(data).build()); BulkInsertRequest request = BulkInsertRequest.builder().records(records).build(); try { Validations.validateBulkInsertRequest(request); @@ -1032,14 +884,21 @@ public void testValidateBulkInsertRequest_validRequest() { } @Test - public void testValidateBulkInsertRequest_validRequestOver10000Records() { - // Bulk operations rely on batching, not a hard cap — must not throw for large lists. + public void testValidateBulkInsertRequest_validRequestWithUpsertAndTokens() { Map data = new HashMap<>(); data.put("name", "john"); - ArrayList records = new ArrayList<>(); - for (int i = 0; i < 10001; i++) { - records.add(BulkInsertRecord.builder().table("table1").data(data).build()); - } + Map tokens = new HashMap<>(); + tokens.put("name", "tok-abc"); + ArrayList records = new ArrayList<>(); + records.add(BulkInsertRequestRecord.builder() + .tableName("table1") + .data(data) + .tokens(tokens) + .upsert(UpsertOptions.builder() + .updateType("REPLACE") + .uniqueColumns(Collections.singletonList("email")) + .build()) + .build()); BulkInsertRequest request = BulkInsertRequest.builder().records(records).build(); try { Validations.validateBulkInsertRequest(request); @@ -1048,270 +907,356 @@ public void testValidateBulkInsertRequest_validRequestOver10000Records() { } } - // ── validateBulkDetokenizeRequest ────────────────────────────────────────── - @Test - public void testValidateBulkDetokenizeRequest_nullRequest() { + public void testValidateBulkInsertRequest_over10000RecordsThrows() { + // Constants.MAX_BULK_DATA_SIZE is a hard ceiling; batching splits the payload but + // does not lift it. + Map data = new HashMap<>(); + data.put("name", "john"); + ArrayList records = new ArrayList<>(); + for (int i = 0; i < 10001; i++) { + records.add(BulkInsertRequestRecord.builder().tableName("table1").data(data).build()); + } + BulkInsertRequest request = BulkInsertRequest.builder().records(records).build(); try { - Validations.validateBulkDetokenizeRequest(null); + Validations.validateBulkInsertRequest(request); Assert.fail(EXCEPTION_NOT_THROWN); } catch (SkyflowException e) { - Assert.assertNotNull(e.getMessage()); + Assert.assertEquals(ErrorMessage.RecordSizeExceedError.getMessage(), e.getMessage()); } } @Test - public void testValidateBulkDetokenizeRequest_nullTokens() { - BulkDetokenizeRequest request = BulkDetokenizeRequest.builder().tokens(null).build(); + public void testValidateBulkInsertRequest_exactly10000RecordsIsValid() { + Map data = new HashMap<>(); + data.put("name", "john"); + ArrayList records = new ArrayList<>(); + for (int i = 0; i < 10000; i++) { + records.add(BulkInsertRequestRecord.builder().tableName("table1").data(data).build()); + } + BulkInsertRequest request = BulkInsertRequest.builder().records(records).build(); try { - Validations.validateBulkDetokenizeRequest(request); - Assert.fail(EXCEPTION_NOT_THROWN); + Validations.validateBulkInsertRequest(request); } catch (SkyflowException e) { - Assert.assertNotNull(e.getMessage()); + Assert.fail(INVALID_EXCEPTION_THROWN); } } @Test - public void testValidateBulkDetokenizeRequest_emptyTokens() { - BulkDetokenizeRequest request = BulkDetokenizeRequest.builder().tokens(new ArrayList<>()).build(); + public void testValidateBulkDetokenizeRequest_over10000TokensThrows() { + List tokens = new ArrayList<>(); + for (int i = 0; i < 10001; i++) { + tokens.add("token-" + i); + } + BulkDetokenizeRequest request = BulkDetokenizeRequest.builder().tokens(tokens).build(); try { Validations.validateBulkDetokenizeRequest(request); Assert.fail(EXCEPTION_NOT_THROWN); } catch (SkyflowException e) { - Assert.assertNotNull(e.getMessage()); + Assert.assertEquals(ErrorMessage.TokensSizeExceedError.getMessage(), e.getMessage()); } } @Test - public void testValidateBulkDetokenizeRequest_nullTokenInList() { + public void testValidateBulkDeleteTokensRequest_over10000TokensThrows() { List tokens = new ArrayList<>(); - tokens.add(null); - BulkDetokenizeRequest request = BulkDetokenizeRequest.builder().tokens(tokens).build(); + for (int i = 0; i < 10001; i++) { + tokens.add("token-" + i); + } + BulkDeleteTokensRequest request = BulkDeleteTokensRequest.builder().tokens(tokens).build(); try { - Validations.validateBulkDetokenizeRequest(request); + Validations.validateBulkDeleteTokensRequest(request); Assert.fail(EXCEPTION_NOT_THROWN); } catch (SkyflowException e) { - Assert.assertNotNull(e.getMessage()); + Assert.assertEquals(ErrorMessage.DeleteTokensSizeExceedError.getMessage(), e.getMessage()); } } @Test - public void testValidateBulkDetokenizeRequest_nullRedactionGroupObject() { - List groupRedactions = new ArrayList<>(); - groupRedactions.add(null); - BulkDetokenizeRequest request = BulkDetokenizeRequest.builder() - .tokens(Collections.singletonList("token1")) - .tokenGroupRedactions(groupRedactions) - .build(); + public void testValidateBulkTokenizeRequest_over10000RecordsThrows() { + ArrayList records = new ArrayList<>(); + for (int i = 0; i < 10001; i++) { + records.add(BulkTokenizeRecord.builder() + .value("value-" + i) + .tokenGroupNames(Collections.singletonList("group")) + .build()); + } + BulkTokenizeRequest request = BulkTokenizeRequest.builder().data(records).build(); try { - Validations.validateBulkDetokenizeRequest(request); + Validations.validateBulkTokenizeRequest(request); Assert.fail(EXCEPTION_NOT_THROWN); } catch (SkyflowException e) { - Assert.assertNotNull(e.getMessage()); + Assert.assertEquals(ErrorMessage.TokenizeDataSizeExceedError.getMessage(), e.getMessage()); } } @Test - public void testValidateBulkDetokenizeRequest_nullGroupNameInRedaction() { - List groupRedactions = Collections.singletonList( - BulkTokenGroupRedactions.builder().redaction("MASKED").build()); - BulkDetokenizeRequest request = BulkDetokenizeRequest.builder() - .tokens(Collections.singletonList("token1")) - .tokenGroupRedactions(groupRedactions) + public void testValidateInsertRequest_unrecognizedUpdateTypeThrows() { + // Previously this was silently dropped during mapping; it is now rejected up front. + Map data = new HashMap<>(); + data.put("name", "john"); + ArrayList records = new ArrayList<>(); + records.add(InsertRequestRecord.builder().data(data).build()); + InsertRequest request = InsertRequest.builder() + .tableName("table1") + .records(records) + .upsert(UpsertOptions.builder() + .uniqueColumns(Collections.singletonList("email")) + .updateType("MERGE") + .build()) .build(); try { - Validations.validateBulkDetokenizeRequest(request); + Validations.validateInsertRequest(request); Assert.fail(EXCEPTION_NOT_THROWN); } catch (SkyflowException e) { - Assert.assertNotNull(e.getMessage()); + Assert.assertEquals(ErrorMessage.InvalidUpsertUpdateType.getMessage(), e.getMessage()); } } @Test - public void testValidateBulkDetokenizeRequest_nullRedactionInGroup() { - List groupRedactions = Collections.singletonList( - BulkTokenGroupRedactions.builder().tokenGroupName("group1").build()); - BulkDetokenizeRequest request = BulkDetokenizeRequest.builder() - .tokens(Collections.singletonList("token1")) - .tokenGroupRedactions(groupRedactions) + public void testValidateInsertRequest_updateTypeWithStrayWhitespaceThrows() { + Map data = new HashMap<>(); + data.put("name", "john"); + ArrayList records = new ArrayList<>(); + records.add(InsertRequestRecord.builder().data(data).build()); + InsertRequest request = InsertRequest.builder() + .tableName("table1") + .records(records) + .upsert(UpsertOptions.builder() + .uniqueColumns(Collections.singletonList("email")) + .updateType("update ") + .build()) .build(); try { - Validations.validateBulkDetokenizeRequest(request); + Validations.validateInsertRequest(request); Assert.fail(EXCEPTION_NOT_THROWN); } catch (SkyflowException e) { - Assert.assertNotNull(e.getMessage()); + Assert.assertEquals(ErrorMessage.InvalidUpsertUpdateType.getMessage(), e.getMessage()); } } @Test - public void testValidateBulkDetokenizeRequest_validRequest() { - BulkDetokenizeRequest request = BulkDetokenizeRequest.builder() - .tokens(Collections.singletonList("token1")) + public void testValidateInsertRequest_nullUpdateTypeIsValid() { + // updateType is optional; only a non-null unrecognized value is rejected. + Map data = new HashMap<>(); + data.put("name", "john"); + ArrayList records = new ArrayList<>(); + records.add(InsertRequestRecord.builder().data(data).build()); + InsertRequest request = InsertRequest.builder() + .tableName("table1") + .records(records) + .upsert(UpsertOptions.builder().uniqueColumns(Collections.singletonList("email")).build()) .build(); try { - Validations.validateBulkDetokenizeRequest(request); + Validations.validateInsertRequest(request); } catch (SkyflowException e) { Assert.fail(INVALID_EXCEPTION_THROWN); } } @Test - public void testValidateBulkDetokenizeRequest_validRequestWithRedactions() { - List groupRedactions = Collections.singletonList( - BulkTokenGroupRedactions.builder().tokenGroupName("group1").redaction("MASKED").build()); - BulkDetokenizeRequest request = BulkDetokenizeRequest.builder() - .tokens(Collections.singletonList("token1")) - .tokenGroupRedactions(groupRedactions) - .build(); + public void testValidateBulkInsertRequest_plainInsertRequestRecordRejected() { + // `records` is inherited as List, so a plain unary record is + // accepted by the compiler. Bulk must reject it, otherwise getRecordsToRetry() would + // hit a ClassCastException later. + Map data = new HashMap<>(); + data.put("name", "john"); + ArrayList records = new ArrayList<>(); + records.add(InsertRequestRecord.builder().tableName("table1").data(data).build()); + BulkInsertRequest request = BulkInsertRequest.builder().records(records).build(); try { - Validations.validateBulkDetokenizeRequest(request); + Validations.validateBulkInsertRequest(request); + Assert.fail(EXCEPTION_NOT_THROWN); } catch (SkyflowException e) { - Assert.fail(INVALID_EXCEPTION_THROWN); + Assert.assertEquals(ErrorMessage.InvalidRecord.getMessage(), e.getMessage()); } } - // ── validateBulkDeleteTokensRequest ──────────────────────────────────────── + // ── validateBulkInsertRequest: table-name presence rule ──────────────────── + // + // The rule is: a table name must be present at the request level OR on EVERY record. + // "Present" means non-null and non-blank at both levels (Validations trims before testing, + // and Utils.hasText applies the same test when mapping to the wire), so a blank record-level + // name is treated as absent and satisfied by the request-level one. @Test - public void testValidateBulkDeleteTokensRequest_nullRequest() { + public void testValidateBulkInsertRequest_requestLevelTableNameSatisfiesBlankRecordTableNames() { + Map data = new HashMap<>(); + data.put("name", "john"); + ArrayList records = new ArrayList<>(); + records.add(BulkInsertRequestRecord.builder().tableName(" ").data(data).build()); + records.add(BulkInsertRequestRecord.builder().tableName("").data(data).build()); + records.add(BulkInsertRequestRecord.builder().data(data).build()); + BulkInsertRequest request = BulkInsertRequest.builder().tableName("cards").records(records).build(); try { - Validations.validateBulkDeleteTokensRequest(null); - Assert.fail(EXCEPTION_NOT_THROWN); + Validations.validateBulkInsertRequest(request); } catch (SkyflowException e) { - Assert.assertNotNull(e.getMessage()); + Assert.fail(INVALID_EXCEPTION_THROWN); } } @Test - public void testValidateBulkDeleteTokensRequest_emptyTokens() { - BulkDeleteTokensRequest request = BulkDeleteTokensRequest.builder().tokens(new ArrayList<>()).build(); + public void testValidateBulkInsertRequest_noRequestLevelTableNameButEveryRecordHasOneIsValid() { + Map data = new HashMap<>(); + data.put("name", "john"); + ArrayList records = new ArrayList<>(); + records.add(BulkInsertRequestRecord.builder().tableName("cards").data(data).build()); + records.add(BulkInsertRequestRecord.builder().tableName("accounts").data(data).build()); + records.add(BulkInsertRequestRecord.builder().tableName("people").data(data).build()); + BulkInsertRequest request = BulkInsertRequest.builder().records(records).build(); try { - Validations.validateBulkDeleteTokensRequest(request); - Assert.fail(EXCEPTION_NOT_THROWN); + Validations.validateBulkInsertRequest(request); } catch (SkyflowException e) { - Assert.assertNotNull(e.getMessage()); + Assert.fail(INVALID_EXCEPTION_THROWN); } } @Test - public void testValidateBulkDeleteTokensRequest_emptyTokenInList() { - BulkDeleteTokensRequest request = BulkDeleteTokensRequest.builder() - .tokens(Arrays.asList("token1", " ")) - .build(); + public void testValidateBulkInsertRequest_noRequestLevelTableNameAndOneRecordMissingThrowsEmptyTable() { + Map data = new HashMap<>(); + data.put("name", "john"); + ArrayList records = new ArrayList<>(); + records.add(BulkInsertRequestRecord.builder().tableName("cards").data(data).build()); + records.add(BulkInsertRequestRecord.builder().data(data).build()); + records.add(BulkInsertRequestRecord.builder().tableName("people").data(data).build()); + BulkInsertRequest request = BulkInsertRequest.builder().records(records).build(); try { - Validations.validateBulkDeleteTokensRequest(request); + Validations.validateBulkInsertRequest(request); Assert.fail(EXCEPTION_NOT_THROWN); } catch (SkyflowException e) { - Assert.assertNotNull(e.getMessage()); + Assert.assertEquals(ErrorMessage.TableNotSpecifiedInRequestAndRecordObject.getMessage(), e.getMessage()); } } @Test - public void testValidateBulkDeleteTokensRequest_validRequest() { - BulkDeleteTokensRequest request = BulkDeleteTokensRequest.builder() - .tokens(Collections.singletonList("token1")) - .build(); - try { - Validations.validateBulkDeleteTokensRequest(request); - } catch (SkyflowException e) { - Assert.fail(INVALID_EXCEPTION_THROWN); - } - } - - // ── validateBulkTokenizeRequest ──────────────────────────────────────────── - - @Test - public void testValidateBulkTokenizeRequest_nullRequest() { + public void testValidateBulkInsertRequest_noRequestLevelTableNameAndOneRecordBlankThrowsEmptyTable() { + Map data = new HashMap<>(); + data.put("name", "john"); + ArrayList records = new ArrayList<>(); + records.add(BulkInsertRequestRecord.builder().tableName("cards").data(data).build()); + records.add(BulkInsertRequestRecord.builder().tableName(" ").data(data).build()); + BulkInsertRequest request = BulkInsertRequest.builder().records(records).build(); try { - Validations.validateBulkTokenizeRequest(null); + Validations.validateBulkInsertRequest(request); Assert.fail(EXCEPTION_NOT_THROWN); } catch (SkyflowException e) { - Assert.assertNotNull(e.getMessage()); + Assert.assertEquals(ErrorMessage.TableNotSpecifiedInRequestAndRecordObject.getMessage(), e.getMessage()); } } @Test - public void testValidateBulkTokenizeRequest_emptyData() { - BulkTokenizeRequest request = BulkTokenizeRequest.builder().data(new ArrayList<>()).build(); + public void testValidateBulkInsertRequest_blankRequestLevelTableNameDoesNotSatisfyRecords() { + // A blank request-level table name is treated as absent too, so records must supply one. + Map data = new HashMap<>(); + data.put("name", "john"); + ArrayList records = new ArrayList<>(); + records.add(BulkInsertRequestRecord.builder().data(data).build()); + BulkInsertRequest request = BulkInsertRequest.builder().tableName(" ").records(records).build(); try { - Validations.validateBulkTokenizeRequest(request); + Validations.validateBulkInsertRequest(request); Assert.fail(EXCEPTION_NOT_THROWN); } catch (SkyflowException e) { - Assert.assertNotNull(e.getMessage()); + Assert.assertEquals(ErrorMessage.TableNotSpecifiedInRequestAndRecordObject.getMessage(), e.getMessage()); } } @Test - public void testValidateBulkTokenizeRequest_nullRecordInList() { - ArrayList data = new ArrayList<>(); - data.add(null); - BulkTokenizeRequest request = BulkTokenizeRequest.builder().data(data).build(); + public void testValidateInsertRequest_upsertAtRequestLevelWithTableAtRecordLevelThrows() { + // upsert must sit at the same level as the table name. + Map data = new HashMap<>(); + data.put("name", "john"); + ArrayList records = new ArrayList<>(); + records.add(InsertRequestRecord.builder().tableName("table1").data(data).build()); + InsertRequest request = InsertRequest.builder() + .records(records) + .upsert(UpsertOptions.builder().uniqueColumns(Collections.singletonList("email")).build()) + .build(); try { - Validations.validateBulkTokenizeRequest(request); + Validations.validateInsertRequest(request); Assert.fail(EXCEPTION_NOT_THROWN); } catch (SkyflowException e) { - Assert.assertNotNull(e.getMessage()); + Assert.assertEquals(ErrorMessage.UpsertTableRequestAtRequestLevel.getMessage(), e.getMessage()); } } @Test - public void testValidateBulkTokenizeRequest_emptyValue() { - ArrayList data = new ArrayList<>(); - data.add(BulkTokenizeRecord.builder().value(" ").build()); - BulkTokenizeRequest request = BulkTokenizeRequest.builder().data(data).build(); + public void testValidateInsertRequest_upsertAtRecordLevelWithTableAtRequestLevelThrows() { + Map data = new HashMap<>(); + data.put("name", "john"); + ArrayList records = new ArrayList<>(); + records.add(InsertRequestRecord.builder() + .data(data) + .upsert(UpsertOptions.builder().uniqueColumns(Collections.singletonList("email")).build()) + .build()); + InsertRequest request = InsertRequest.builder().tableName("table1").records(records).build(); try { - Validations.validateBulkTokenizeRequest(request); + Validations.validateInsertRequest(request); Assert.fail(EXCEPTION_NOT_THROWN); } catch (SkyflowException e) { - Assert.assertNotNull(e.getMessage()); + Assert.assertEquals(ErrorMessage.UpsertTableRequestAtRecordLevel.getMessage(), e.getMessage()); } } @Test - public void testValidateBulkTokenizeRequest_emptyGroupNameInList() { - ArrayList data = new ArrayList<>(); - data.add(BulkTokenizeRecord.builder().value("value1").tokenGroupNames(Arrays.asList("group1", " ")).build()); - BulkTokenizeRequest request = BulkTokenizeRequest.builder().data(data).build(); + public void testValidateInsertRequest_upsertOnSomeRecordsOnlyIsValid() { + // Upsert is optional per record; it only has to sit at the same level as the table name. + Map data = new HashMap<>(); + data.put("name", "john"); + ArrayList records = new ArrayList<>(); + records.add(InsertRequestRecord.builder() + .tableName("table1") + .data(data) + .upsert(UpsertOptions.builder().uniqueColumns(Collections.singletonList("email")).build()) + .build()); + records.add(InsertRequestRecord.builder().tableName("table1").data(data).build()); + InsertRequest request = InsertRequest.builder().records(records).build(); try { - Validations.validateBulkTokenizeRequest(request); - Assert.fail(EXCEPTION_NOT_THROWN); + Validations.validateInsertRequest(request); } catch (SkyflowException e) { - Assert.assertNotNull(e.getMessage()); + Assert.fail(INVALID_EXCEPTION_THROWN); } } @Test - public void testValidateBulkTokenizeRequest_validRequestWithTokenGroupNames() { - ArrayList data = new ArrayList<>(); - data.add(BulkTokenizeRecord.builder().value("value1").tokenGroupNames(Collections.singletonList("group1")).build()); - BulkTokenizeRequest request = BulkTokenizeRequest.builder().data(data).build(); + public void testValidateInsertRequest_upsertOnEveryRecordWithTableOnEveryRecordIsValid() { + Map data = new HashMap<>(); + data.put("name", "john"); + ArrayList records = new ArrayList<>(); + for (int i = 0; i < 2; i++) { + records.add(InsertRequestRecord.builder() + .tableName("table1") + .data(data) + .upsert(UpsertOptions.builder().uniqueColumns(Collections.singletonList("email")).build()) + .build()); + } + InsertRequest request = InsertRequest.builder().records(records).build(); try { - Validations.validateBulkTokenizeRequest(request); + Validations.validateInsertRequest(request); } catch (SkyflowException e) { Assert.fail(INVALID_EXCEPTION_THROWN); } } @Test - public void testValidateBulkTokenizeRequest_validRequestWithoutTokenGroupNames() { - // Unlike the non-bulk tokenize validator, tokenGroupNames is optional for bulk requests. - ArrayList data = new ArrayList<>(); - data.add(BulkTokenizeRecord.builder().value("value1").build()); - BulkTokenizeRequest request = BulkTokenizeRequest.builder().data(data).build(); + public void testValidateInsertRequest_noUpsertAnywhereIsValid() { + Map data = new HashMap<>(); + data.put("name", "john"); + ArrayList records = new ArrayList<>(); + records.add(InsertRequestRecord.builder().data(data).build()); + InsertRequest request = InsertRequest.builder().tableName("table1").records(records).build(); try { - Validations.validateBulkTokenizeRequest(request); + Validations.validateInsertRequest(request); } catch (SkyflowException e) { Assert.fail(INVALID_EXCEPTION_THROWN); } } - // ── validateQueryRequest ─────────────────────────────────────────────────── + // ── validateBulkDetokenizeRequest ────────────────────────────────────────── @Test - public void testValidateQueryRequest_nullQuery() { - com.skyflow.vault.data.QueryRequest request = com.skyflow.vault.data.QueryRequest.builder().build(); + public void testValidateBulkDetokenizeRequest_nullRequest() { try { - Validations.validateQueryRequest(request); + Validations.validateBulkDetokenizeRequest(null); Assert.fail(EXCEPTION_NOT_THROWN); } catch (SkyflowException e) { Assert.assertNotNull(e.getMessage()); @@ -1319,10 +1264,10 @@ public void testValidateQueryRequest_nullQuery() { } @Test - public void testValidateQueryRequest_emptyQuery() { - com.skyflow.vault.data.QueryRequest request = com.skyflow.vault.data.QueryRequest.builder().query(" ").build(); + public void testValidateBulkDetokenizeRequest_nullTokens() { + BulkDetokenizeRequest request = BulkDetokenizeRequest.builder().tokens(null).build(); try { - Validations.validateQueryRequest(request); + Validations.validateBulkDetokenizeRequest(request); Assert.fail(EXCEPTION_NOT_THROWN); } catch (SkyflowException e) { Assert.assertNotNull(e.getMessage()); @@ -1330,26 +1275,10 @@ public void testValidateQueryRequest_emptyQuery() { } @Test - public void testValidateQueryRequest_validRequest() { - com.skyflow.vault.data.QueryRequest request = com.skyflow.vault.data.QueryRequest.builder() - .query("SELECT * FROM table1") - .build(); - try { - Validations.validateQueryRequest(request); - } catch (SkyflowException e) { - Assert.fail(INVALID_EXCEPTION_THROWN); - } - } - - // ── validateGetRequest ───────────────────────────────────────────────────── - - @Test - public void testValidateGetRequest_nullTable() { - com.skyflow.vault.data.GetRequest request = com.skyflow.vault.data.GetRequest.builder() - .ids(new ArrayList<>(Collections.singletonList("id1"))) - .build(); + public void testValidateBulkDetokenizeRequest_emptyTokens() { + BulkDetokenizeRequest request = BulkDetokenizeRequest.builder().tokens(new ArrayList<>()).build(); try { - Validations.validateGetRequest(request); + Validations.validateBulkDetokenizeRequest(request); Assert.fail(EXCEPTION_NOT_THROWN); } catch (SkyflowException e) { Assert.assertNotNull(e.getMessage()); @@ -1357,13 +1286,12 @@ public void testValidateGetRequest_nullTable() { } @Test - public void testValidateGetRequest_emptyTable() { - com.skyflow.vault.data.GetRequest request = com.skyflow.vault.data.GetRequest.builder() - .table(" ") - .ids(new ArrayList<>(Collections.singletonList("id1"))) - .build(); + public void testValidateBulkDetokenizeRequest_nullTokenInList() { + List tokens = new ArrayList<>(); + tokens.add(null); + BulkDetokenizeRequest request = BulkDetokenizeRequest.builder().tokens(tokens).build(); try { - Validations.validateGetRequest(request); + Validations.validateBulkDetokenizeRequest(request); Assert.fail(EXCEPTION_NOT_THROWN); } catch (SkyflowException e) { Assert.assertNotNull(e.getMessage()); @@ -1371,13 +1299,15 @@ public void testValidateGetRequest_emptyTable() { } @Test - public void testValidateGetRequest_emptyIds() { - com.skyflow.vault.data.GetRequest request = com.skyflow.vault.data.GetRequest.builder() - .table("table1") - .ids(new ArrayList<>()) + public void testValidateBulkDetokenizeRequest_nullRedactionGroupObject() { + List groupRedactions = new ArrayList<>(); + groupRedactions.add(null); + BulkDetokenizeRequest request = BulkDetokenizeRequest.builder() + .tokens(Collections.singletonList("token1")) + .tokenGroupRedactions(groupRedactions) .build(); try { - Validations.validateGetRequest(request); + Validations.validateBulkDetokenizeRequest(request); Assert.fail(EXCEPTION_NOT_THROWN); } catch (SkyflowException e) { Assert.assertNotNull(e.getMessage()); @@ -1385,15 +1315,15 @@ public void testValidateGetRequest_emptyIds() { } @Test - public void testValidateGetRequest_nullIdInIds() { - ArrayList ids = new ArrayList<>(); - ids.add(null); - com.skyflow.vault.data.GetRequest request = com.skyflow.vault.data.GetRequest.builder() - .table("table1") - .ids(ids) + public void testValidateBulkDetokenizeRequest_nullGroupNameInRedaction() { + List groupRedactions = Collections.singletonList( + TokenGroupRedactions.builder().redaction("MASKED").build()); + BulkDetokenizeRequest request = BulkDetokenizeRequest.builder() + .tokens(Collections.singletonList("token1")) + .tokenGroupRedactions(groupRedactions) .build(); try { - Validations.validateGetRequest(request); + Validations.validateBulkDetokenizeRequest(request); Assert.fail(EXCEPTION_NOT_THROWN); } catch (SkyflowException e) { Assert.assertNotNull(e.getMessage()); @@ -1401,12 +1331,15 @@ public void testValidateGetRequest_nullIdInIds() { } @Test - public void testValidateGetRequest_neitherIdsNorUniqueValues() { - com.skyflow.vault.data.GetRequest request = com.skyflow.vault.data.GetRequest.builder() - .table("table1") + public void testValidateBulkDetokenizeRequest_nullRedactionInGroup() { + List groupRedactions = Collections.singletonList( + TokenGroupRedactions.builder().tokenGroupName("group1").build()); + BulkDetokenizeRequest request = BulkDetokenizeRequest.builder() + .tokens(Collections.singletonList("token1")) + .tokenGroupRedactions(groupRedactions) .build(); try { - Validations.validateGetRequest(request); + Validations.validateBulkDetokenizeRequest(request); Assert.fail(EXCEPTION_NOT_THROWN); } catch (SkyflowException e) { Assert.assertNotNull(e.getMessage()); @@ -1414,46 +1347,38 @@ public void testValidateGetRequest_neitherIdsNorUniqueValues() { } @Test - public void testValidateGetRequest_bothIdsAndUniqueValues() { - Map uniqueValue = new HashMap<>(); - uniqueValue.put("email", "john@example.com"); - com.skyflow.vault.data.GetRequest request = com.skyflow.vault.data.GetRequest.builder() - .table("table1") - .ids(new ArrayList<>(Collections.singletonList("id1"))) - .uniqueValues(Collections.singletonList(uniqueValue)) + public void testValidateBulkDetokenizeRequest_validRequest() { + BulkDetokenizeRequest request = BulkDetokenizeRequest.builder() + .tokens(Collections.singletonList("token1")) .build(); try { - Validations.validateGetRequest(request); - Assert.fail(EXCEPTION_NOT_THROWN); + Validations.validateBulkDetokenizeRequest(request); } catch (SkyflowException e) { - Assert.assertNotNull(e.getMessage()); + Assert.fail(INVALID_EXCEPTION_THROWN); } } @Test - public void testValidateGetRequest_emptyUniqueValues() { - com.skyflow.vault.data.GetRequest request = com.skyflow.vault.data.GetRequest.builder() - .table("table1") - .uniqueValues(new ArrayList<>()) + public void testValidateBulkDetokenizeRequest_validRequestWithRedactions() { + List groupRedactions = Collections.singletonList( + TokenGroupRedactions.builder().tokenGroupName("group1").redaction("MASKED").build()); + BulkDetokenizeRequest request = BulkDetokenizeRequest.builder() + .tokens(Collections.singletonList("token1")) + .tokenGroupRedactions(groupRedactions) .build(); try { - Validations.validateGetRequest(request); - Assert.fail(EXCEPTION_NOT_THROWN); + Validations.validateBulkDetokenizeRequest(request); } catch (SkyflowException e) { - Assert.assertNotNull(e.getMessage()); + Assert.fail(INVALID_EXCEPTION_THROWN); } } + // ── validateBulkDeleteTokensRequest ──────────────────────────────────────── + @Test - public void testValidateGetRequest_nullUniqueValueInUniqueValues() { - List> uniqueValues = new ArrayList<>(); - uniqueValues.add(null); - com.skyflow.vault.data.GetRequest request = com.skyflow.vault.data.GetRequest.builder() - .table("table1") - .uniqueValues(uniqueValues) - .build(); + public void testValidateBulkDeleteTokensRequest_nullRequest() { try { - Validations.validateGetRequest(request); + Validations.validateBulkDeleteTokensRequest(null); Assert.fail(EXCEPTION_NOT_THROWN); } catch (SkyflowException e) { Assert.assertNotNull(e.getMessage()); @@ -1461,14 +1386,10 @@ public void testValidateGetRequest_nullUniqueValueInUniqueValues() { } @Test - public void testValidateGetRequest_emptyFields() { - com.skyflow.vault.data.GetRequest request = com.skyflow.vault.data.GetRequest.builder() - .table("table1") - .ids(new ArrayList<>(Collections.singletonList("id1"))) - .fields(new ArrayList<>()) - .build(); + public void testValidateBulkDeleteTokensRequest_emptyTokens() { + BulkDeleteTokensRequest request = BulkDeleteTokensRequest.builder().tokens(new ArrayList<>()).build(); try { - Validations.validateGetRequest(request); + Validations.validateBulkDeleteTokensRequest(request); Assert.fail(EXCEPTION_NOT_THROWN); } catch (SkyflowException e) { Assert.assertNotNull(e.getMessage()); @@ -1476,16 +1397,12 @@ public void testValidateGetRequest_emptyFields() { } @Test - public void testValidateGetRequest_nullColumnRedactionObject() { - List columnRedactions = new ArrayList<>(); - columnRedactions.add(null); - com.skyflow.vault.data.GetRequest request = com.skyflow.vault.data.GetRequest.builder() - .table("table1") - .ids(new ArrayList<>(Collections.singletonList("id1"))) - .columnRedactions(columnRedactions) + public void testValidateBulkDeleteTokensRequest_emptyTokenInList() { + BulkDeleteTokensRequest request = BulkDeleteTokensRequest.builder() + .tokens(Arrays.asList("token1", " ")) .build(); try { - Validations.validateGetRequest(request); + Validations.validateBulkDeleteTokensRequest(request); Assert.fail(EXCEPTION_NOT_THROWN); } catch (SkyflowException e) { Assert.assertNotNull(e.getMessage()); @@ -1493,35 +1410,23 @@ public void testValidateGetRequest_nullColumnRedactionObject() { } @Test - public void testValidateGetRequest_nullColumnNameInColumnRedaction() { - com.skyflow.vault.data.ColumnRedaction redaction = com.skyflow.vault.data.ColumnRedaction.builder() - .redaction("MASKED") - .build(); - com.skyflow.vault.data.GetRequest request = com.skyflow.vault.data.GetRequest.builder() - .table("table1") - .ids(new ArrayList<>(Collections.singletonList("id1"))) - .columnRedactions(Collections.singletonList(redaction)) + public void testValidateBulkDeleteTokensRequest_validRequest() { + BulkDeleteTokensRequest request = BulkDeleteTokensRequest.builder() + .tokens(Collections.singletonList("token1")) .build(); try { - Validations.validateGetRequest(request); - Assert.fail(EXCEPTION_NOT_THROWN); + Validations.validateBulkDeleteTokensRequest(request); } catch (SkyflowException e) { - Assert.assertNotNull(e.getMessage()); + Assert.fail(INVALID_EXCEPTION_THROWN); } } + // ── validateBulkTokenizeRequest ──────────────────────────────────────────── + @Test - public void testValidateGetRequest_nullRedactionInColumnRedaction() { - com.skyflow.vault.data.ColumnRedaction redaction = com.skyflow.vault.data.ColumnRedaction.builder() - .columnName("email") - .build(); - com.skyflow.vault.data.GetRequest request = com.skyflow.vault.data.GetRequest.builder() - .table("table1") - .ids(new ArrayList<>(Collections.singletonList("id1"))) - .columnRedactions(Collections.singletonList(redaction)) - .build(); + public void testValidateBulkTokenizeRequest_nullRequest() { try { - Validations.validateGetRequest(request); + Validations.validateBulkTokenizeRequest(null); Assert.fail(EXCEPTION_NOT_THROWN); } catch (SkyflowException e) { Assert.assertNotNull(e.getMessage()); @@ -1529,53 +1434,36 @@ public void testValidateGetRequest_nullRedactionInColumnRedaction() { } @Test - public void testValidateGetRequest_validRequestWithIds() { - com.skyflow.vault.data.GetRequest request = com.skyflow.vault.data.GetRequest.builder() - .table("table1") - .ids(new ArrayList<>(Collections.singletonList("id1"))) - .build(); + public void testValidateBulkTokenizeRequest_emptyData() { + BulkTokenizeRequest request = BulkTokenizeRequest.builder().data(new ArrayList<>()).build(); try { - Validations.validateGetRequest(request); + Validations.validateBulkTokenizeRequest(request); + Assert.fail(EXCEPTION_NOT_THROWN); } catch (SkyflowException e) { - Assert.fail(INVALID_EXCEPTION_THROWN); + Assert.assertNotNull(e.getMessage()); } } @Test - public void testValidateGetRequest_validRequestWithUniqueValuesAndColumnRedactions() { - Map uniqueValue = new HashMap<>(); - uniqueValue.put("email", "john@example.com"); - com.skyflow.vault.data.ColumnRedaction redaction = com.skyflow.vault.data.ColumnRedaction.builder() - .columnName("email") - .redaction("MASKED") - .build(); - com.skyflow.vault.data.GetRequest request = com.skyflow.vault.data.GetRequest.builder() - .table("table1") - .uniqueValues(Collections.singletonList(uniqueValue)) - .columnRedactions(Collections.singletonList(redaction)) - .build(); + public void testValidateBulkTokenizeRequest_nullRecordInList() { + ArrayList data = new ArrayList<>(); + data.add(null); + BulkTokenizeRequest request = BulkTokenizeRequest.builder().data(data).build(); try { - Validations.validateGetRequest(request); + Validations.validateBulkTokenizeRequest(request); + Assert.fail(EXCEPTION_NOT_THROWN); } catch (SkyflowException e) { - Assert.fail(INVALID_EXCEPTION_THROWN); + Assert.assertNotNull(e.getMessage()); } } - // ── validateGetRequest (multi-table records) ────────────────────────────── - @Test - public void testValidateGetRequest_bothSingleTableFieldsAndRecordsPassed() { - com.skyflow.vault.data.GetRecordRequest record = com.skyflow.vault.data.GetRecordRequest.builder() - .table("table2") - .ids(new ArrayList<>(Collections.singletonList("id2"))) - .build(); - com.skyflow.vault.data.GetRequest request = com.skyflow.vault.data.GetRequest.builder() - .table("table1") - .ids(new ArrayList<>(Collections.singletonList("id1"))) - .records(Collections.singletonList(record)) - .build(); + public void testValidateBulkTokenizeRequest_emptyValue() { + ArrayList data = new ArrayList<>(); + data.add(BulkTokenizeRecord.builder().value(" ").build()); + BulkTokenizeRequest request = BulkTokenizeRequest.builder().data(data).build(); try { - Validations.validateGetRequest(request); + Validations.validateBulkTokenizeRequest(request); Assert.fail(EXCEPTION_NOT_THROWN); } catch (SkyflowException e) { Assert.assertNotNull(e.getMessage()); @@ -1583,14 +1471,12 @@ public void testValidateGetRequest_bothSingleTableFieldsAndRecordsPassed() { } @Test - public void testValidateGetRequest_nullRecordInRecords() { - List records = new ArrayList<>(); - records.add(null); - com.skyflow.vault.data.GetRequest request = com.skyflow.vault.data.GetRequest.builder() - .records(records) - .build(); + public void testValidateBulkTokenizeRequest_emptyGroupNameInList() { + ArrayList data = new ArrayList<>(); + data.add(BulkTokenizeRecord.builder().value("value1").tokenGroupNames(Arrays.asList("group1", " ")).build()); + BulkTokenizeRequest request = BulkTokenizeRequest.builder().data(data).build(); try { - Validations.validateGetRequest(request); + Validations.validateBulkTokenizeRequest(request); Assert.fail(EXCEPTION_NOT_THROWN); } catch (SkyflowException e) { Assert.assertNotNull(e.getMessage()); @@ -1598,45 +1484,31 @@ public void testValidateGetRequest_nullRecordInRecords() { } @Test - public void testValidateGetRequest_invalidRecordWithinRecords() { - // second record is missing both ids and uniqueValues - com.skyflow.vault.data.GetRecordRequest validRecord = com.skyflow.vault.data.GetRecordRequest.builder() - .table("table1") - .ids(new ArrayList<>(Collections.singletonList("id1"))) - .build(); - com.skyflow.vault.data.GetRecordRequest invalidRecord = com.skyflow.vault.data.GetRecordRequest.builder() - .table("table2") - .build(); - com.skyflow.vault.data.GetRequest request = com.skyflow.vault.data.GetRequest.builder() - .records(java.util.Arrays.asList(validRecord, invalidRecord)) - .build(); + public void testValidateBulkTokenizeRequest_validRequestWithTokenGroupNames() { + ArrayList data = new ArrayList<>(); + data.add(BulkTokenizeRecord.builder().value("value1").tokenGroupNames(Collections.singletonList("group1")).build()); + BulkTokenizeRequest request = BulkTokenizeRequest.builder().data(data).build(); try { - Validations.validateGetRequest(request); - Assert.fail(EXCEPTION_NOT_THROWN); + Validations.validateBulkTokenizeRequest(request); } catch (SkyflowException e) { - Assert.assertNotNull(e.getMessage()); + Assert.fail(INVALID_EXCEPTION_THROWN); } } @Test - public void testValidateGetRequest_validMultiTableRecords() { - com.skyflow.vault.data.GetRecordRequest record1 = com.skyflow.vault.data.GetRecordRequest.builder() - .table("table1") - .ids(new ArrayList<>(Collections.singletonList("id1"))) - .build(); - Map uniqueValue = new HashMap<>(); - uniqueValue.put("email", "john@example.com"); - com.skyflow.vault.data.GetRecordRequest record2 = com.skyflow.vault.data.GetRecordRequest.builder() - .table("table2") - .uniqueValues(Collections.singletonList(uniqueValue)) - .build(); - com.skyflow.vault.data.GetRequest request = com.skyflow.vault.data.GetRequest.builder() - .records(java.util.Arrays.asList(record1, record2)) - .build(); + public void testValidateBulkTokenizeRequest_validRequestWithoutTokenGroupNames() { + // Unlike the non-bulk tokenize validator, tokenGroupNames is optional for bulk requests. + ArrayList data = new ArrayList<>(); + data.add(BulkTokenizeRecord.builder().value("value1").build()); + BulkTokenizeRequest request = BulkTokenizeRequest.builder().data(data).build(); try { - Validations.validateGetRequest(request); + Validations.validateBulkTokenizeRequest(request); } catch (SkyflowException e) { Assert.fail(INVALID_EXCEPTION_THROWN); } } + + // Tests for validateQueryRequest / validateGetRequest were removed: + // those unary validators no longer exist (bulk-only module). + } diff --git a/flowvault/src/test/java/com/skyflow/vault/controller/VaultControllerTests.java b/flowvault/src/test/java/com/skyflow/vault/controller/VaultControllerTests.java index 2e3773cc..9d96df49 100644 --- a/flowvault/src/test/java/com/skyflow/vault/controller/VaultControllerTests.java +++ b/flowvault/src/test/java/com/skyflow/vault/controller/VaultControllerTests.java @@ -8,23 +8,17 @@ import com.skyflow.errors.SkyflowException; import com.skyflow.generated.rest.ApiClient; import com.skyflow.generated.rest.core.ApiClientApiException; -import com.skyflow.generated.rest.core.ApiClientException; import com.skyflow.generated.rest.core.ApiClientHttpResponse; import com.skyflow.generated.rest.core.RequestOptions; import com.skyflow.generated.rest.resources.flowservice.FlowserviceClient; import com.skyflow.generated.rest.resources.flowservice.RawFlowserviceClient; -import com.skyflow.generated.rest.resources.records.RawRecordsClient; -import com.skyflow.generated.rest.resources.records.RecordsClient; import com.skyflow.generated.rest.types.FlowTokenizeResponseObjectToken; import com.skyflow.generated.rest.types.V1DeleteTokenResponseObject; -import com.skyflow.generated.rest.types.V1ExecuteQueryRecordResponse; -import com.skyflow.generated.rest.types.V1ExecuteQueryResponse; import com.skyflow.generated.rest.types.V1FlowDeleteTokenResponse; import com.skyflow.generated.rest.types.V1FlowDetokenizeResponse; import com.skyflow.generated.rest.types.V1FlowDetokenizeResponseObject; import com.skyflow.generated.rest.types.V1FlowTokenizeResponse; import com.skyflow.generated.rest.types.V1FlowTokenizeResponseObject; -import com.skyflow.generated.rest.types.V1GetResponse; import com.skyflow.generated.rest.types.V1InsertResponse; import com.skyflow.generated.rest.types.V1RecordResponseObject; import com.skyflow.utils.Constants; @@ -32,35 +26,20 @@ import com.skyflow.vault.data.BulkDeleteTokensResponse; import com.skyflow.vault.data.BulkDetokenizeRequest; import com.skyflow.vault.data.BulkDetokenizeResponse; -import com.skyflow.vault.data.BulkInsertRecord; +import com.skyflow.vault.data.BulkInsertRequestRecord; import com.skyflow.vault.data.BulkInsertRequest; import com.skyflow.vault.data.BulkInsertResponse; +import com.skyflow.vault.data.BulkInsertResponseRecord; import com.skyflow.vault.data.BulkTokenizeRecord; import com.skyflow.vault.data.BulkTokenizeRequest; import com.skyflow.vault.data.BulkTokenizeResponse; -import com.skyflow.vault.data.DeleteTokensRequest; -import com.skyflow.vault.data.DeleteTokensResponse; -import com.skyflow.vault.data.DetokenizeData; +import com.skyflow.vault.data.DeleteTokensOptions; import com.skyflow.vault.data.DetokenizeOptions; -import com.skyflow.vault.data.DetokenizeRequest; -import com.skyflow.vault.data.DetokenizeResponse; -import com.skyflow.vault.data.GetOptions; -import com.skyflow.vault.data.GetRequest; -import com.skyflow.vault.data.GetResponse; import com.skyflow.vault.data.InsertOptions; -import com.skyflow.vault.data.InsertRecord; -import com.skyflow.vault.data.InsertRequest; -import com.skyflow.vault.data.InsertResponse; -import com.skyflow.vault.data.QueryOptions; -import com.skyflow.vault.data.QueryRequest; -import com.skyflow.vault.data.QueryResponse; +import com.skyflow.vault.data.InsertRequestRecord; import com.skyflow.vault.data.RequestInterceptor; -import com.skyflow.vault.data.Success; -import com.skyflow.vault.data.Token; +import com.skyflow.vault.data.TokenGroupRedactions; import com.skyflow.vault.data.TokenizeOptions; -import com.skyflow.vault.data.TokenizeRecord; -import com.skyflow.vault.data.TokenizeRequest; -import com.skyflow.vault.data.TokenizeResponse; import okhttp3.Protocol; import okhttp3.Request; import okhttp3.Response; @@ -118,427 +97,8 @@ private static RawFlowserviceClient mockRawFlowservice(ApiClient mockApi) { return mockRaw; } - private static RawRecordsClient mockRawRecords(ApiClient mockApi) { - RecordsClient mockRecords = Mockito.mock(RecordsClient.class); - RawRecordsClient mockRaw = Mockito.mock(RawRecordsClient.class); - when(mockApi.records()).thenReturn(mockRecords); - when(mockRecords.withRawResponse()).thenReturn(mockRaw); - return mockRaw; - } - - // ── insert ──────────────────────────────────────────────────────────────── - - @Test - public void testInsert_success() throws Exception { - ApiClient mockApi = Mockito.mock(ApiClient.class); - RawFlowserviceClient mockRaw = mockRawFlowservice(mockApi); - - Map tokens = new HashMap<>(); - tokens.put("name", "tok-abc"); - V1RecordResponseObject record = V1RecordResponseObject.builder() - .skyflowId("sky-id-1") - .tokens(tokens) - .build(); - V1InsertResponse body = V1InsertResponse.builder().records(Collections.singletonList(record)).build(); - ApiClientHttpResponse httpResp = new ApiClientHttpResponse<>(body, buildOkHttpResponse()); - when(mockRaw.insert(any(), any())).thenReturn(httpResp); - - VaultController controller = createControllerWithMock(mockApi); - - Map data = new HashMap<>(); - data.put("name", "john"); - ArrayList records = new ArrayList<>(); - records.add(InsertRecord.builder().table("table1").data(data).build()); - InsertRequest request = InsertRequest.builder().records(records).build(); - - InsertResponse response = controller.insert(request); - Assert.assertNotNull(INVALID_EXCEPTION_THROWN, response); - Assert.assertEquals(1, response.getInsertedFields().size()); - Assert.assertEquals("sky-id-1", response.getInsertedFields().get(0).get("skyflowId")); - Assert.assertTrue(response.getErrors().isEmpty()); - } - - @Test - public void testInsert_apiErrorThrowsSkyflowException() throws Exception { - ApiClient mockApi = Mockito.mock(ApiClient.class); - RawFlowserviceClient mockRaw = mockRawFlowservice(mockApi); - when(mockRaw.insert(any(), any())) - .thenThrow(new ApiClientApiException("insert failed", 401, "unauthorized")); - - VaultController controller = createControllerWithMock(mockApi); - - Map data = new HashMap<>(); - data.put("name", "john"); - ArrayList records = new ArrayList<>(); - records.add(InsertRecord.builder().table("table1").data(data).build()); - InsertRequest request = InsertRequest.builder().records(records).build(); - - try { - controller.insert(request); - Assert.fail(EXCEPTION_NOT_THROWN); - } catch (SkyflowException e) { - Assert.assertEquals(401, e.getHttpCode()); - } - } - - @Test - public void testInsert_networkErrorThrowsSkyflowException() throws Exception { - ApiClient mockApi = Mockito.mock(ApiClient.class); - RawFlowserviceClient mockRaw = mockRawFlowservice(mockApi); - when(mockRaw.insert(any(), any())) - .thenThrow(new ApiClientException("Network error executing HTTP request")); - - VaultController controller = createControllerWithMock(mockApi); - - Map data = new HashMap<>(); - data.put("name", "john"); - ArrayList records = new ArrayList<>(); - records.add(InsertRecord.builder().table("table1").data(data).build()); - InsertRequest request = InsertRequest.builder().records(records).build(); - - try { - controller.insert(request); - Assert.fail(EXCEPTION_NOT_THROWN); - } catch (SkyflowException e) { - Assert.assertNotNull(e.getMessage()); - } - } - - @Test - public void testInsert_invalidRequestThrowsSkyflowException() throws Exception { - ApiClient mockApi = Mockito.mock(ApiClient.class); - VaultController controller = createControllerWithMock(mockApi); - InsertRequest request = InsertRequest.builder().records(new ArrayList<>()).build(); - try { - controller.insert(request); - Assert.fail(EXCEPTION_NOT_THROWN); - } catch (SkyflowException e) { - Assert.assertNotNull(e.getMessage()); - } - } - - // ── detokenize ──────────────────────────────────────────────────────────── - - @Test - public void testDetokenize_success() throws Exception { - ApiClient mockApi = Mockito.mock(ApiClient.class); - RawFlowserviceClient mockRaw = mockRawFlowservice(mockApi); - - V1FlowDetokenizeResponseObject record = V1FlowDetokenizeResponseObject.builder() - .token("token1") - .value("secret-value") - .build(); - V1FlowDetokenizeResponse body = V1FlowDetokenizeResponse.builder() - .response(Collections.singletonList(record)) - .build(); - ApiClientHttpResponse httpResp = - new ApiClientHttpResponse<>(body, buildOkHttpResponse()); - when(mockRaw.detokenize(any(), any())).thenReturn(httpResp); - - VaultController controller = createControllerWithMock(mockApi); - - ArrayList data = new ArrayList<>(); - data.add(new DetokenizeData("token1")); - DetokenizeRequest request = DetokenizeRequest.builder().detokenizeData(data).build(); - - DetokenizeResponse response = controller.detokenize(request); - Assert.assertNotNull(INVALID_EXCEPTION_THROWN, response); - Assert.assertEquals(1, response.getDetokenizedFields().size()); - Assert.assertEquals("secret-value", response.getDetokenizedFields().get(0).getValue()); - Assert.assertTrue(response.getErrors().isEmpty()); - } - - @Test - public void testDetokenize_apiErrorThrowsSkyflowException() throws Exception { - ApiClient mockApi = Mockito.mock(ApiClient.class); - RawFlowserviceClient mockRaw = mockRawFlowservice(mockApi); - when(mockRaw.detokenize(any(), any())) - .thenThrow(new ApiClientApiException("detokenize failed", 401, "unauthorized")); - - VaultController controller = createControllerWithMock(mockApi); - - ArrayList data = new ArrayList<>(); - data.add(new DetokenizeData("token1")); - DetokenizeRequest request = DetokenizeRequest.builder().detokenizeData(data).build(); - - try { - controller.detokenize(request); - Assert.fail(EXCEPTION_NOT_THROWN); - } catch (SkyflowException e) { - Assert.assertEquals(401, e.getHttpCode()); - } - } - - @Test - public void testDetokenize_networkErrorThrowsSkyflowException() throws Exception { - ApiClient mockApi = Mockito.mock(ApiClient.class); - RawFlowserviceClient mockRaw = mockRawFlowservice(mockApi); - when(mockRaw.detokenize(any(), any())) - .thenThrow(new ApiClientException("Network error executing HTTP request")); - - VaultController controller = createControllerWithMock(mockApi); - - ArrayList data = new ArrayList<>(); - data.add(new DetokenizeData("token1")); - DetokenizeRequest request = DetokenizeRequest.builder().detokenizeData(data).build(); - - try { - controller.detokenize(request); - Assert.fail(EXCEPTION_NOT_THROWN); - } catch (SkyflowException e) { - Assert.assertNotNull(e.getMessage()); - } - } - - @Test - public void testDetokenize_nullRequestThrowsSkyflowException() throws Exception { - ApiClient mockApi = Mockito.mock(ApiClient.class); - VaultController controller = createControllerWithMock(mockApi); - try { - controller.detokenize(null); - Assert.fail(EXCEPTION_NOT_THROWN); - } catch (SkyflowException e) { - Assert.assertNotNull(e.getMessage()); - } - } - - // ── tokenize ────────────────────────────────────────────────────────────── - - @Test - public void testTokenize_success() throws Exception { - ApiClient mockApi = Mockito.mock(ApiClient.class); - RawFlowserviceClient mockRaw = mockRawFlowservice(mockApi); - - FlowTokenizeResponseObjectToken token = FlowTokenizeResponseObjectToken.builder() - .tokenGroupName("group1") - .token("tok-abc") - .build(); - V1FlowTokenizeResponseObject responseObject = V1FlowTokenizeResponseObject.builder() - .value("value1") - .tokens(Collections.singletonList(token)) - .build(); - V1FlowTokenizeResponse body = V1FlowTokenizeResponse.builder() - .response(Collections.singletonList(responseObject)) - .build(); - ApiClientHttpResponse httpResp = - new ApiClientHttpResponse<>(body, buildOkHttpResponse()); - when(mockRaw.tokenize(any(), any())).thenReturn(httpResp); - - VaultController controller = createControllerWithMock(mockApi); - - ArrayList records = new ArrayList<>(); - records.add(TokenizeRecord.builder() - .value("value1") - .tokenGroupNames(Collections.singletonList("group1")) - .build()); - TokenizeRequest request = TokenizeRequest.builder().data(records).build(); - - TokenizeResponse response = controller.tokenize(request); - Assert.assertNotNull(INVALID_EXCEPTION_THROWN, response); - Assert.assertEquals(1, response.getTokenizedData().size()); - Assert.assertEquals("tok-abc", response.getTokenizedData().get(0).getTokens().get("group1")); - Assert.assertTrue(response.getErrors().isEmpty()); - } - - @Test - public void testTokenize_apiErrorThrowsSkyflowException() throws Exception { - ApiClient mockApi = Mockito.mock(ApiClient.class); - RawFlowserviceClient mockRaw = mockRawFlowservice(mockApi); - when(mockRaw.tokenize(any(), any())) - .thenThrow(new ApiClientApiException("tokenize failed", 401, "unauthorized")); - - VaultController controller = createControllerWithMock(mockApi); - - ArrayList records = new ArrayList<>(); - records.add(TokenizeRecord.builder() - .value("value1") - .tokenGroupNames(Collections.singletonList("group1")) - .build()); - TokenizeRequest request = TokenizeRequest.builder().data(records).build(); - - try { - controller.tokenize(request); - Assert.fail(EXCEPTION_NOT_THROWN); - } catch (SkyflowException e) { - Assert.assertEquals(401, e.getHttpCode()); - } - } - - @Test - public void testTokenize_networkErrorThrowsSkyflowException() throws Exception { - ApiClient mockApi = Mockito.mock(ApiClient.class); - RawFlowserviceClient mockRaw = mockRawFlowservice(mockApi); - when(mockRaw.tokenize(any(), any())) - .thenThrow(new ApiClientException("Network error executing HTTP request")); - - VaultController controller = createControllerWithMock(mockApi); - - ArrayList records = new ArrayList<>(); - records.add(TokenizeRecord.builder() - .value("value1") - .tokenGroupNames(Collections.singletonList("group1")) - .build()); - TokenizeRequest request = TokenizeRequest.builder().data(records).build(); - - try { - controller.tokenize(request); - Assert.fail(EXCEPTION_NOT_THROWN); - } catch (SkyflowException e) { - Assert.assertNotNull(e.getMessage()); - } - } - - @Test - public void testTokenize_nullRequestThrowsSkyflowException() throws Exception { - ApiClient mockApi = Mockito.mock(ApiClient.class); - VaultController controller = createControllerWithMock(mockApi); - try { - controller.tokenize(null); - Assert.fail(EXCEPTION_NOT_THROWN); - } catch (SkyflowException e) { - Assert.assertNotNull(e.getMessage()); - } - } - - // ── deleteTokens ────────────────────────────────────────────────────────── - - @Test - public void testDeleteTokens_success() throws Exception { - ApiClient mockApi = Mockito.mock(ApiClient.class); - RawFlowserviceClient mockRaw = mockRawFlowservice(mockApi); - - V1DeleteTokenResponseObject record = V1DeleteTokenResponseObject.builder() - .value("token1") - .build(); - V1FlowDeleteTokenResponse body = V1FlowDeleteTokenResponse.builder() - .tokens(Collections.singletonList(record)) - .build(); - ApiClientHttpResponse httpResp = - new ApiClientHttpResponse<>(body, buildOkHttpResponse()); - when(mockRaw.deletetoken(any(), any())).thenReturn(httpResp); - - VaultController controller = createControllerWithMock(mockApi); - - DeleteTokensRequest request = DeleteTokensRequest.builder() - .tokens(Collections.singletonList("token1")) - .build(); - - DeleteTokensResponse response = controller.deleteTokens(request); - Assert.assertNotNull(INVALID_EXCEPTION_THROWN, response); - Assert.assertEquals(Collections.singletonList("token1"), response.getTokens()); - Assert.assertTrue(response.getErrors().isEmpty()); - } - - @Test - public void testDeleteTokens_apiErrorThrowsSkyflowException() throws Exception { - ApiClient mockApi = Mockito.mock(ApiClient.class); - RawFlowserviceClient mockRaw = mockRawFlowservice(mockApi); - when(mockRaw.deletetoken(any(), any())) - .thenThrow(new ApiClientApiException("delete failed", 404, "not found")); - - VaultController controller = createControllerWithMock(mockApi); - - DeleteTokensRequest request = DeleteTokensRequest.builder() - .tokens(Collections.singletonList("token1")) - .build(); - - try { - controller.deleteTokens(request); - Assert.fail(EXCEPTION_NOT_THROWN); - } catch (SkyflowException e) { - Assert.assertEquals(404, e.getHttpCode()); - } - } - - @Test - public void testDeleteTokens_networkErrorThrowsSkyflowException() throws Exception { - ApiClient mockApi = Mockito.mock(ApiClient.class); - RawFlowserviceClient mockRaw = mockRawFlowservice(mockApi); - when(mockRaw.deletetoken(any(), any())) - .thenThrow(new ApiClientException("Network error executing HTTP request")); - - VaultController controller = createControllerWithMock(mockApi); - - DeleteTokensRequest request = DeleteTokensRequest.builder() - .tokens(Collections.singletonList("token1")) - .build(); - - try { - controller.deleteTokens(request); - Assert.fail(EXCEPTION_NOT_THROWN); - } catch (SkyflowException e) { - Assert.assertNotNull(e.getMessage()); - } - } - - @Test - public void testDeleteTokens_nullRequestThrowsSkyflowException() throws Exception { - ApiClient mockApi = Mockito.mock(ApiClient.class); - VaultController controller = createControllerWithMock(mockApi); - try { - controller.deleteTokens(null); - Assert.fail(EXCEPTION_NOT_THROWN); - } catch (SkyflowException e) { - Assert.assertNotNull(e.getMessage()); - } - } - - // ── insert/detokenize interceptor header wiring ────────────────────────── - - @Test - public void testInsert_interceptorAddsCustomHeader() throws Exception { - ApiClient mockApi = Mockito.mock(ApiClient.class); - RawFlowserviceClient mockRaw = mockRawFlowservice(mockApi); - - Map tokens = new HashMap<>(); - tokens.put("name", "tok-abc"); - V1RecordResponseObject record = V1RecordResponseObject.builder().skyflowId("sky-id-1").tokens(tokens).build(); - V1InsertResponse body = V1InsertResponse.builder().records(Collections.singletonList(record)).build(); - ApiClientHttpResponse httpResp = new ApiClientHttpResponse<>(body, buildOkHttpResponse()); - when(mockRaw.insert(any(), any())).thenReturn(httpResp); - - VaultController controller = createControllerWithMock(mockApi); - - Map data = new HashMap<>(); - data.put("name", "john"); - ArrayList records = new ArrayList<>(); - records.add(InsertRecord.builder().table("table1").data(data).build()); - InsertRequest request = InsertRequest.builder().records(records).build(); - - RequestInterceptor interceptor = ctx -> ctx.addHeader(CustomHeaderKey.SkyflowAccountName, "acct-name"); - InsertOptions options = InsertOptions.builder().interceptor(interceptor).build(); - - controller.insert(request, options); - - ArgumentCaptor captor = ArgumentCaptor.forClass(RequestOptions.class); - Mockito.verify(mockRaw).insert(any(), captor.capture()); - Assert.assertEquals("acct-name", captor.getValue().getHeaders().get(CustomHeaderKey.SkyflowAccountName.toString())); - } - - @Test - public void testDetokenize_noOptionsDoesNotAddCustomHeader() throws Exception { - ApiClient mockApi = Mockito.mock(ApiClient.class); - RawFlowserviceClient mockRaw = mockRawFlowservice(mockApi); - - V1FlowDetokenizeResponseObject record = V1FlowDetokenizeResponseObject.builder() - .token("token1").value("secret-value").build(); - V1FlowDetokenizeResponse body = V1FlowDetokenizeResponse.builder() - .response(Collections.singletonList(record)).build(); - ApiClientHttpResponse httpResp = new ApiClientHttpResponse<>(body, buildOkHttpResponse()); - when(mockRaw.detokenize(any(), any())).thenReturn(httpResp); - - VaultController controller = createControllerWithMock(mockApi); - - ArrayList data = new ArrayList<>(); - data.add(new DetokenizeData("token1")); - DetokenizeRequest request = DetokenizeRequest.builder().detokenizeData(data).build(); - - controller.detokenize(request, null); - - ArgumentCaptor captor = ArgumentCaptor.forClass(RequestOptions.class); - Mockito.verify(mockRaw).detokenize(any(), captor.capture()); - Assert.assertNull(captor.getValue().getHeaders().get(CustomHeaderKey.SkyflowAccountID.toString())); - } + // Tests for the unary insert / detokenize / tokenize / deleteTokens controller methods + // (and their interceptor-header wiring) were removed: VaultController is bulk-only now. // ── bulkInsert ──────────────────────────────────────────────────────────── @@ -558,17 +118,18 @@ public void testBulkInsert_success() throws Exception { Map data = new HashMap<>(); data.put("name", "john"); - ArrayList records = new ArrayList<>(); - records.add(BulkInsertRecord.builder().table("table1").data(data).build()); + ArrayList records = new ArrayList<>(); + records.add(BulkInsertRequestRecord.builder().tableName("table1").data(data).build()); BulkInsertRequest request = BulkInsertRequest.builder().records(records).build(); BulkInsertResponse response = controller.bulkInsert(request); Assert.assertNotNull(INVALID_EXCEPTION_THROWN, response); - Assert.assertEquals(1, response.getSuccess().size()); - Assert.assertEquals("sky-id-1", response.getSuccess().get(0).getSkyflowId()); - Assert.assertTrue(response.getErrors().isEmpty()); + Assert.assertEquals(1, response.getRecords().size()); + Assert.assertEquals("sky-id-1", response.getRecords().get(0).getSkyflowId()); + Assert.assertNull(response.getRecords().get(0).getError()); Assert.assertEquals(1, response.getSummary().getTotalRecords()); Assert.assertEquals(1, response.getSummary().getTotalInserted()); + Assert.assertEquals(0, response.getSummary().getTotalFailed()); } @Test @@ -600,14 +161,14 @@ public void testBulkInsertAsync_success() throws Exception { Map data = new HashMap<>(); data.put("name", "john"); - ArrayList records = new ArrayList<>(); - records.add(BulkInsertRecord.builder().table("table1").data(data).build()); + ArrayList records = new ArrayList<>(); + records.add(BulkInsertRequestRecord.builder().tableName("table1").data(data).build()); BulkInsertRequest request = BulkInsertRequest.builder().records(records).build(); BulkInsertResponse response = controller.bulkInsertAsync(request).get(5, TimeUnit.SECONDS); Assert.assertNotNull(INVALID_EXCEPTION_THROWN, response); - Assert.assertEquals(1, response.getSuccess().size()); - Assert.assertEquals("sky-id-1", response.getSuccess().get(0).getSkyflowId()); + Assert.assertEquals(1, response.getRecords().size()); + Assert.assertEquals("sky-id-1", response.getRecords().get(0).getSkyflowId()); } @Test @@ -626,18 +187,18 @@ public void testBulkInsert_interceptorAddsCustomHeader() throws Exception { Map data = new HashMap<>(); data.put("name", "john"); - ArrayList records = new ArrayList<>(); - records.add(BulkInsertRecord.builder().table("table1").data(data).build()); + ArrayList records = new ArrayList<>(); + records.add(BulkInsertRequestRecord.builder().tableName("table1").data(data).build()); BulkInsertRequest request = BulkInsertRequest.builder().records(records).build(); - RequestInterceptor interceptor = ctx -> ctx.addHeader(CustomHeaderKey.SkyflowAccountID, "acct-123"); + RequestInterceptor interceptor = ctx -> ctx.addHeader(CustomHeaderKey.SkyflowAccountId, "acct-123"); InsertOptions options = InsertOptions.builder().interceptor(interceptor).build(); controller.bulkInsert(request, options); ArgumentCaptor captor = ArgumentCaptor.forClass(RequestOptions.class); Mockito.verify(mockRaw).insert(any(), captor.capture()); - Assert.assertEquals("acct-123", captor.getValue().getHeaders().get(CustomHeaderKey.SkyflowAccountID.toString())); + Assert.assertEquals("acct-123", captor.getValue().getHeaders().get(CustomHeaderKey.SkyflowAccountId.toString())); } // ── bulkDetokenize ──────────────────────────────────────────────────────── @@ -662,9 +223,11 @@ public void testBulkDetokenize_success() throws Exception { BulkDetokenizeResponse response = controller.bulkDetokenize(request); Assert.assertNotNull(INVALID_EXCEPTION_THROWN, response); - Assert.assertEquals(1, response.getSuccess().size()); - Assert.assertEquals("secret-value", response.getSuccess().get(0).getValue()); - Assert.assertTrue(response.getErrors().isEmpty()); + Assert.assertEquals(1, response.getRecords().size()); + Assert.assertEquals("token1", response.getRecords().get(0).getToken()); + Assert.assertNull(response.getRecords().get(0).getError()); + Assert.assertEquals(1, response.getSummary().getTotalDetokenized()); + Assert.assertEquals(0, response.getSummary().getTotalFailed()); } @Test @@ -704,7 +267,8 @@ public void testBulkDetokenizeAsync_success() throws Exception { BulkDetokenizeResponse response = controller.bulkDetokenizeAsync(request).get(5, TimeUnit.SECONDS); Assert.assertNotNull(INVALID_EXCEPTION_THROWN, response); - Assert.assertEquals(1, response.getSuccess().size()); + Assert.assertEquals(1, response.getRecords().size()); + Assert.assertNull(response.getRecords().get(0).getError()); } @Test @@ -868,15 +432,16 @@ public void testBulkInsertAsync_apiErrorCapturedInErrors() throws Exception { Map data = new HashMap<>(); data.put("name", "john"); - ArrayList records = new ArrayList<>(); - records.add(BulkInsertRecord.builder().table("table1").data(data).build()); + ArrayList records = new ArrayList<>(); + records.add(BulkInsertRequestRecord.builder().tableName("table1").data(data).build()); BulkInsertRequest request = BulkInsertRequest.builder().records(records).build(); BulkInsertResponse response = controller.bulkInsertAsync(request).get(5, TimeUnit.SECONDS); Assert.assertNotNull(INVALID_EXCEPTION_THROWN, response); - Assert.assertTrue(response.getSuccess().isEmpty()); - Assert.assertEquals(1, response.getErrors().size()); - Assert.assertEquals(401, response.getErrors().get(0).getCode()); + Assert.assertEquals(1, response.getRecords().size()); + Assert.assertEquals(401, response.getRecords().get(0).getHttpCode()); + Assert.assertNotNull(response.getRecords().get(0).getError()); + Assert.assertNull(response.getRecords().get(0).getSkyflowId()); } @Test @@ -974,9 +539,11 @@ public void testBulkDetokenizeAsync_apiErrorCapturedInErrors() throws Exception BulkDetokenizeResponse response = controller.bulkDetokenizeAsync(request).get(5, TimeUnit.SECONDS); Assert.assertNotNull(INVALID_EXCEPTION_THROWN, response); - Assert.assertTrue(response.getSuccess().isEmpty()); - Assert.assertEquals(1, response.getErrors().size()); - Assert.assertEquals(401, response.getErrors().get(0).getCode()); + Assert.assertEquals(1, response.getRecords().size()); + Assert.assertEquals(401, response.getRecords().get(0).getHttpCode()); + Assert.assertNotNull(response.getRecords().get(0).getError()); + Assert.assertEquals(1, response.getSummary().getTotalFailed()); + Assert.assertEquals(0, response.getSummary().getTotalDetokenized()); } // ── multi-batch aggregation / partial-batch failure ────────────────────── @@ -1003,27 +570,28 @@ public void testBulkInsert_multiBatchAggregatesAcrossBatches() throws Exception VaultController controller = createControllerWithMock(mockApi); // Constants.INSERT_BATCH_SIZE defaults to 50, so 75 records forces exactly two batches - // (50 + 25) — processBulkInsertSync/insertBatchFutures must merge success/error lists - // collected from more than one CompletableFuture into a single BulkInsertResponse. - ArrayList records = new ArrayList<>(); + // (50 + 25) — processBulkInsertSync/insertBatchFutures must merge the per-batch record + // lists collected from more than one CompletableFuture into a single BulkInsertResponse. + ArrayList records = new ArrayList<>(); for (int i = 0; i < 75; i++) { Map data = new HashMap<>(); data.put("name", "john" + i); - records.add(BulkInsertRecord.builder().table("table1").data(data).build()); + records.add(BulkInsertRequestRecord.builder().tableName("table1").data(data).build()); } BulkInsertRequest request = BulkInsertRequest.builder().records(records).build(); BulkInsertResponse response = controller.bulkInsert(request); Assert.assertNotNull(INVALID_EXCEPTION_THROWN, response); Mockito.verify(mockRaw, Mockito.times(2)).insert(any(), any()); - Assert.assertTrue(response.getErrors().isEmpty()); - Assert.assertEquals(2, response.getSuccess().size()); + Assert.assertEquals(2, response.getRecords().size()); + Assert.assertTrue(response.getRecords().stream().allMatch(r -> r.getError() == null)); Assert.assertEquals(75, response.getSummary().getTotalRecords()); Assert.assertEquals(2, response.getSummary().getTotalInserted()); + Assert.assertEquals(0, response.getSummary().getTotalFailed()); - Success batch1Success = response.getSuccess().stream() + BulkInsertResponseRecord batch1Success = response.getRecords().stream() .filter(s -> "sky-id-batch1".equals(s.getSkyflowId())).findFirst().orElse(null); - Success batch2Success = response.getSuccess().stream() + BulkInsertResponseRecord batch2Success = response.getRecords().stream() .filter(s -> "sky-id-batch2".equals(s.getSkyflowId())).findFirst().orElse(null); Assert.assertNotNull(batch1Success); Assert.assertNotNull(batch2Success); @@ -1051,11 +619,11 @@ public void testBulkInsert_partialBatchFailureKeepsBatch1SuccessAndBatch2Error() VaultController controller = createControllerWithMock(mockApi); // 51 records -> batch 1 has 50 records (succeeds), batch 2 has 1 record (fails). - ArrayList records = new ArrayList<>(); + ArrayList records = new ArrayList<>(); for (int i = 0; i < 51; i++) { Map data = new HashMap<>(); data.put("name", "john" + i); - records.add(BulkInsertRecord.builder().table("table1").data(data).build()); + records.add(BulkInsertRequestRecord.builder().tableName("table1").data(data).build()); } BulkInsertRequest request = BulkInsertRequest.builder().records(records).build(); @@ -1063,13 +631,19 @@ public void testBulkInsert_partialBatchFailureKeepsBatch1SuccessAndBatch2Error() Assert.assertNotNull(INVALID_EXCEPTION_THROWN, response); Mockito.verify(mockRaw, Mockito.times(2)).insert(any(), any()); - Assert.assertEquals(1, response.getSuccess().size()); - Assert.assertEquals("sky-id-batch1", response.getSuccess().get(0).getSkyflowId()); - Assert.assertEquals(0, response.getSuccess().get(0).getIndex()); + Assert.assertEquals(2, response.getRecords().size()); - Assert.assertEquals(1, response.getErrors().size()); - Assert.assertEquals(50, response.getErrors().get(0).getIndex()); - Assert.assertEquals(500, response.getErrors().get(0).getCode()); + BulkInsertResponseRecord inserted = response.getRecords().stream() + .filter(r -> r.getError() == null).findFirst().orElse(null); + Assert.assertNotNull(inserted); + Assert.assertEquals("sky-id-batch1", inserted.getSkyflowId()); + Assert.assertEquals(0, inserted.getIndex()); + + BulkInsertResponseRecord failed = response.getRecords().stream() + .filter(r -> r.getError() != null).findFirst().orElse(null); + Assert.assertNotNull(failed); + Assert.assertEquals(50, failed.getIndex()); + Assert.assertEquals(500, failed.getHttpCode()); Assert.assertEquals(51, response.getSummary().getTotalRecords()); Assert.assertEquals(1, response.getSummary().getTotalInserted()); @@ -1101,353 +675,424 @@ public void testBulkInsert_successWithListOfMapsTokenShape() throws Exception { Map data = new HashMap<>(); data.put("name", "john"); - ArrayList records = new ArrayList<>(); - records.add(BulkInsertRecord.builder().table("table1").data(data).build()); + ArrayList records = new ArrayList<>(); + records.add(BulkInsertRequestRecord.builder().tableName("table1").data(data).build()); BulkInsertRequest request = BulkInsertRequest.builder().records(records).build(); BulkInsertResponse response = controller.bulkInsert(request); Assert.assertNotNull(INVALID_EXCEPTION_THROWN, response); - Assert.assertEquals(1, response.getSuccess().size()); - - Success success = response.getSuccess().get(0); - Assert.assertNotNull(success.getTokens()); - List field1Tokens = success.getTokens().get("field1"); - Assert.assertNotNull(field1Tokens); - Assert.assertEquals(1, field1Tokens.size()); - Assert.assertEquals("tok-xyz", field1Tokens.get(0).getToken()); - Assert.assertEquals("group1", field1Tokens.get(0).getTokenGroupName()); + Assert.assertEquals(1, response.getRecords().size()); + + BulkInsertResponseRecord inserted = response.getRecords().get(0); + Assert.assertNotNull(inserted.getFields()); + // The token map is surfaced verbatim as `fields`, so a List token shape survives intact. + Object field1Tokens = inserted.getFields().get("field1"); + Assert.assertTrue(field1Tokens instanceof List); + Assert.assertEquals(1, ((List) field1Tokens).size()); + Map field1Token = (Map) ((List) field1Tokens).get(0); + Assert.assertEquals("tok-xyz", field1Token.get("token")); + Assert.assertEquals("group1", field1Token.get("tokenGroupName")); } - // ── detokenize/tokenize interceptor header wiring ───────────────────────── - - @Test - public void testDetokenize_interceptorAddsCustomHeader() throws Exception { - ApiClient mockApi = Mockito.mock(ApiClient.class); - RawFlowserviceClient mockRaw = mockRawFlowservice(mockApi); - - V1FlowDetokenizeResponseObject record = V1FlowDetokenizeResponseObject.builder() - .token("token1").value("secret-value").build(); - V1FlowDetokenizeResponse body = V1FlowDetokenizeResponse.builder() - .response(Collections.singletonList(record)).build(); - ApiClientHttpResponse httpResp = new ApiClientHttpResponse<>(body, buildOkHttpResponse()); - when(mockRaw.detokenize(any(), any())).thenReturn(httpResp); - - VaultController controller = createControllerWithMock(mockApi); - - ArrayList data = new ArrayList<>(); - data.add(new DetokenizeData("token1")); - DetokenizeRequest request = DetokenizeRequest.builder().detokenizeData(data).build(); + // Tests for the unary query / get controller methods were removed: VaultController is bulk-only now. - RequestInterceptor interceptor = ctx -> ctx.addHeader(CustomHeaderKey.SkyflowAccountName, "acct-name"); - DetokenizeOptions options = DetokenizeOptions.builder().interceptor(interceptor).build(); + // ───────────────────────────────────────────────────────────────────────── + // Request fidelity through batch dispatch + // + // Constants.INSERT/DETOKENIZE/TOKENIZE/DELETE_TOKENS_BATCH_SIZE all default to 50, so a + // 120-item request produces exactly three batches (50 + 50 + 20). These tests assert that + // (a) non-batched fields are re-applied to EVERY outgoing batch request, (b) item order is + // preserved end to end, (c) the SDK-assigned response index equals the item's position in + // the ORIGINAL user list, and (d) a registered interceptor runs once per batch. + // ───────────────────────────────────────────────────────────────────────── + + private static final int MULTI_BATCH_ITEM_COUNT = 120; + private static final int EXPECTED_BATCH_COUNT = 3; + + /** Interceptor that records each RequestContext it is handed and stamps a per-batch header. */ + private static final class CountingInterceptor implements RequestInterceptor { + private final List contexts = + java.util.Collections.synchronizedList(new ArrayList<>()); + + @Override + public void intercept(com.skyflow.vault.data.RequestContext context) { + int callNumber; + synchronized (contexts) { + callNumber = contexts.size(); + contexts.add(context); + } + context.addHeader(CustomHeaderKey.SkyflowAccountId, "batch-" + callNumber); + } - controller.detokenize(request, options); + int callCount() { + return contexts.size(); + } - ArgumentCaptor captor = ArgumentCaptor.forClass(RequestOptions.class); - Mockito.verify(mockRaw).detokenize(any(), captor.capture()); - Assert.assertEquals("acct-name", captor.getValue().getHeaders().get(CustomHeaderKey.SkyflowAccountName.toString())); + List contexts() { + return contexts; + } } - @Test - public void testTokenize_interceptorAddsCustomHeader() throws Exception { - ApiClient mockApi = Mockito.mock(ApiClient.class); - RawFlowserviceClient mockRaw = mockRawFlowservice(mockApi); + private static void assertInterceptorRanOncePerBatch(CountingInterceptor interceptor, + List capturedOptions) { + Assert.assertEquals(EXPECTED_BATCH_COUNT, interceptor.callCount()); + // Each batch must get its own RequestContext — never a shared/reused one. + java.util.Set identities = new java.util.HashSet<>(); + for (com.skyflow.vault.data.RequestContext ctx : interceptor.contexts()) { + identities.add(System.identityHashCode(ctx)); + } + Assert.assertEquals(EXPECTED_BATCH_COUNT, identities.size()); + + // The header the interceptor set on each context must reach that batch's RequestOptions. + Assert.assertEquals(EXPECTED_BATCH_COUNT, capturedOptions.size()); + java.util.Set headerValues = new java.util.HashSet<>(); + for (RequestOptions options : capturedOptions) { + String value = options.getHeaders().get(CustomHeaderKey.SkyflowAccountId.toString()); + Assert.assertNotNull("Interceptor header missing on a batch", value); + headerValues.add(value); + } + Assert.assertEquals( + new java.util.HashSet<>(java.util.Arrays.asList("batch-0", "batch-1", "batch-2")), + headerValues); + } - FlowTokenizeResponseObjectToken token = FlowTokenizeResponseObjectToken.builder() - .tokenGroupName("group1").token("tok-abc").build(); - V1FlowTokenizeResponseObject responseObject = V1FlowTokenizeResponseObject.builder() - .value("value1").tokens(Collections.singletonList(token)).build(); - V1FlowTokenizeResponse body = V1FlowTokenizeResponse.builder() - .response(Collections.singletonList(responseObject)).build(); - ApiClientHttpResponse httpResp = new ApiClientHttpResponse<>(body, buildOkHttpResponse()); - when(mockRaw.tokenize(any(), any())).thenReturn(httpResp); + private static ArrayList multiBatchInsertRecords() { + ArrayList records = new ArrayList<>(); + for (int i = 0; i < MULTI_BATCH_ITEM_COUNT; i++) { + Map data = new HashMap<>(); + data.put("pos", String.valueOf(i)); + records.add(BulkInsertRequestRecord.builder().data(data).build()); + } + return records; + } - VaultController controller = createControllerWithMock(mockApi); + private static List multiBatchTokens() { + List tokens = new ArrayList<>(); + for (int i = 0; i < MULTI_BATCH_ITEM_COUNT; i++) { + tokens.add("token-" + i); + } + return tokens; + } - ArrayList records = new ArrayList<>(); - records.add(TokenizeRecord.builder().value("value1").tokenGroupNames(Collections.singletonList("group1")).build()); - TokenizeRequest request = TokenizeRequest.builder().data(records).build(); + /** Echoes each request record back as a response record whose skyflowId encodes its "pos". */ + private static void stubInsertEcho(RawFlowserviceClient mockRaw) { + when(mockRaw.insert(any(), any())).thenAnswer(invocation -> { + com.skyflow.generated.rest.resources.flowservice.requests.V1InsertRequest req = + invocation.getArgument(0); + List responseRecords = new ArrayList<>(); + for (com.skyflow.generated.rest.types.V1InsertRecordData record : req.getRecords().get()) { + responseRecords.add(V1RecordResponseObject.builder() + .skyflowId("sky-" + record.getData().get().get("pos")) + .tableName(record.getTableName().orElse(null)) + .build()); + } + V1InsertResponse body = V1InsertResponse.builder().records(responseRecords).build(); + return new ApiClientHttpResponse<>(body, buildOkHttpResponse()); + }); + } - RequestInterceptor interceptor = ctx -> ctx.addHeader(CustomHeaderKey.SkyflowAccountID, "acct-123"); - TokenizeOptions options = TokenizeOptions.builder().interceptor(interceptor).build(); + /** Echoes each requested token back as a detokenize response record. */ + private static void stubDetokenizeEcho(RawFlowserviceClient mockRaw) { + when(mockRaw.detokenize(any(), any())).thenAnswer(invocation -> { + com.skyflow.generated.rest.resources.flowservice.requests.V1FlowDetokenizeRequest req = + invocation.getArgument(0); + List responseRecords = new ArrayList<>(); + for (String token : req.getTokens().get()) { + responseRecords.add(V1FlowDetokenizeResponseObject.builder().token(token).build()); + } + V1FlowDetokenizeResponse body = V1FlowDetokenizeResponse.builder() + .response(responseRecords).build(); + return new ApiClientHttpResponse<>(body, buildOkHttpResponse()); + }); + } - controller.tokenize(request, options); + /** Echoes each requested token back as a delete-token response record. */ + private static void stubDeleteTokensEcho(RawFlowserviceClient mockRaw) { + when(mockRaw.deletetoken(any(), any())).thenAnswer(invocation -> { + com.skyflow.generated.rest.resources.flowservice.requests.V1FlowDeleteTokenRequest req = + invocation.getArgument(0); + List responseRecords = new ArrayList<>(); + for (String token : req.getTokens().get()) { + responseRecords.add(V1DeleteTokenResponseObject.builder().value(token).build()); + } + V1FlowDeleteTokenResponse body = V1FlowDeleteTokenResponse.builder() + .tokens(responseRecords).build(); + return new ApiClientHttpResponse<>(body, buildOkHttpResponse()); + }); + } - ArgumentCaptor captor = ArgumentCaptor.forClass(RequestOptions.class); - Mockito.verify(mockRaw).tokenize(any(), captor.capture()); - Assert.assertEquals("acct-123", captor.getValue().getHeaders().get(CustomHeaderKey.SkyflowAccountID.toString())); + /** Echoes each requested tokenize value back with one token per requested group name. */ + private static void stubTokenizeEcho(RawFlowserviceClient mockRaw) { + when(mockRaw.tokenize(any(), any())).thenAnswer(invocation -> { + com.skyflow.generated.rest.resources.flowservice.requests.V1FlowTokenizeRequest req = + invocation.getArgument(0); + List responseRecords = new ArrayList<>(); + for (com.skyflow.generated.rest.types.V1FlowTokenizeRequestObject obj : req.getData().get()) { + FlowTokenizeResponseObjectToken token = FlowTokenizeResponseObjectToken.builder() + .tokenGroupName("group1") + .token("tok-" + obj.getValue().get()) + .build(); + responseRecords.add(V1FlowTokenizeResponseObject.builder() + .value(obj.getValue().get()) + .tokens(Collections.singletonList(token)) + .build()); + } + V1FlowTokenizeResponse body = V1FlowTokenizeResponse.builder().response(responseRecords).build(); + return new ApiClientHttpResponse<>(body, buildOkHttpResponse()); + }); } - // ── query ───────────────────────────────────────────────────────────────── + // ── bulk insert: batch dispatch fidelity ───────────────────────────────── @Test - public void testQuery_success() throws Exception { + public void testBulkInsert_tableNameAndVaultIdReAppliedOnEveryBatch() throws Exception { ApiClient mockApi = Mockito.mock(ApiClient.class); - RawRecordsClient mockRaw = mockRawRecords(mockApi); - - Map data = new HashMap<>(); - data.put("skyflowId", "sky-id-1"); - data.put("name", "john"); - V1ExecuteQueryRecordResponse record = V1ExecuteQueryRecordResponse.builder().data(data).build(); - V1ExecuteQueryResponse body = V1ExecuteQueryResponse.builder().records(Collections.singletonList(record)).build(); - ApiClientHttpResponse httpResp = new ApiClientHttpResponse<>(body, buildOkHttpResponse()); - when(mockRaw.flowServiceExecuteQuery(any(), any())).thenReturn(httpResp); + RawFlowserviceClient mockRaw = mockRawFlowservice(mockApi); + stubInsertEcho(mockRaw); VaultController controller = createControllerWithMock(mockApi); + BulkInsertRequest request = BulkInsertRequest.builder() + .tableName("cards") + .records(multiBatchInsertRecords()) + .build(); - QueryRequest request = QueryRequest.builder().query("SELECT * FROM table1").build(); - - QueryResponse response = controller.query(request); - Assert.assertNotNull(INVALID_EXCEPTION_THROWN, response); - Assert.assertEquals(1, response.getFields().size()); - Assert.assertEquals("sky-id-1", response.getFields().get(0).get("skyflowId")); - Assert.assertNull(response.getErrors()); + controller.bulkInsert(request); + + ArgumentCaptor captor = + ArgumentCaptor.forClass(com.skyflow.generated.rest.resources.flowservice.requests.V1InsertRequest.class); + Mockito.verify(mockRaw, Mockito.times(EXPECTED_BATCH_COUNT)).insert(captor.capture(), any()); + + int expectedPos = 0; + for (com.skyflow.generated.rest.resources.flowservice.requests.V1InsertRequest sent : captor.getAllValues()) { + // insertBatch rebuilds the request per batch, so tableName/vaultId must be re-applied. + Assert.assertEquals("cards", sent.getTableName().get()); + Assert.assertEquals("vault123", sent.getVaultId().get()); + for (com.skyflow.generated.rest.types.V1InsertRecordData record : sent.getRecords().get()) { + Assert.assertEquals("cards", record.getTableName().get()); + Assert.assertEquals(String.valueOf(expectedPos), record.getData().get().get("pos")); + expectedPos++; + } + } + Assert.assertEquals(MULTI_BATCH_ITEM_COUNT, expectedPos); } @Test - public void testQuery_apiErrorThrowsSkyflowException() throws Exception { + public void testBulkInsert_responseIndexMapsToOriginalInputPosition_acrossBatches() throws Exception { ApiClient mockApi = Mockito.mock(ApiClient.class); - RawRecordsClient mockRaw = mockRawRecords(mockApi); - when(mockRaw.flowServiceExecuteQuery(any(), any())) - .thenThrow(new ApiClientApiException("query failed", 401, "unauthorized")); + RawFlowserviceClient mockRaw = mockRawFlowservice(mockApi); + stubInsertEcho(mockRaw); VaultController controller = createControllerWithMock(mockApi); - QueryRequest request = QueryRequest.builder().query("SELECT * FROM table1").build(); + BulkInsertRequest request = BulkInsertRequest.builder() + .tableName("cards") + .records(multiBatchInsertRecords()) + .build(); - try { - controller.query(request); - Assert.fail(EXCEPTION_NOT_THROWN); - } catch (SkyflowException e) { - Assert.assertEquals(401, e.getHttpCode()); + BulkInsertResponse response = controller.bulkInsert(request); + + Assert.assertEquals(MULTI_BATCH_ITEM_COUNT, response.getRecords().size()); + for (int i = 0; i < MULTI_BATCH_ITEM_COUNT; i++) { + BulkInsertResponseRecord record = response.getRecords().get(i); + Assert.assertEquals(i, record.getIndex()); + // skyflowId encodes the input record's position, so this proves index -> input position. + Assert.assertEquals("sky-" + i, record.getSkyflowId()); } } @Test - public void testQuery_networkErrorThrowsSkyflowException() throws Exception { + public void testBulkInsert_interceptorInvokedOncePerBatchWithDistinctContext() throws Exception { ApiClient mockApi = Mockito.mock(ApiClient.class); - RawRecordsClient mockRaw = mockRawRecords(mockApi); - when(mockRaw.flowServiceExecuteQuery(any(), any())) - .thenThrow(new ApiClientException("Network error executing HTTP request")); + RawFlowserviceClient mockRaw = mockRawFlowservice(mockApi); + stubInsertEcho(mockRaw); VaultController controller = createControllerWithMock(mockApi); - QueryRequest request = QueryRequest.builder().query("SELECT * FROM table1").build(); + BulkInsertRequest request = BulkInsertRequest.builder() + .tableName("cards") + .records(multiBatchInsertRecords()) + .build(); - try { - controller.query(request); - Assert.fail(EXCEPTION_NOT_THROWN); - } catch (SkyflowException e) { - Assert.assertNotNull(e.getMessage()); - } - } + CountingInterceptor interceptor = new CountingInterceptor(); + controller.bulkInsert(request, InsertOptions.builder().interceptor(interceptor).build()); - @Test - public void testQuery_emptyQueryThrowsSkyflowException() throws Exception { - ApiClient mockApi = Mockito.mock(ApiClient.class); - VaultController controller = createControllerWithMock(mockApi); - QueryRequest request = QueryRequest.builder().build(); - try { - controller.query(request); - Assert.fail(EXCEPTION_NOT_THROWN); - } catch (SkyflowException e) { - Assert.assertNotNull(e.getMessage()); - } + ArgumentCaptor captor = ArgumentCaptor.forClass(RequestOptions.class); + Mockito.verify(mockRaw, Mockito.times(EXPECTED_BATCH_COUNT)).insert(any(), captor.capture()); + assertInterceptorRanOncePerBatch(interceptor, captor.getAllValues()); } + // ── bulk detokenize: batch dispatch fidelity ───────────────────────────── + @Test - public void testQuery_interceptorAddsCustomHeader() throws Exception { + public void testBulkDetokenize_vaultIdAndRedactionsReachEveryBatchAndTokenOrderPreserved() throws Exception { ApiClient mockApi = Mockito.mock(ApiClient.class); - RawRecordsClient mockRaw = mockRawRecords(mockApi); - - Map data = new HashMap<>(); - data.put("skyflowId", "sky-id-1"); - V1ExecuteQueryRecordResponse record = V1ExecuteQueryRecordResponse.builder().data(data).build(); - V1ExecuteQueryResponse body = V1ExecuteQueryResponse.builder().records(Collections.singletonList(record)).build(); - ApiClientHttpResponse httpResp = new ApiClientHttpResponse<>(body, buildOkHttpResponse()); - when(mockRaw.flowServiceExecuteQuery(any(), any())).thenReturn(httpResp); + RawFlowserviceClient mockRaw = mockRawFlowservice(mockApi); + stubDetokenizeEcho(mockRaw); VaultController controller = createControllerWithMock(mockApi); - QueryRequest request = QueryRequest.builder().query("SELECT * FROM table1").build(); + List tokens = multiBatchTokens(); + BulkDetokenizeRequest request = BulkDetokenizeRequest.builder() + .tokens(tokens) + .tokenGroupRedactions(Collections.singletonList( + TokenGroupRedactions.builder().tokenGroupName("group one").redaction("MASKED").build())) + .build(); - RequestInterceptor interceptor = ctx -> ctx.addHeader(CustomHeaderKey.SkyflowAccountID, "acct-123"); - QueryOptions options = QueryOptions.builder().interceptor(interceptor).build(); + controller.bulkDetokenize(request); - controller.query(request, options); + ArgumentCaptor captor = + ArgumentCaptor.forClass(com.skyflow.generated.rest.resources.flowservice.requests.V1FlowDetokenizeRequest.class); + Mockito.verify(mockRaw, Mockito.times(EXPECTED_BATCH_COUNT)).detokenize(captor.capture(), any()); - ArgumentCaptor captor = ArgumentCaptor.forClass(RequestOptions.class); - Mockito.verify(mockRaw).flowServiceExecuteQuery(any(), captor.capture()); - Assert.assertEquals("acct-123", captor.getValue().getHeaders().get(CustomHeaderKey.SkyflowAccountID.toString())); + List flattened = new ArrayList<>(); + for (com.skyflow.generated.rest.resources.flowservice.requests.V1FlowDetokenizeRequest sent : captor.getAllValues()) { + Assert.assertEquals("vault123", sent.getVaultId().get()); + Assert.assertTrue(sent.getTokenGroupRedactions().isPresent()); + Assert.assertEquals("group one", sent.getTokenGroupRedactions().get().get(0).getTokenGroupName().get()); + Assert.assertEquals("MASKED", sent.getTokenGroupRedactions().get().get(0).getRedaction().get()); + flattened.addAll(sent.getTokens().get()); + } + Assert.assertEquals(tokens, flattened); } - // ── get ─────────────────────────────────────────────────────────────────── - @Test - public void testGet_success() throws Exception { + public void testBulkDetokenize_responseIndexMapsToOriginalInputPosition_acrossBatches() throws Exception { ApiClient mockApi = Mockito.mock(ApiClient.class); RawFlowserviceClient mockRaw = mockRawFlowservice(mockApi); - - Map data = new HashMap<>(); - data.put("name", "john"); - V1RecordResponseObject record = V1RecordResponseObject.builder().skyflowId("sky-id-1").data(data).build(); - V1GetResponse body = V1GetResponse.builder().records(Collections.singletonList(record)).build(); - ApiClientHttpResponse httpResp = new ApiClientHttpResponse<>(body, buildOkHttpResponse()); - when(mockRaw.get(any(), any())).thenReturn(httpResp); + stubDetokenizeEcho(mockRaw); VaultController controller = createControllerWithMock(mockApi); + List tokens = multiBatchTokens(); + BulkDetokenizeRequest request = BulkDetokenizeRequest.builder().tokens(tokens).build(); - GetRequest request = GetRequest.builder() - .table("table1") - .ids(new ArrayList<>(Collections.singletonList("sky-id-1"))) - .build(); + BulkDetokenizeResponse response = controller.bulkDetokenize(request); - GetResponse response = controller.get(request); - Assert.assertNotNull(INVALID_EXCEPTION_THROWN, response); - Assert.assertEquals(1, response.getData().size()); - Assert.assertEquals("sky-id-1", response.getData().get(0).get("skyflowId")); - Assert.assertEquals("john", response.getData().get(0).get("name")); - Assert.assertTrue(response.getErrors().isEmpty()); + Assert.assertEquals(MULTI_BATCH_ITEM_COUNT, response.getRecords().size()); + for (int i = 0; i < MULTI_BATCH_ITEM_COUNT; i++) { + Assert.assertEquals(i, response.getRecords().get(i).getIndex()); + Assert.assertEquals(tokens.get(i), response.getRecords().get(i).getToken()); + } } @Test - public void testGet_apiErrorThrowsSkyflowException() throws Exception { + public void testBulkDetokenize_interceptorInvokedOncePerBatchWithDistinctContext() throws Exception { ApiClient mockApi = Mockito.mock(ApiClient.class); RawFlowserviceClient mockRaw = mockRawFlowservice(mockApi); - when(mockRaw.get(any(), any())) - .thenThrow(new ApiClientApiException("get failed", 401, "unauthorized")); + stubDetokenizeEcho(mockRaw); VaultController controller = createControllerWithMock(mockApi); - GetRequest request = GetRequest.builder() - .table("table1") - .ids(new ArrayList<>(Collections.singletonList("sky-id-1"))) - .build(); + BulkDetokenizeRequest request = BulkDetokenizeRequest.builder().tokens(multiBatchTokens()).build(); - try { - controller.get(request); - Assert.fail(EXCEPTION_NOT_THROWN); - } catch (SkyflowException e) { - Assert.assertEquals(401, e.getHttpCode()); - } + CountingInterceptor interceptor = new CountingInterceptor(); + controller.bulkDetokenize(request, DetokenizeOptions.builder().interceptor(interceptor).build()); + + ArgumentCaptor captor = ArgumentCaptor.forClass(RequestOptions.class); + Mockito.verify(mockRaw, Mockito.times(EXPECTED_BATCH_COUNT)).detokenize(any(), captor.capture()); + assertInterceptorRanOncePerBatch(interceptor, captor.getAllValues()); } + // ── bulk delete tokens: batch dispatch fidelity ────────────────────────── + @Test - public void testGet_networkErrorThrowsSkyflowException() throws Exception { + public void testBulkDeleteTokens_vaultIdOnEveryBatchAndTokenOrderPreserved() throws Exception { ApiClient mockApi = Mockito.mock(ApiClient.class); RawFlowserviceClient mockRaw = mockRawFlowservice(mockApi); - when(mockRaw.get(any(), any())) - .thenThrow(new ApiClientException("Network error executing HTTP request")); + stubDeleteTokensEcho(mockRaw); VaultController controller = createControllerWithMock(mockApi); - GetRequest request = GetRequest.builder() - .table("table1") - .ids(new ArrayList<>(Collections.singletonList("sky-id-1"))) - .build(); + List tokens = multiBatchTokens(); + BulkDeleteTokensRequest request = BulkDeleteTokensRequest.builder().tokens(tokens).build(); - try { - controller.get(request); - Assert.fail(EXCEPTION_NOT_THROWN); - } catch (SkyflowException e) { - Assert.assertNotNull(e.getMessage()); + BulkDeleteTokensResponse response = controller.bulkDeleteTokens(request); + + ArgumentCaptor captor = + ArgumentCaptor.forClass(com.skyflow.generated.rest.resources.flowservice.requests.V1FlowDeleteTokenRequest.class); + Mockito.verify(mockRaw, Mockito.times(EXPECTED_BATCH_COUNT)).deletetoken(captor.capture(), any()); + + List flattened = new ArrayList<>(); + for (com.skyflow.generated.rest.resources.flowservice.requests.V1FlowDeleteTokenRequest sent : captor.getAllValues()) { + Assert.assertEquals("vault123", sent.getVaultId().get()); + flattened.addAll(sent.getTokens().get()); } - } + Assert.assertEquals(tokens, flattened); - @Test - public void testGet_invalidRequestThrowsSkyflowException() throws Exception { - ApiClient mockApi = Mockito.mock(ApiClient.class); - VaultController controller = createControllerWithMock(mockApi); - GetRequest request = GetRequest.builder().table("table1").build(); - try { - controller.get(request); - Assert.fail(EXCEPTION_NOT_THROWN); - } catch (SkyflowException e) { - Assert.assertNotNull(e.getMessage()); + Assert.assertEquals(MULTI_BATCH_ITEM_COUNT, response.getSuccess().size()); + for (int i = 0; i < MULTI_BATCH_ITEM_COUNT; i++) { + Assert.assertEquals(i, response.getSuccess().get(i).getIndex()); + Assert.assertEquals(tokens.get(i), response.getSuccess().get(i).getToken()); } } @Test - public void testGet_interceptorAddsCustomHeader() throws Exception { + public void testBulkDeleteTokens_interceptorInvokedOncePerBatchWithDistinctContext() throws Exception { ApiClient mockApi = Mockito.mock(ApiClient.class); RawFlowserviceClient mockRaw = mockRawFlowservice(mockApi); - - Map data = new HashMap<>(); - data.put("name", "john"); - V1RecordResponseObject record = V1RecordResponseObject.builder().skyflowId("sky-id-1").data(data).build(); - V1GetResponse body = V1GetResponse.builder().records(Collections.singletonList(record)).build(); - ApiClientHttpResponse httpResp = new ApiClientHttpResponse<>(body, buildOkHttpResponse()); - when(mockRaw.get(any(), any())).thenReturn(httpResp); + stubDeleteTokensEcho(mockRaw); VaultController controller = createControllerWithMock(mockApi); - GetRequest request = GetRequest.builder() - .table("table1") - .ids(new ArrayList<>(Collections.singletonList("sky-id-1"))) - .build(); - - RequestInterceptor interceptor = ctx -> ctx.addHeader(CustomHeaderKey.SkyflowAccountID, "acct-123"); - GetOptions options = GetOptions.builder().interceptor(interceptor).build(); + BulkDeleteTokensRequest request = BulkDeleteTokensRequest.builder().tokens(multiBatchTokens()).build(); - controller.get(request, options); + CountingInterceptor interceptor = new CountingInterceptor(); + controller.bulkDeleteTokens(request, DeleteTokensOptions.builder().interceptor(interceptor).build()); ArgumentCaptor captor = ArgumentCaptor.forClass(RequestOptions.class); - Mockito.verify(mockRaw).get(any(), captor.capture()); - Assert.assertEquals("acct-123", captor.getValue().getHeaders().get(CustomHeaderKey.SkyflowAccountID.toString())); + Mockito.verify(mockRaw, Mockito.times(EXPECTED_BATCH_COUNT)).deletetoken(any(), captor.capture()); + assertInterceptorRanOncePerBatch(interceptor, captor.getAllValues()); } + // ── bulk tokenize: batch dispatch fidelity ─────────────────────────────── + @Test - public void testGet_multiTableRecordsSuccess() throws Exception { + public void testBulkTokenize_vaultIdOnEveryBatchAndValueOrderPreserved() throws Exception { ApiClient mockApi = Mockito.mock(ApiClient.class); RawFlowserviceClient mockRaw = mockRawFlowservice(mockApi); - - Map data1 = new HashMap<>(); - data1.put("name", "john"); - V1RecordResponseObject record1 = V1RecordResponseObject.builder() - .skyflowId("sky-id-1").tableName("table1").data(data1).build(); - Map data2 = new HashMap<>(); - data2.put("email", "jane@example.com"); - V1RecordResponseObject record2 = V1RecordResponseObject.builder() - .skyflowId("sky-id-2").tableName("table2").data(data2).build(); - V1GetResponse body = V1GetResponse.builder().records(java.util.Arrays.asList(record1, record2)).build(); - ApiClientHttpResponse httpResp = new ApiClientHttpResponse<>(body, buildOkHttpResponse()); - when(mockRaw.get(any(), any())).thenReturn(httpResp); + stubTokenizeEcho(mockRaw); VaultController controller = createControllerWithMock(mockApi); + ArrayList data = new ArrayList<>(); + for (int i = 0; i < MULTI_BATCH_ITEM_COUNT; i++) { + data.add(BulkTokenizeRecord.builder() + .value("value-" + i) + .tokenGroupNames(Collections.singletonList("group1")) + .build()); + } + BulkTokenizeRequest request = BulkTokenizeRequest.builder().data(data).build(); - com.skyflow.vault.data.GetRecordRequest recordRequest1 = com.skyflow.vault.data.GetRecordRequest.builder() - .table("table1") - .ids(new ArrayList<>(Collections.singletonList("sky-id-1"))) - .build(); - com.skyflow.vault.data.GetRecordRequest recordRequest2 = com.skyflow.vault.data.GetRecordRequest.builder() - .table("table2") - .ids(new ArrayList<>(Collections.singletonList("sky-id-2"))) - .build(); - GetRequest request = GetRequest.builder() - .records(java.util.Arrays.asList(recordRequest1, recordRequest2)) - .build(); + controller.bulkTokenize(request); - GetResponse response = controller.get(request); - Assert.assertNotNull(INVALID_EXCEPTION_THROWN, response); - Assert.assertEquals(2, response.getData().size()); - Assert.assertEquals("table1", response.getData().get(0).get("tableName")); - Assert.assertEquals("table2", response.getData().get(1).get("tableName")); + ArgumentCaptor captor = + ArgumentCaptor.forClass(com.skyflow.generated.rest.resources.flowservice.requests.V1FlowTokenizeRequest.class); + Mockito.verify(mockRaw, Mockito.times(EXPECTED_BATCH_COUNT)).tokenize(captor.capture(), any()); + + List flattened = new ArrayList<>(); + for (com.skyflow.generated.rest.resources.flowservice.requests.V1FlowTokenizeRequest sent : captor.getAllValues()) { + Assert.assertEquals("vault123", sent.getVaultId().get()); + for (com.skyflow.generated.rest.types.V1FlowTokenizeRequestObject obj : sent.getData().get()) { + Assert.assertEquals(Collections.singletonList("group1"), obj.getTokenGroupNames().get()); + flattened.add(obj.getValue().get()); + } + } + Assert.assertEquals(MULTI_BATCH_ITEM_COUNT, flattened.size()); + for (int i = 0; i < MULTI_BATCH_ITEM_COUNT; i++) { + Assert.assertEquals("value-" + i, flattened.get(i)); + } } @Test - public void testGet_bothTableAndRecordsThrowsSkyflowException() throws Exception { + public void testBulkTokenize_interceptorInvokedOncePerBatchWithDistinctContext() throws Exception { ApiClient mockApi = Mockito.mock(ApiClient.class); + RawFlowserviceClient mockRaw = mockRawFlowservice(mockApi); + stubTokenizeEcho(mockRaw); + VaultController controller = createControllerWithMock(mockApi); + ArrayList data = new ArrayList<>(); + for (int i = 0; i < MULTI_BATCH_ITEM_COUNT; i++) { + data.add(BulkTokenizeRecord.builder().value("value-" + i).build()); + } + BulkTokenizeRequest request = BulkTokenizeRequest.builder().data(data).build(); - com.skyflow.vault.data.GetRecordRequest recordRequest = com.skyflow.vault.data.GetRecordRequest.builder() - .table("table2") - .ids(new ArrayList<>(Collections.singletonList("id2"))) - .build(); - GetRequest request = GetRequest.builder() - .table("table1") - .ids(new ArrayList<>(Collections.singletonList("id1"))) - .records(Collections.singletonList(recordRequest)) - .build(); + CountingInterceptor interceptor = new CountingInterceptor(); + controller.bulkTokenize(request, TokenizeOptions.builder().interceptor(interceptor).build()); - try { - controller.get(request); - Assert.fail(EXCEPTION_NOT_THROWN); - } catch (SkyflowException e) { - Assert.assertNotNull(e.getMessage()); - } + ArgumentCaptor captor = ArgumentCaptor.forClass(RequestOptions.class); + Mockito.verify(mockRaw, Mockito.times(EXPECTED_BATCH_COUNT)).tokenize(any(), captor.capture()); + assertInterceptorRanOncePerBatch(interceptor, captor.getAllValues()); } } diff --git a/flowvault/src/test/java/com/skyflow/vault/data/BulkResponseTests.java b/flowvault/src/test/java/com/skyflow/vault/data/BulkResponseTests.java index ead482b1..d765345e 100644 --- a/flowvault/src/test/java/com/skyflow/vault/data/BulkResponseTests.java +++ b/flowvault/src/test/java/com/skyflow/vault/data/BulkResponseTests.java @@ -6,46 +6,47 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; +import java.util.HashMap; import java.util.List; +import java.util.Map; /** * Tests for the Bulk*Response classes: {@link BulkInsertResponse}, {@link BulkDetokenizeResponse}, * {@link BulkDeleteTokensResponse} and {@link BulkTokenizeResponse}. * - *

Each has a 2-arg constructor (success/errors only, summary and originalPayload-derived - * fields stay null) and a 3-arg constructor (adds originalPayload and computes a summary). - * BulkInsertResponse/BulkDetokenizeResponse additionally derive a retry list from the - * originalPayload, filtering on a "5xx except 529" retryable-status rule. + *

BulkDeleteTokensResponse/BulkTokenizeResponse each have a 2-arg constructor (success/errors + * only, summary and originalPayload-derived fields stay null) and a 3-arg constructor (adds + * originalPayload and computes a summary). BulkInsertResponse and BulkDetokenizeResponse instead + * carry a single unified {@code records} list, with a 1-arg (per-batch) and a 2-arg (final, + * summary-computing) constructor. Both additionally derive a retry list from the originalPayload, + * filtering on a "5xx except 529" retryable-status rule. */ public class BulkResponseTests { // ── BulkInsertResponse ─────────────────────────────────────────────────── @Test - public void testBulkInsertResponse_twoArgConstructorLeavesSummaryAndRetryDependenciesNull() { - List success = Collections.emptyList(); - List errors = Collections.emptyList(); + public void testBulkInsertResponse_oneArgConstructorLeavesSummaryAndRetryDependenciesNull() { + List records = Collections.emptyList(); - BulkInsertResponse response = new BulkInsertResponse(success, errors); + BulkInsertResponse response = new BulkInsertResponse(records); Assert.assertNull(response.getSummary()); - Assert.assertEquals(success, response.getSuccess()); - Assert.assertEquals(errors, response.getErrors()); - // No errors, so the retry list is empty and originalPayload (null) is never dereferenced. + Assert.assertEquals(records, response.getRecords()); + // No failed records, so the retry list is empty and originalPayload (null) is never dereferenced. Assert.assertTrue(response.getRecordsToRetry().isEmpty()); } @Test - public void testBulkInsertResponse_threeArgConstructorComputesSummary() { - List success = Collections.singletonList( - new Success(0, "id-1", null, null, "table1")); - List errors = Collections.singletonList( - new ErrorRecord(1, "failed", 400)); - ArrayList originalPayload = new ArrayList<>(Arrays.asList( - BulkInsertRecord.builder().table("table1").build(), - BulkInsertRecord.builder().table("table1").build())); + public void testBulkInsertResponse_twoArgConstructorComputesSummary() { + List records = Arrays.asList( + new BulkInsertResponseRecord(0, "table1", "id-1", null, null, 200, null, null), + new BulkInsertResponseRecord(1, null, null, null, null, 400, "failed", null)); + List originalPayload = new ArrayList<>(Arrays.asList( + BulkInsertRequestRecord.builder().tableName("table1").build(), + BulkInsertRequestRecord.builder().tableName("table1").build())); - BulkInsertResponse response = new BulkInsertResponse(success, errors, originalPayload); + BulkInsertResponse response = new BulkInsertResponse(records, originalPayload); Assert.assertNotNull(response.getSummary()); Assert.assertEquals(2, response.getSummary().getTotalRecords()); @@ -53,25 +54,46 @@ public void testBulkInsertResponse_threeArgConstructorComputesSummary() { Assert.assertEquals(1, response.getSummary().getTotalFailed()); } + @Test + public void testBulkInsertResponse_recordsPreserveIndexAndInheritedFields() { + Map fields = new HashMap<>(); + fields.put("name", "token-name"); + Map hashedData = new HashMap<>(); + hashedData.put("name", "hashed-name"); + + BulkInsertResponseRecord record = new BulkInsertResponseRecord( + 7, "table1", "id-1", fields, hashedData, 200, null, null); + + BulkInsertResponse response = new BulkInsertResponse(Collections.singletonList(record)); + + BulkInsertResponseRecord actual = response.getRecords().get(0); + Assert.assertEquals(7, actual.getIndex()); + Assert.assertEquals("table1", actual.getTableName()); + Assert.assertEquals("id-1", actual.getSkyflowId()); + Assert.assertEquals(fields, actual.getFields()); + Assert.assertEquals(hashedData, actual.getHashedData()); + Assert.assertEquals(200, actual.getHttpCode()); + Assert.assertNull(actual.getError()); + } + @Test public void testBulkInsertResponse_getRecordsToRetryFiltersRetryableStatusCodesOnly() { - BulkInsertRecord record0 = BulkInsertRecord.builder().table("t0").build(); - BulkInsertRecord record1 = BulkInsertRecord.builder().table("t1").build(); - BulkInsertRecord record2 = BulkInsertRecord.builder().table("t2").build(); - BulkInsertRecord record3 = BulkInsertRecord.builder().table("t3").build(); - ArrayList originalPayload = new ArrayList<>( + BulkInsertRequestRecord record0 = BulkInsertRequestRecord.builder().tableName("t0").build(); + BulkInsertRequestRecord record1 = BulkInsertRequestRecord.builder().tableName("t1").build(); + BulkInsertRequestRecord record2 = BulkInsertRequestRecord.builder().tableName("t2").build(); + BulkInsertRequestRecord record3 = BulkInsertRequestRecord.builder().tableName("t3").build(); + List originalPayload = new ArrayList<>( Arrays.asList(record0, record1, record2, record3)); - List errors = Arrays.asList( - new ErrorRecord(0, "server error", 500), // retryable (lower bound) - new ErrorRecord(1, "bad request", 400), // not retryable - new ErrorRecord(2, "server error", 599), // retryable (upper bound) - new ErrorRecord(3, "special case", 529)); // explicitly excluded + List records = Arrays.asList( + new BulkInsertResponseRecord(0, null, null, null, null, 500, "server error", null), // retryable (lower bound) + new BulkInsertResponseRecord(1, null, null, null, null, 400, "bad request", null), // not retryable + new BulkInsertResponseRecord(2, null, null, null, null, 599, "server error", null), // retryable (upper bound) + new BulkInsertResponseRecord(3, null, null, null, null, 529, "special case", null)); // explicitly excluded - BulkInsertResponse response = new BulkInsertResponse( - Collections.emptyList(), errors, originalPayload); + BulkInsertResponse response = new BulkInsertResponse(records, originalPayload); - ArrayList recordsToRetry = response.getRecordsToRetry(); + List recordsToRetry = response.getRecordsToRetry(); Assert.assertEquals(2, recordsToRetry.size()); Assert.assertTrue(recordsToRetry.contains(record0)); @@ -82,51 +104,96 @@ public void testBulkInsertResponse_getRecordsToRetryFiltersRetryableStatusCodesO @Test public void testBulkInsertResponse_toStringNotNull() { - BulkInsertResponse response = new BulkInsertResponse(Collections.emptyList(), Collections.emptyList()); + BulkInsertResponse response = new BulkInsertResponse(Collections.emptyList()); Assert.assertNotNull(response.toString()); } + @Test + public void testBulkInsertResponse_toStringSerializesSummaryAndRecordsButNotInternals() { + List records = Collections.singletonList( + new BulkInsertResponseRecord(0, "table1", "id-1", null, null, 200, null, null)); + List originalPayload = new ArrayList( + Collections.singletonList(BulkInsertRequestRecord.builder().tableName("table1").build())); + + BulkInsertResponse response = new BulkInsertResponse(records, originalPayload); + // Populate the lazily-derived internal so we can prove it is still excluded. + response.getRecordsToRetry(); + String json = response.toString(); + + Assert.assertTrue(json.contains("summary")); + Assert.assertTrue(json.contains("records")); + // serializeNulls() spells out the nulls on each record. + Assert.assertTrue(json.contains("\"error\":null")); + // transient internals stay out of the JSON. + Assert.assertFalse(json.contains("originalPayload")); + Assert.assertFalse(json.contains("recordsToRetry")); + } + + @Test + public void testBulkInsertResponse_getRecordsToRetryOnPerBatchResponseDoesNotThrow() { + // The 1-arg constructor leaves originalPayload null. A 5xx record must not NPE here. + List records = Collections.singletonList( + new BulkInsertResponseRecord(0, null, null, null, null, 500, "server error", null)); + + BulkInsertResponse response = new BulkInsertResponse(records); + + Assert.assertTrue(response.getRecordsToRetry().isEmpty()); + } + // ── BulkDetokenizeResponse ─────────────────────────────────────────────── @Test - public void testBulkDetokenizeResponse_twoArgConstructorLeavesSummaryNull() { - List success = Collections.emptyList(); - List errors = Collections.emptyList(); + public void testBulkDetokenizeResponse_oneArgConstructorLeavesSummaryAndRetryDependenciesNull() { + List records = Collections.emptyList(); - BulkDetokenizeResponse response = new BulkDetokenizeResponse(success, errors); + BulkDetokenizeResponse response = new BulkDetokenizeResponse(records); Assert.assertNull(response.getSummary()); - Assert.assertEquals(success, response.getSuccess()); - Assert.assertEquals(errors, response.getErrors()); + Assert.assertEquals(records, response.getRecords()); + // No retryable records, so originalPayload (null) is never dereferenced. Assert.assertTrue(response.getTokensToRetry().isEmpty()); } @Test - public void testBulkDetokenizeResponse_threeArgConstructorComputesSummary() { - List success = Collections.singletonList( - new DetokenizeResponseObject(0, "tok-1", "value", "group1", null, null)); - List errors = Collections.singletonList(new ErrorRecord(1, "failed", 404)); + public void testBulkDetokenizeResponse_twoArgConstructorComputesSummary() { + List records = Arrays.asList( + new BulkDetokenizeResponseRecord(0, "tok-1", "secret-value", "group1", null, 200, null, null), + new BulkDetokenizeResponseRecord(1, "tok-2", null, null, null, 404, "failed", null)); List originalPayload = Arrays.asList("tok-1", "tok-2"); - BulkDetokenizeResponse response = new BulkDetokenizeResponse(success, errors, originalPayload); + BulkDetokenizeResponse response = new BulkDetokenizeResponse(records, originalPayload); Assert.assertNotNull(response.getSummary()); Assert.assertEquals(2, response.getSummary().getTotalTokens()); Assert.assertEquals(1, response.getSummary().getTotalDetokenized()); Assert.assertEquals(1, response.getSummary().getTotalFailed()); + Assert.assertEquals(records, response.getRecords()); + } + + @Test + public void testBulkDetokenizeResponse_summaryTotalTokensComesFromOriginalPayloadNotRecords() { + // Only one of the three submitted tokens came back, so totalTokens tracks the payload size. + List records = Collections.singletonList( + new BulkDetokenizeResponseRecord(0, "tok-0", "plain-0", "group1", null, 200, null, null)); + List originalPayload = Arrays.asList("tok-0", "tok-1", "tok-2"); + + BulkDetokenizeResponse response = new BulkDetokenizeResponse(records, originalPayload); + + Assert.assertEquals(3, response.getSummary().getTotalTokens()); + Assert.assertEquals(1, response.getSummary().getTotalDetokenized()); + Assert.assertEquals(0, response.getSummary().getTotalFailed()); } @Test public void testBulkDetokenizeResponse_getTokensToRetryFiltersRetryableStatusCodesOnly() { List originalPayload = Arrays.asList("tok-0", "tok-1", "tok-2", "tok-3"); - List errors = Arrays.asList( - new ErrorRecord(0, "server error", 500), - new ErrorRecord(1, "bad request", 400), - new ErrorRecord(2, "server error", 599), - new ErrorRecord(3, "special case", 529)); + List records = Arrays.asList( + new BulkDetokenizeResponseRecord(0, null, null, null, null, 500, "server error", null), + new BulkDetokenizeResponseRecord(1, null, null, null, null, 400, "bad request", null), + new BulkDetokenizeResponseRecord(2, null, null, null, null, 599, "server error", null), + new BulkDetokenizeResponseRecord(3, null, null, null, null, 529, "special case", null)); - BulkDetokenizeResponse response = new BulkDetokenizeResponse( - Collections.emptyList(), errors, originalPayload); + BulkDetokenizeResponse response = new BulkDetokenizeResponse(records, originalPayload); List tokensToRetry = response.getTokensToRetry(); @@ -139,10 +206,51 @@ public void testBulkDetokenizeResponse_getTokensToRetryFiltersRetryableStatusCod @Test public void testBulkDetokenizeResponse_toStringNotNull() { - BulkDetokenizeResponse response = new BulkDetokenizeResponse(Collections.emptyList(), Collections.emptyList()); + BulkDetokenizeResponse response = new BulkDetokenizeResponse(Collections.emptyList()); Assert.assertNotNull(response.toString()); } + @Test + public void testBulkDetokenizeResponse_toStringSerializesSummaryAndRecordsButNotInternals() { + List records = Collections.singletonList( + new BulkDetokenizeResponseRecord(0, "tok-0", "plain-0", "group1", null, 200, null, null)); + List originalPayload = Collections.singletonList("tok-0"); + + BulkDetokenizeResponse response = new BulkDetokenizeResponse(records, originalPayload); + // Populate the lazily-derived internal so we can prove it is still excluded. + response.getTokensToRetry(); + String json = response.toString(); + + Assert.assertTrue(json.contains("summary")); + Assert.assertTrue(json.contains("records")); + // serializeNulls() spells out the nulls on each record. + Assert.assertTrue(json.contains("\"error\":null")); + // transient internals stay out of the JSON. + Assert.assertFalse(json.contains("originalPayload")); + Assert.assertFalse(json.contains("tokensToRetry")); + } + + @Test + public void testBulkDetokenizeResponse_getTokensToRetryOnPerBatchResponseDoesNotThrow() { + List records = Collections.singletonList( + new BulkDetokenizeResponseRecord(0, "tok-0", null, null, null, 500, "server error", null)); + + BulkDetokenizeResponse response = new BulkDetokenizeResponse(records); + + Assert.assertTrue(response.getTokensToRetry().isEmpty()); + } + + @Test + public void testBulkDetokenizeResponse_recordsExposeDetokenizedValue() { + // value is passed through verbatim from the generated response object. + List records = Collections.singletonList( + new BulkDetokenizeResponseRecord(0, "tok-0", "john@example.com", "group1", null, 200, null, null)); + + BulkDetokenizeResponse response = new BulkDetokenizeResponse(records); + + Assert.assertEquals("john@example.com", response.getRecords().get(0).getValue()); + } + // ── BulkDeleteTokensResponse ───────────────────────────────────────────── @Test diff --git a/flowvault/src/test/java/com/skyflow/vault/data/RecordAndRedactionTests.java b/flowvault/src/test/java/com/skyflow/vault/data/RecordAndRedactionTests.java index c51d88fd..5e6ee8f8 100644 --- a/flowvault/src/test/java/com/skyflow/vault/data/RecordAndRedactionTests.java +++ b/flowvault/src/test/java/com/skyflow/vault/data/RecordAndRedactionTests.java @@ -1,6 +1,5 @@ package com.skyflow.vault.data; -import com.skyflow.enums.UpsertType; import org.junit.Assert; import org.junit.Test; @@ -11,8 +10,8 @@ /** * Tests for the plain record/redaction data holders: {@link TokenGroupRedactions}, - * {@link BulkTokenGroupRedactions}, {@link InsertRecord}, {@link BulkInsertRecord}, - * {@link TokenizeRecord} and {@link BulkTokenizeRecord}. None of these classes perform + * {@link InsertRequestRecord}, {@link BulkInsertRequestRecord} + * and {@link BulkTokenizeRecord}. None of these classes perform * validation in their builders, so coverage here is builder-construction plus getters. */ public class RecordAndRedactionTests { @@ -37,106 +36,89 @@ public void testTokenGroupRedactions_defaultsAreNull() { Assert.assertNull(redaction.getRedaction()); } - // ── BulkTokenGroupRedactions ───────────────────────────────────────────── + // BulkTokenGroupRedactions tests removed: the class was deleted; bulk detokenize now reuses + // TokenGroupRedactions (covered above). - @Test - public void testBulkTokenGroupRedactions_gettersReturnBuilderValues() { - BulkTokenGroupRedactions redaction = BulkTokenGroupRedactions.builder() - .tokenGroupName("group2") - .redaction("REDACT") - .build(); - - Assert.assertEquals("group2", redaction.getTokenGroupName()); - Assert.assertEquals("REDACT", redaction.getRedaction()); - } + // ── InsertRequestRecord ───────────────────────────────────────────────────────── @Test - public void testBulkTokenGroupRedactions_defaultsAreNull() { - BulkTokenGroupRedactions redaction = BulkTokenGroupRedactions.builder().build(); - Assert.assertNull(redaction.getTokenGroupName()); - Assert.assertNull(redaction.getRedaction()); - } - - // ── InsertRecord ───────────────────────────────────────────────────────── - - @Test - public void testInsertRecord_gettersReturnBuilderValues() { + public void testInsertRequestRecord_gettersReturnBuilderValues() { Map data = new HashMap<>(); data.put("name", "John"); - List upsert = Arrays.asList("id"); + Map tokens = new HashMap<>(); + tokens.put("name", "token-value"); + UpsertOptions upsert = UpsertOptions.builder() + .uniqueColumns(Arrays.asList("id")) + .updateType("UPDATE") + .build(); - InsertRecord record = InsertRecord.builder() - .table("persons") + InsertRequestRecord record = InsertRequestRecord.builder() + .tableName("persons") .data(data) + .tokens(tokens) .upsert(upsert) - .upsertType(UpsertType.UPDATE) .build(); - Assert.assertEquals("persons", record.getTable()); + Assert.assertEquals("persons", record.getTableName()); Assert.assertEquals(data, record.getData()); + Assert.assertEquals(tokens, record.getTokens()); Assert.assertEquals(upsert, record.getUpsert()); - Assert.assertEquals(UpsertType.UPDATE, record.getUpsertType()); } @Test - public void testInsertRecord_defaultsAreNull() { - InsertRecord record = InsertRecord.builder().build(); - Assert.assertNull(record.getTable()); + public void testInsertRequestRecord_defaultsAreNull() { + InsertRequestRecord record = InsertRequestRecord.builder().build(); + Assert.assertNull(record.getTableName()); Assert.assertNull(record.getData()); + Assert.assertNull(record.getTokens()); Assert.assertNull(record.getUpsert()); - Assert.assertNull(record.getUpsertType()); } - // ── BulkInsertRecord ───────────────────────────────────────────────────── + // ── BulkInsertRequestRecord ────────────────────────────────────────────── @Test - public void testBulkInsertRecord_gettersReturnBuilderValues() { + public void testBulkInsertRequestRecord_gettersReturnBuilderValues() { Map data = new HashMap<>(); data.put("name", "Jane"); - List upsert = Arrays.asList("id"); + Map tokens = new HashMap<>(); + tokens.put("name", "token-1"); + UpsertOptions upsert = UpsertOptions.builder() + .updateType("REPLACE") + .uniqueColumns(Arrays.asList("id")) + .build(); - BulkInsertRecord record = BulkInsertRecord.builder() - .table("persons") + BulkInsertRequestRecord record = BulkInsertRequestRecord.builder() + .tableName("persons") .data(data) + .tokens(tokens) .upsert(upsert) - .upsertType(UpsertType.REPLACE) .build(); - Assert.assertEquals("persons", record.getTable()); + Assert.assertEquals("persons", record.getTableName()); Assert.assertEquals(data, record.getData()); + Assert.assertEquals(tokens, record.getTokens()); Assert.assertEquals(upsert, record.getUpsert()); - Assert.assertEquals(UpsertType.REPLACE, record.getUpsertType()); + Assert.assertEquals("REPLACE", record.getUpsert().getUpdateType()); + Assert.assertEquals(Arrays.asList("id"), record.getUpsert().getUniqueColumns()); } @Test - public void testBulkInsertRecord_defaultsAreNull() { - BulkInsertRecord record = BulkInsertRecord.builder().build(); - Assert.assertNull(record.getTable()); + public void testBulkInsertRequestRecord_defaultsAreNull() { + BulkInsertRequestRecord record = BulkInsertRequestRecord.builder().build(); + Assert.assertNull(record.getTableName()); Assert.assertNull(record.getData()); + Assert.assertNull(record.getTokens()); Assert.assertNull(record.getUpsert()); - Assert.assertNull(record.getUpsertType()); } - // ── TokenizeRecord ─────────────────────────────────────────────────────── - @Test - public void testTokenizeRecord_gettersReturnBuilderValues() { - List groups = Arrays.asList("group1", "group2"); - TokenizeRecord record = TokenizeRecord.builder() - .value("secret-value") - .tokenGroupNames(groups) - .build(); - - Assert.assertEquals("secret-value", record.getValue()); - Assert.assertEquals(groups, record.getTokenGroupNames()); + public void testBulkInsertRequestRecord_isAnInsertRequestRecord() { + InsertRequestRecord record = BulkInsertRequestRecord.builder().tableName("persons").build(); + Assert.assertTrue(record instanceof BulkInsertRequestRecord); + Assert.assertEquals("persons", record.getTableName()); } - @Test - public void testTokenizeRecord_defaultsAreNull() { - TokenizeRecord record = TokenizeRecord.builder().build(); - Assert.assertNull(record.getValue()); - Assert.assertNull(record.getTokenGroupNames()); - } + // Tests for TokenizeRecord were removed: the class no longer exists (bulk-only module). // ── BulkTokenizeRecord ─────────────────────────────────────────────────── diff --git a/flowvault/src/test/java/com/skyflow/vault/data/RequestResponseWrapperTests.java b/flowvault/src/test/java/com/skyflow/vault/data/RequestResponseWrapperTests.java index bdbbe6a8..9e00b238 100644 --- a/flowvault/src/test/java/com/skyflow/vault/data/RequestResponseWrapperTests.java +++ b/flowvault/src/test/java/com/skyflow/vault/data/RequestResponseWrapperTests.java @@ -1,6 +1,5 @@ package com.skyflow.vault.data; -import com.skyflow.enums.UpsertType; import org.junit.Assert; import org.junit.Test; @@ -12,192 +11,174 @@ import java.util.Map; /** - * Tests for the simple request/response wrapper classes: {@link DeleteTokensRequest}, - * {@link InsertRequest}, {@link InsertResponse}, {@link TokenizeRequest}, {@link TokenizeResponse}, - * {@link DetokenizeRequest}, {@link DetokenizeResponse}, {@link DetokenizeRecordResponse}, - * {@link DetokenizeData}, {@link BulkDeleteTokensRequest}, {@link BulkDetokenizeRequest}, - * {@link BulkTokenizeRequest}, {@link BulkInsertRequest} and {@link DeleteTokensResponse}. + * Tests for the simple request/response wrapper classes: {@link InsertRequest}, + * {@link InsertResponseRecord}, {@link DetokenizeRequest}, {@link DetokenizeResponseRecord}, + * {@link BulkDeleteTokensRequest}, {@link BulkDetokenizeRequest}, {@link BulkTokenizeRequest} + * and {@link BulkInsertRequest}. */ public class RequestResponseWrapperTests { - // ── DeleteTokensRequest ────────────────────────────────────────────────── - - @Test - public void testDeleteTokensRequest_getterReturnsBuilderValue() { - List tokens = Arrays.asList("tok-1", "tok-2"); - DeleteTokensRequest request = DeleteTokensRequest.builder().tokens(tokens).build(); - Assert.assertEquals(tokens, request.getTokens()); - } - - @Test - public void testDeleteTokensRequest_defaultIsNull() { - DeleteTokensRequest request = DeleteTokensRequest.builder().build(); - Assert.assertNull(request.getTokens()); - } + // Tests for DeleteTokensRequest were removed: the class no longer exists (bulk-only module). // ── InsertRequest ──────────────────────────────────────────────────────── @Test public void testInsertRequest_gettersReturnBuilderValues() { - ArrayList records = new ArrayList<>(Collections.singletonList( - InsertRecord.builder().table("persons").build())); - List upsert = Arrays.asList("id"); + ArrayList records = new ArrayList<>(Collections.singletonList( + InsertRequestRecord.builder().tableName("persons").build())); + UpsertOptions upsert = UpsertOptions.builder() + .uniqueColumns(Arrays.asList("id")) + .updateType("UPDATE") + .build(); InsertRequest request = InsertRequest.builder() - .table("persons") .upsert(upsert) - .upsertType(UpsertType.UPDATE) .records(records) .build(); - Assert.assertEquals("persons", request.getTable()); Assert.assertEquals(upsert, request.getUpsert()); - Assert.assertEquals(UpsertType.UPDATE, request.getUpsertType()); Assert.assertEquals(records, request.getRecords()); } + @Test + public void testInsertRequest_tableNameGetterReturnsBuilderValue() { + InsertRequest request = InsertRequest.builder().tableName("persons").build(); + Assert.assertEquals("persons", request.getTableName()); + } + @Test public void testInsertRequest_defaultsAreNull() { InsertRequest request = InsertRequest.builder().build(); - Assert.assertNull(request.getTable()); Assert.assertNull(request.getUpsert()); - Assert.assertNull(request.getUpsertType()); Assert.assertNull(request.getRecords()); + Assert.assertNull(request.getTableName()); } - // ── InsertResponse ─────────────────────────────────────────────────────── + // ── InsertRequestRecord ───────────────────────────────────────────────────────── @Test - public void testInsertResponse_gettersReturnConstructorValues() { - ArrayList> insertedFields = new ArrayList<>(); - HashMap field = new HashMap<>(); - field.put("skyflow_id", "id-1"); - insertedFields.add(field); - ArrayList> errors = new ArrayList<>(); - - InsertResponse response = new InsertResponse(insertedFields, errors); + public void testInsertRequestRecord_gettersReturnBuilderValues() { + Map data = new HashMap<>(); + data.put("name", "john"); + Map tokens = new HashMap<>(); + tokens.put("name", "tok-abc"); + UpsertOptions upsert = UpsertOptions.builder().uniqueColumns(Arrays.asList("id")).build(); + + InsertRequestRecord record = InsertRequestRecord.builder() + .tableName("persons") + .data(data) + .tokens(tokens) + .upsert(upsert) + .build(); - Assert.assertEquals(insertedFields, response.getInsertedFields()); - Assert.assertEquals(errors, response.getErrors()); + Assert.assertEquals("persons", record.getTableName()); + Assert.assertEquals(data, record.getData()); + Assert.assertEquals(tokens, record.getTokens()); + Assert.assertEquals(upsert, record.getUpsert()); } - // ── TokenizeRequest ────────────────────────────────────────────────────── + // ── UpsertOptions ──────────────────────────────────────────────────────── @Test - public void testTokenizeRequest_getterReturnsBuilderValue() { - ArrayList data = new ArrayList<>(Collections.singletonList( - TokenizeRecord.builder().value("v1").build())); - TokenizeRequest request = TokenizeRequest.builder().data(data).build(); - Assert.assertEquals(data, request.getData()); - } + public void testUpsertOptions_gettersReturnBuilderValues() { + List uniqueColumns = Arrays.asList("id", "email"); + UpsertOptions upsert = UpsertOptions.builder() + .updateType("REPLACE") + .uniqueColumns(uniqueColumns) + .build(); - @Test - public void testTokenizeRequest_defaultIsNull() { - TokenizeRequest request = TokenizeRequest.builder().build(); - Assert.assertNull(request.getData()); + Assert.assertEquals("REPLACE", upsert.getUpdateType()); + Assert.assertEquals(uniqueColumns, upsert.getUniqueColumns()); } - // ── TokenizeResponse ───────────────────────────────────────────────────── + // Tests for InsertResponse were removed: the class no longer exists (bulk-only module). + + // ── InsertResponseRecord ───────────────────────────────────────────────── @Test - public void testTokenizeResponse_defaultConstructorInitializesEmptyErrors() { - TokenizeResponse response = new TokenizeResponse(); - Assert.assertNotNull(response.getErrors()); - Assert.assertTrue(response.getErrors().isEmpty()); - Assert.assertNull(response.getTokenizedData()); + public void testInsertResponseRecord_gettersReturnConstructorValues() { + Map fields = new HashMap<>(); + fields.put("name", "tok-abc"); + Map hashedData = new HashMap<>(); + hashedData.put("name", "hashed-value"); + + InsertResponseRecord record = new InsertResponseRecord( + "persons", "id-1", fields, hashedData, 200, null); + + Assert.assertEquals("persons", record.getTableName()); + Assert.assertEquals("id-1", record.getSkyflowId()); + Assert.assertEquals(fields, record.getFields()); + Assert.assertEquals(hashedData, record.getHashedData()); + Assert.assertEquals(200, record.getHttpCode()); + Assert.assertNull(record.getError()); } @Test - public void testTokenizeResponse_errorsConstructorAndTokenizedDataSetter() { - ArrayList> errors = new ArrayList<>(); - HashMap error = new HashMap<>(); - error.put("error", "failed"); - errors.add(error); - - TokenizeResponse response = new TokenizeResponse(errors); - Assert.assertEquals(errors, response.getErrors()); - - List tokenizedData = Collections.singletonList(new TokenizeData("v1", 0)); - response.setTokenizedData(tokenizedData); - Assert.assertEquals(tokenizedData, response.getTokenizedData()); + public void testInsertResponseRecord_errorCase() { + InsertResponseRecord record = new InsertResponseRecord( + "persons", null, null, null, 400, "insert failed"); + + Assert.assertEquals("persons", record.getTableName()); + Assert.assertNull(record.getSkyflowId()); + Assert.assertEquals("insert failed", record.getError()); + Assert.assertEquals(400, record.getHttpCode()); } + // Tests for TokenizeRequest / TokenizeResponse were removed: those classes no longer exist (bulk-only module). + // ── DetokenizeRequest ──────────────────────────────────────────────────── @Test public void testDetokenizeRequest_gettersReturnBuilderValues() { - ArrayList detokenizeData = new ArrayList<>(Collections.singletonList( - new DetokenizeData("tok-1"))); + List tokens = Collections.singletonList("tok-1"); List redactions = Collections.singletonList( TokenGroupRedactions.builder().tokenGroupName("group1").redaction("MASK").build()); DetokenizeRequest request = DetokenizeRequest.builder() - .detokenizeData(detokenizeData) + .tokens(tokens) .tokenGroupRedactions(redactions) .build(); - Assert.assertEquals(detokenizeData, request.getDetokenizeData()); + Assert.assertEquals(tokens, request.getTokens()); Assert.assertEquals(redactions, request.getTokenGroupRedactions()); } @Test public void testDetokenizeRequest_defaultsAreNull() { DetokenizeRequest request = DetokenizeRequest.builder().build(); - Assert.assertNull(request.getDetokenizeData()); + Assert.assertNull(request.getTokens()); Assert.assertNull(request.getTokenGroupRedactions()); } - // ── DetokenizeResponse ─────────────────────────────────────────────────── - - @Test - public void testDetokenizeResponse_gettersReturnConstructorValues() { - ArrayList detokenizedFields = new ArrayList<>(Collections.singletonList( - new DetokenizeRecordResponse("tok-1", "value", null, "group1", null))); - ArrayList errors = new ArrayList<>(); - - DetokenizeResponse response = new DetokenizeResponse(detokenizedFields, errors); + // Tests for DetokenizeResponse were removed: the class no longer exists (bulk-only module). - Assert.assertEquals(detokenizedFields, response.getDetokenizedFields()); - Assert.assertEquals(errors, response.getErrors()); - Assert.assertNotNull(response.toString()); - } - - // ── DetokenizeRecordResponse ───────────────────────────────────────────── + // ── DetokenizeResponseRecord ───────────────────────────────────────────── @Test - public void testDetokenizeRecordResponse_gettersReturnConstructorValues() { + public void testDetokenizeResponseRecord_gettersReturnConstructorValues() { Map metadata = new HashMap<>(); metadata.put("key", "value"); - DetokenizeRecordResponse response = new DetokenizeRecordResponse( - "tok-1", "plain-value", null, "group1", metadata); + DetokenizeResponseRecord response = new DetokenizeResponseRecord( + "tok-1", "secret-value", "group1", metadata, 200, null); Assert.assertEquals("tok-1", response.getToken()); - Assert.assertEquals("plain-value", response.getValue()); Assert.assertNull(response.getError()); Assert.assertEquals("group1", response.getTokenGroupName()); Assert.assertEquals(metadata, response.getMetadata()); - Assert.assertNotNull(response.toString()); + Assert.assertEquals(200, response.getHttpCode()); } @Test - public void testDetokenizeRecordResponse_errorCase() { - DetokenizeRecordResponse response = new DetokenizeRecordResponse( - "tok-2", null, "Token not found", null, null); + public void testDetokenizeResponseRecord_errorCase() { + DetokenizeResponseRecord response = new DetokenizeResponseRecord( + "tok-2", null, null, null, 404, "Token not found"); Assert.assertEquals("tok-2", response.getToken()); Assert.assertEquals("Token not found", response.getError()); - Assert.assertNull(response.getValue()); Assert.assertNull(response.getTokenGroupName()); Assert.assertNull(response.getMetadata()); - } - - // ── DetokenizeData ─────────────────────────────────────────────────────── - - @Test - public void testDetokenizeData_getterReturnsConstructorValue() { - DetokenizeData data = new DetokenizeData("tok-1"); - Assert.assertEquals("tok-1", data.getToken()); + Assert.assertEquals(404, response.getHttpCode()); } // ── BulkDeleteTokensRequest ────────────────────────────────────────────── @@ -220,8 +201,8 @@ public void testBulkDeleteTokensRequest_defaultIsNull() { @Test public void testBulkDetokenizeRequest_gettersReturnBuilderValues() { List tokens = Arrays.asList("tok-1", "tok-2"); - List redactions = Collections.singletonList( - BulkTokenGroupRedactions.builder().tokenGroupName("group1").redaction("MASK").build()); + List redactions = Collections.singletonList( + TokenGroupRedactions.builder().tokenGroupName("group1").redaction("MASK").build()); BulkDetokenizeRequest request = BulkDetokenizeRequest.builder() .tokens(tokens) @@ -239,6 +220,16 @@ public void testBulkDetokenizeRequest_defaultsAreNull() { Assert.assertNull(request.getTokenGroupRedactions()); } + @Test + public void testBulkDetokenizeRequest_isADetokenizeRequest() { + // Bulk detokenize now shares the unary request contract; all state is inherited. + BulkDetokenizeRequest request = BulkDetokenizeRequest.builder() + .tokens(Collections.singletonList("tok-1")) + .build(); + DetokenizeRequest asUnary = request; + Assert.assertEquals(Collections.singletonList("tok-1"), asUnary.getTokens()); + } + // ── BulkTokenizeRequest ────────────────────────────────────────────────── @Test @@ -259,45 +250,41 @@ public void testBulkTokenizeRequest_defaultIsNull() { @Test public void testBulkInsertRequest_gettersReturnBuilderValues() { - ArrayList records = new ArrayList<>(Collections.singletonList( - BulkInsertRecord.builder().table("persons").build())); - List upsert = Arrays.asList("id"); + List records = new ArrayList<>(Collections.singletonList( + BulkInsertRequestRecord.builder().tableName("persons").build())); + UpsertOptions upsert = UpsertOptions.builder() + .updateType("REPLACE") + .uniqueColumns(Arrays.asList("id")) + .build(); BulkInsertRequest request = BulkInsertRequest.builder() - .table("persons") + .tableName("persons") .upsert(upsert) - .upsertType(UpsertType.REPLACE) .records(records) .build(); - Assert.assertEquals("persons", request.getTable()); + Assert.assertEquals("persons", request.getTableName()); Assert.assertEquals(upsert, request.getUpsert()); - Assert.assertEquals(UpsertType.REPLACE, request.getUpsertType()); + Assert.assertEquals("REPLACE", request.getUpsert().getUpdateType()); + Assert.assertEquals(Arrays.asList("id"), request.getUpsert().getUniqueColumns()); Assert.assertEquals(records, request.getRecords()); } @Test public void testBulkInsertRequest_defaultsAreNull() { BulkInsertRequest request = BulkInsertRequest.builder().build(); - Assert.assertNull(request.getTable()); + Assert.assertNull(request.getTableName()); Assert.assertNull(request.getUpsert()); - Assert.assertNull(request.getUpsertType()); Assert.assertNull(request.getRecords()); } - // ── DeleteTokensResponse ───────────────────────────────────────────────── - @Test - public void testDeleteTokensResponse_gettersReturnConstructorValues() { - List tokens = Arrays.asList("tok-1", "tok-2"); - ArrayList> errors = new ArrayList<>(); - HashMap error = new HashMap<>(); - error.put("error", "failed"); - errors.add(error); + public void testBulkInsertRequest_isAnInsertRequest() { + InsertRequest request = BulkInsertRequest.builder().tableName("persons").build(); + Assert.assertTrue(request instanceof BulkInsertRequest); + Assert.assertEquals("persons", request.getTableName()); + } - DeleteTokensResponse response = new DeleteTokensResponse(tokens, errors); + // Tests for DeleteTokensResponse were removed: the class no longer exists (bulk-only module). - Assert.assertEquals(tokens, response.getTokens()); - Assert.assertEquals(errors, response.getErrors()); - } } diff --git a/flowvault/src/test/java/com/skyflow/vault/data/ResponseComponentTests.java b/flowvault/src/test/java/com/skyflow/vault/data/ResponseComponentTests.java index 22e38197..f8e30a93 100644 --- a/flowvault/src/test/java/com/skyflow/vault/data/ResponseComponentTests.java +++ b/flowvault/src/test/java/com/skyflow/vault/data/ResponseComponentTests.java @@ -3,108 +3,92 @@ import org.junit.Assert; import org.junit.Test; -import java.util.Collections; import java.util.HashMap; import java.util.LinkedHashMap; -import java.util.List; import java.util.Map; /** * Tests for the response/success/summary building-block classes that carry real - * constructor logic or toString() serialization: {@link Success}, {@link Summary}, - * {@link Token}, {@link TokenizeData}, {@link TokenizeSuccess}, {@link TokenizeSummary}, + * constructor logic or toString() serialization: {@link BulkInsertResponseRecord}, + * {@link BulkSummary}, {@link TokenizeSuccess}, {@link TokenizeSummary}, * {@link DeleteTokensSuccess}, {@link DeleteTokensSummary}, {@link DetokenizeSummary}, - * {@link ErrorRecord} and {@link DetokenizeResponseObject}. + * {@link ErrorRecord} and {@link BulkDetokenizeResponseRecord}. */ public class ResponseComponentTests { - // ── Success ────────────────────────────────────────────────────────────── + // Tests for Success, Summary and Token were removed: the bulk insert response contract + // replaced those classes with BulkInsertResponseRecord / BulkSummary, covered below. + + // ── BulkInsertResponseRecord ───────────────────────────────────────────── + + @Test + public void testBulkInsertResponseRecord_gettersReturnConstructorValues() { + Map fields = new HashMap<>(); + fields.put("name", "tok-1"); + Map hashedData = new HashMap<>(); + hashedData.put("name", "hashed-1"); + + BulkInsertResponseRecord record = new BulkInsertResponseRecord( + 2, "persons", "skyflow-id-1", fields, hashedData, 200, null, null); + + Assert.assertEquals(2, record.getIndex()); + Assert.assertEquals("persons", record.getTableName()); + Assert.assertEquals("skyflow-id-1", record.getSkyflowId()); + Assert.assertEquals(fields, record.getFields()); + Assert.assertEquals(hashedData, record.getHashedData()); + Assert.assertEquals(200, record.getHttpCode()); + Assert.assertNull(record.getError()); + } @Test - public void testSuccess_gettersReturnConstructorValues() { - Map> tokens = new HashMap<>(); - tokens.put("group1", Collections.singletonList(new Token("tok-1", "group1"))); - Map data = new HashMap<>(); - data.put("name", "John"); - - Success success = new Success(2, "skyflow-id-1", tokens, data, "persons"); - - Assert.assertEquals(2, success.getIndex()); - Assert.assertEquals("skyflow-id-1", success.getSkyflowId()); - Assert.assertEquals(tokens, success.getTokens()); - Assert.assertEquals(data, success.getData()); - Assert.assertEquals("persons", success.getTable()); + public void testBulkInsertResponseRecord_errorCase() { + BulkInsertResponseRecord record = new BulkInsertResponseRecord( + 3, null, null, null, null, 500, "Internal Server Error", null); + + Assert.assertEquals(3, record.getIndex()); + Assert.assertEquals(500, record.getHttpCode()); + Assert.assertEquals("Internal Server Error", record.getError()); + Assert.assertNull(record.getTableName()); + Assert.assertNull(record.getSkyflowId()); + Assert.assertNull(record.getFields()); + Assert.assertNull(record.getHashedData()); } @Test - public void testSuccess_toStringContainsSkyflowId() { - Success success = new Success(0, "skyflow-id-2", new HashMap<>(), new HashMap<>(), "persons"); - String json = success.toString(); + public void testBulkInsertResponseRecord_toStringSerializesNulls() { + BulkInsertResponseRecord record = new BulkInsertResponseRecord( + 0, "persons", "skyflow-id-2", null, null, 200, null, null); + String json = record.toString(); Assert.assertNotNull(json); Assert.assertTrue(json.contains("skyflow-id-2")); + Assert.assertTrue(json.contains("\"index\":0")); + Assert.assertTrue(json.contains("\"fields\":null")); } - // ── Summary ────────────────────────────────────────────────────────────── + // ── BulkSummary ────────────────────────────────────────────────────────── @Test - public void testSummary_noArgConstructorDefaultsToZero() { - Summary summary = new Summary(); + public void testBulkSummary_noArgConstructorDefaultsToZero() { + BulkSummary summary = new BulkSummary(); Assert.assertEquals(0, summary.getTotalRecords()); Assert.assertEquals(0, summary.getTotalInserted()); Assert.assertEquals(0, summary.getTotalFailed()); } @Test - public void testSummary_allArgConstructor() { - Summary summary = new Summary(10, 8, 2); + public void testBulkSummary_allArgConstructor() { + BulkSummary summary = new BulkSummary(10, 8, 2); Assert.assertEquals(10, summary.getTotalRecords()); Assert.assertEquals(8, summary.getTotalInserted()); Assert.assertEquals(2, summary.getTotalFailed()); } @Test - public void testSummary_toStringNotNull() { - Assert.assertNotNull(new Summary(1, 1, 0).toString()); - } - - // ── Token ──────────────────────────────────────────────────────────────── - - @Test - public void testToken_gettersReturnConstructorValues() { - Token token = new Token("tok-value", "group-name"); - Assert.assertEquals("tok-value", token.getToken()); - Assert.assertEquals("group-name", token.getTokenGroupName()); + public void testBulkSummary_toStringNotNull() { + Assert.assertNotNull(new BulkSummary(1, 1, 0).toString()); } - // ── TokenizeData ───────────────────────────────────────────────────────── - // Note: TokenizeData has an `index` field set in the constructor but exposes - // no getter for it - only getValue()/getTokens() are accessible. - - @Test - public void testTokenizeData_getValueAndEmptyTokensOnConstruction() { - TokenizeData data = new TokenizeData("plain-value", 0); - Assert.assertEquals("plain-value", data.getValue()); - Assert.assertNotNull(data.getTokens()); - Assert.assertTrue(data.getTokens().isEmpty()); - } - - @Test - public void testTokenizeData_addTokenPopulatesTokensMap() { - TokenizeData data = new TokenizeData("plain-value", 1); - data.addToken("group1", "tok-1"); - data.addToken("group2", "tok-2"); - - Map expected = new LinkedHashMap<>(); - expected.put("group1", "tok-1"); - expected.put("group2", "tok-2"); - Assert.assertEquals(expected, data.getTokens()); - } - - @Test - public void testTokenizeData_toStringNotNull() { - TokenizeData data = new TokenizeData("value", 0); - Assert.assertNotNull(data.toString()); - } + // Tests for TokenizeData were removed: the class no longer exists (bulk-only module). // ── TokenizeSuccess ────────────────────────────────────────────────────── @@ -239,38 +223,55 @@ public void testErrorRecord_toStringNotNull() { Assert.assertNotNull(new ErrorRecord(0, "err", 400).toString()); } - // ── DetokenizeResponseObject ───────────────────────────────────────────── + // DetokenizeResponseObject tests removed: the class was deleted; the bulk detokenize response + // contract replaced it with BulkDetokenizeResponseRecord, covered below. + + // ── BulkDetokenizeResponseRecord ───────────────────────────────────────── @Test - public void testDetokenizeResponseObject_gettersReturnConstructorValues() { + public void testBulkDetokenizeResponseRecord_gettersReturnConstructorValues() { Map metadata = new HashMap<>(); metadata.put("key", "value"); - DetokenizeResponseObject obj = new DetokenizeResponseObject( - 4, "tok-1", "plain-value", "group1", null, metadata); + BulkDetokenizeResponseRecord record = new BulkDetokenizeResponseRecord( + 4, "tok-1", "secret-value", "group1", metadata, 200, null, null); - Assert.assertEquals(4, obj.getIndex()); - Assert.assertEquals("tok-1", obj.getToken()); - Assert.assertEquals("plain-value", obj.getValue()); - Assert.assertEquals("group1", obj.getTokenGroupName()); - Assert.assertNull(obj.getError()); - Assert.assertEquals(metadata, obj.getMetadata()); + Assert.assertEquals(4, record.getIndex()); + Assert.assertEquals("tok-1", record.getToken()); + Assert.assertEquals("group1", record.getTokenGroupName()); + Assert.assertEquals(metadata, record.getMetadata()); + Assert.assertEquals(200, record.getHttpCode()); + Assert.assertNull(record.getError()); } @Test - public void testDetokenizeResponseObject_errorCase() { - DetokenizeResponseObject obj = new DetokenizeResponseObject( - 0, "tok-2", null, null, "Token not found", null); - - Assert.assertEquals("Token not found", obj.getError()); - Assert.assertNull(obj.getValue()); - Assert.assertNull(obj.getTokenGroupName()); - Assert.assertNull(obj.getMetadata()); + public void testBulkDetokenizeResponseRecord_errorCase() { + BulkDetokenizeResponseRecord record = new BulkDetokenizeResponseRecord( + 0, "tok-2", null, null, null, 404, "Token not found", null); + + Assert.assertEquals(0, record.getIndex()); + Assert.assertEquals("Token not found", record.getError()); + Assert.assertEquals(404, record.getHttpCode()); + Assert.assertNull(record.getTokenGroupName()); + Assert.assertNull(record.getMetadata()); } @Test - public void testDetokenizeResponseObject_toStringNotNull() { - DetokenizeResponseObject obj = new DetokenizeResponseObject(0, "tok", "v", "g", null, null); - Assert.assertNotNull(obj.toString()); + public void testBulkDetokenizeResponseRecord_isADetokenizeResponseRecord() { + BulkDetokenizeResponseRecord record = new BulkDetokenizeResponseRecord( + 1, "tok", "plain", "group", null, 200, null, null); + Assert.assertTrue(record instanceof DetokenizeResponseRecord); + } + + @Test + public void testBulkDetokenizeResponseRecord_toStringSerializesNulls() { + BulkDetokenizeResponseRecord record = new BulkDetokenizeResponseRecord( + 2, "tok", null, null, null, 200, null, null); + String json = record.toString(); + + Assert.assertNotNull(json); + Assert.assertTrue(json.contains("\"index\":2")); + Assert.assertTrue(json.contains("\"token\":\"tok\"")); + Assert.assertTrue(json.contains("\"error\":null")); } } diff --git a/v2/src/main/java/com/skyflow/vault/data/InsertRequest.java b/v2/src/main/java/com/skyflow/vault/data/InsertRequest.java index f2127a77..a7066236 100644 --- a/v2/src/main/java/com/skyflow/vault/data/InsertRequest.java +++ b/v2/src/main/java/com/skyflow/vault/data/InsertRequest.java @@ -9,7 +9,6 @@ public class InsertRequest extends BaseInsertRequest{ private final InsertRequestBuilder builder; private InsertRequest(InsertRequestBuilder builder) { - super(builder); this.builder = builder; } @@ -50,7 +49,7 @@ public TokenMode getTokenMode() { return this.builder.tokenMode; } - public static final class InsertRequestBuilder extends BaseInsertRequestBuilder{ + public static final class InsertRequestBuilder { private String table; private ArrayList> values; private ArrayList> tokens; diff --git a/v2/src/main/java/com/skyflow/vault/data/InsertResponse.java b/v2/src/main/java/com/skyflow/vault/data/InsertResponse.java index bc09b0c2..29954b82 100644 --- a/v2/src/main/java/com/skyflow/vault/data/InsertResponse.java +++ b/v2/src/main/java/com/skyflow/vault/data/InsertResponse.java @@ -6,7 +6,25 @@ import java.util.HashMap; public class InsertResponse extends BaseInsertResponse { + private final ArrayList> insertedFields; + private final ArrayList> errors; + public InsertResponse(ArrayList> insertedFields, ArrayList> errors) { - super(insertedFields, errors); + this.insertedFields = insertedFields; + this.errors = errors; + } + + public ArrayList> getInsertedFields() { + return insertedFields; + } + + public ArrayList> getErrors() { + return errors; + } + + @Override + public String toString() { + Gson gson = new Gson().newBuilder().serializeNulls().create(); + return gson.toJson(this); } } diff --git a/v2/src/main/java/com/skyflow/vault/tokens/DetokenizeData.java b/v2/src/main/java/com/skyflow/vault/tokens/DetokenizeData.java index 0353aa6c..0a8d7ed5 100644 --- a/v2/src/main/java/com/skyflow/vault/tokens/DetokenizeData.java +++ b/v2/src/main/java/com/skyflow/vault/tokens/DetokenizeData.java @@ -4,20 +4,21 @@ import com.skyflow.vault.data.BaseDetokenizeData; public class DetokenizeData extends BaseDetokenizeData { + private final String token; private final RedactionType redactionType; public DetokenizeData(String token) { - super(token); + this.token = token; this.redactionType = RedactionType.DEFAULT; } public DetokenizeData(String token, RedactionType redactionType) { - super(token); + this.token = token; this.redactionType = redactionType == null ? RedactionType.DEFAULT : redactionType; } public String getToken() { - return super.getToken(); + return this.token; } public RedactionType getRedactionType() { diff --git a/v2/src/main/java/com/skyflow/vault/tokens/DetokenizeRequest.java b/v2/src/main/java/com/skyflow/vault/tokens/DetokenizeRequest.java index 481c0c16..83c7e017 100644 --- a/v2/src/main/java/com/skyflow/vault/tokens/DetokenizeRequest.java +++ b/v2/src/main/java/com/skyflow/vault/tokens/DetokenizeRequest.java @@ -1,11 +1,12 @@ package com.skyflow.vault.tokens; import com.skyflow.logs.InfoLogs; +import com.skyflow.vault.data.BaseDetokenizeRequest; import com.skyflow.utils.logger.LogUtil; import java.util.ArrayList; -public class DetokenizeRequest { +public class DetokenizeRequest extends BaseDetokenizeRequest { private final DetokenizeRequestBuilder builder; private DetokenizeRequest(DetokenizeRequestBuilder builder) { diff --git a/v2/src/main/java/com/skyflow/vault/tokens/DetokenizeResponse.java b/v2/src/main/java/com/skyflow/vault/tokens/DetokenizeResponse.java index bd1d7cde..e7f69c34 100644 --- a/v2/src/main/java/com/skyflow/vault/tokens/DetokenizeResponse.java +++ b/v2/src/main/java/com/skyflow/vault/tokens/DetokenizeResponse.java @@ -4,10 +4,11 @@ import com.google.gson.FieldAttributes; import com.google.gson.Gson; import com.google.gson.GsonBuilder; +import com.skyflow.vault.data.BaseDetokenizeResponse; import java.util.ArrayList; -public class DetokenizeResponse { +public class DetokenizeResponse extends BaseDetokenizeResponse { private final ArrayList detokenizedFields; private final ArrayList errors; diff --git a/v2/src/test/java/com/skyflow/RequestFidelityTests.java b/v2/src/test/java/com/skyflow/RequestFidelityTests.java new file mode 100644 index 00000000..0a63d89e --- /dev/null +++ b/v2/src/test/java/com/skyflow/RequestFidelityTests.java @@ -0,0 +1,1406 @@ +package com.skyflow; + +import com.google.gson.JsonArray; +import com.google.gson.JsonNull; +import com.google.gson.JsonObject; +import com.skyflow.config.ConnectionConfig; +import com.skyflow.config.Credentials; +import com.skyflow.config.VaultConfig; +import com.skyflow.enums.Env; +import com.skyflow.enums.RedactionType; +import com.skyflow.enums.RequestMethod; +import com.skyflow.enums.TokenMode; +import com.skyflow.errors.SkyflowException; +import com.skyflow.generated.rest.ApiClient; +import com.skyflow.generated.rest.resources.query.QueryClient; +import com.skyflow.generated.rest.resources.query.requests.QueryServiceExecuteQueryBody; +import com.skyflow.generated.rest.resources.records.RawRecordsClient; +import com.skyflow.generated.rest.resources.records.RecordsClient; +import com.skyflow.generated.rest.resources.records.requests.RecordServiceBatchOperationBody; +import com.skyflow.generated.rest.resources.records.requests.RecordServiceBulkDeleteRecordBody; +import com.skyflow.generated.rest.resources.records.requests.RecordServiceBulkGetRecordRequest; +import com.skyflow.generated.rest.resources.records.requests.RecordServiceInsertRecordBody; +import com.skyflow.generated.rest.resources.records.requests.RecordServiceUpdateRecordBody; +import com.skyflow.generated.rest.resources.records.requests.UploadFileV2Request; +import com.skyflow.generated.rest.resources.records.types.RecordServiceBulkGetRecordRequestOrderBy; +import com.skyflow.generated.rest.resources.records.types.RecordServiceBulkGetRecordRequestRedaction; +import com.skyflow.generated.rest.resources.tokens.requests.V1DetokenizePayload; +import com.skyflow.generated.rest.resources.tokens.requests.V1TokenizePayload; +import com.skyflow.generated.rest.types.BatchRecordMethod; +import com.skyflow.generated.rest.types.RedactionEnumRedaction; +import com.skyflow.generated.rest.types.UploadFileV2Response; +import com.skyflow.generated.rest.types.V1BatchOperationResponse; +import com.skyflow.generated.rest.types.V1BatchRecord; +import com.skyflow.generated.rest.types.V1BulkDeleteRecordResponse; +import com.skyflow.generated.rest.types.V1BulkGetRecordResponse; +import com.skyflow.generated.rest.types.V1Byot; +import com.skyflow.generated.rest.types.V1DetokenizeRecordRequest; +import com.skyflow.generated.rest.types.V1FieldRecords; +import com.skyflow.generated.rest.types.V1GetQueryResponse; +import com.skyflow.generated.rest.types.V1InsertRecordResponse; +import com.skyflow.generated.rest.types.V1TokenizeRecordRequest; +import com.skyflow.generated.rest.types.V1TokenizeResponse; +import com.skyflow.generated.rest.types.V1UpdateRecordResponse; +import com.skyflow.utils.HttpUtility; +import com.skyflow.utils.Utils; +import com.skyflow.vault.connection.InvokeConnectionRequest; +import com.skyflow.vault.controller.ConnectionController; +import com.skyflow.vault.controller.VaultController; +import com.skyflow.vault.data.DeleteRequest; +import com.skyflow.vault.data.FileUploadRequest; +import com.skyflow.vault.data.GetRequest; +import com.skyflow.vault.data.InsertRequest; +import com.skyflow.vault.data.QueryRequest; +import com.skyflow.vault.data.UpdateRequest; +import com.skyflow.vault.tokens.ColumnValue; +import com.skyflow.vault.tokens.DetokenizeData; +import com.skyflow.vault.tokens.DetokenizeRequest; +import com.skyflow.vault.tokens.TokenizeRequest; +import okhttp3.Protocol; +import okhttp3.Request; +import okhttp3.Response; +import org.junit.Assert; +import org.junit.Test; +import org.mockito.ArgumentCaptor; +import org.mockito.Mockito; + +import java.io.File; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.net.ProtocolException; +import java.nio.file.Files; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Base64; +import java.util.Collections; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.when; + +/** + * Request-fidelity tests for the v2 SDK. + * + *

Every test here builds a request the way an SDK user would, runs it through the real + * production mapping code ({@link VaultClient}'s request-body builders, {@link VaultController}'s + * inline builders, {@link Utils} / {@link HttpUtility} for connections) and asserts the exact value + * that lands on the outgoing generated REST request object. + * + *

Tests suffixed {@code _knownGap} pin behaviour that is a confirmed defect. They assert the + * CURRENT behaviour on purpose so that any future change to production code is caught here. + */ +public class RequestFidelityTests { + + private static final String VAULT_ID = "vault123"; + private static final String CLUSTER_ID = "cluster123"; + private static final String API_KEY = "sky-ab123-abcd1234cdef1234abcd4321cdef4321"; // gitleaks:allow + private static final String CONNECTION_URL = "https://conn.example.com/api/{resource}/details"; + + // Non-trivial values reused across tests. + private static final String NON_ASCII_NAME = "日本語 テスト"; + private static final String NON_ASCII_CITY = "北京市 朝阳区"; + private static final String MULTILINE_NOTE = "line one\nline two with spaces"; + + // ------------------------------------------------------------------ + // harness + // ------------------------------------------------------------------ + + private static VaultConfig testVaultConfig() { + VaultConfig config = new VaultConfig(); + config.setVaultId(VAULT_ID); + config.setClusterId(CLUSTER_ID); + config.setEnv(Env.DEV); + return config; + } + + private static Credentials apiKeyCredentials() { + Credentials credentials = new Credentials(); + credentials.setApiKey(API_KEY); + return credentials; + } + + /** + * VaultClient's request-body builders are {@code protected} and this test lives in the same + * {@code com.skyflow} package, so they can be exercised directly with no mocking at all. + */ + private static VaultClient newVaultClient() { + return new VaultClient(testVaultConfig(), apiKeyCredentials()); + } + + private static VaultController newControllerWithMockApi(ApiClient mockApiClient) throws Exception { + VaultController controller = new VaultController(testVaultConfig(), apiKeyCredentials()); + Field field = VaultClient.class.getDeclaredField("apiClient"); + field.setAccessible(true); + field.set(controller, mockApiClient); + return controller; + } + + private static Response buildOkHttpResponse() { + return new Response.Builder() + .request(new Request.Builder().url("https://dummy.example.com").build()) + .protocol(Protocol.HTTP_1_1) + .code(200) + .message("OK") + .header(com.skyflow.utils.Constants.REQUEST_ID_HEADER_KEY, "req-fidelity-1") + .build(); + } + + private static HashMap richRow() { + HashMap nested = new HashMap<>(); + nested.put("city", NON_ASCII_CITY); + nested.put("zip", "100000"); + + HashMap row = new HashMap<>(); + row.put("name", NON_ASCII_NAME); + row.put("notes", MULTILINE_NOTE); + row.put("age", 42); + row.put("balance", 3.14); + row.put("active", true); + row.put("address", nested); + return row; + } + + private static ArrayList> rows(HashMap... maps) { + return new ArrayList<>(Arrays.asList(maps)); + } + + private static ArrayList list(String... items) { + return new ArrayList<>(Arrays.asList(items)); + } + + // ================================================================== + // insert — bulk branch (continueOnError == false) + // ================================================================== + + @Test + public void testInsertBulk_everyBuilderValueReachesGeneratedBody() { + VaultClient client = newVaultClient(); + + HashMap row1 = richRow(); + HashMap row2 = new HashMap<>(); + row2.put("name", "second record"); + + InsertRequest request = InsertRequest.builder() + .table("cards") + .values(rows(row1, row2)) + .returnTokens(true) + .upsert("email") + .tokenMode(TokenMode.ENABLE_STRICT) + .continueOnError(false) + .build(); + + RecordServiceInsertRecordBody body = client.getBulkInsertRequestBody(request); + + Assert.assertTrue("tokenization must carry returnTokens", body.getTokenization().isPresent()); + Assert.assertTrue(body.getTokenization().get()); + Assert.assertEquals("email", body.getUpsert().get()); + Assert.assertEquals(V1Byot.ENABLE_STRICT, body.getByot().get()); + + List records = body.getRecords().get(); + Assert.assertEquals(2, records.size()); + + Map fields0 = records.get(0).getFields().get(); + Assert.assertEquals(NON_ASCII_NAME, fields0.get("name")); + Assert.assertEquals(MULTILINE_NOTE, fields0.get("notes")); + Assert.assertEquals(42, fields0.get("age")); + Assert.assertEquals(3.14, (Double) fields0.get("balance"), 0.0); + Assert.assertEquals(Boolean.TRUE, fields0.get("active")); + @SuppressWarnings("unchecked") + Map address = (Map) fields0.get("address"); + Assert.assertEquals(NON_ASCII_CITY, address.get("city")); + Assert.assertEquals("100000", address.get("zip")); + Assert.assertEquals("whole values map must be carried verbatim", row1, fields0); + + Assert.assertEquals("second record", records.get(1).getFields().get().get("name")); + Assert.assertFalse("no tokens supplied -> tokens absent", records.get(0).getTokens().isPresent()); + } + + @Test + public void testInsertBulk_homogeneousReachesGeneratedBody() { + VaultClient client = newVaultClient(); + InsertRequest request = InsertRequest.builder() + .table("cards") + .values(rows(richRow())) + .homogeneous(true) + .build(); + + RecordServiceInsertRecordBody body = client.getBulkInsertRequestBody(request); + Assert.assertTrue(body.getHomogeneous().isPresent()); + Assert.assertTrue(body.getHomogeneous().get()); + } + + @Test + public void testInsertBulk_defaultsReachGeneratedBody() { + VaultClient client = newVaultClient(); + InsertRequest request = InsertRequest.builder() + .table("cards") + .values(rows(richRow())) + .build(); + + RecordServiceInsertRecordBody body = client.getBulkInsertRequestBody(request); + Assert.assertFalse("returnTokens defaults to false", body.getTokenization().get()); + Assert.assertEquals("tokenMode defaults to DISABLE -> byot DISABLE", V1Byot.DISABLE, body.getByot().get()); + Assert.assertFalse("upsert not set -> absent", body.getUpsert().isPresent()); + Assert.assertFalse("homogeneous not set -> absent", body.getHomogeneous().isPresent()); + } + + @Test + public void testInsertBulk_tokensPairPositionallyWithValues() { + VaultClient client = newVaultClient(); + + HashMap value0 = new HashMap<>(); + value0.put("card_number", "4111111111111111"); + HashMap value1 = new HashMap<>(); + value1.put("card_number", "5111111111111111"); + + HashMap token0 = new HashMap<>(); + token0.put("card_number", "token-for-row-0"); + HashMap token1 = new HashMap<>(); + token1.put("card_number", "token-for-row-1"); + + InsertRequest request = InsertRequest.builder() + .table("cards") + .values(rows(value0, value1)) + .tokens(rows(token0, token1)) + .tokenMode(TokenMode.ENABLE_STRICT) + .build(); + + List records = client.getBulkInsertRequestBody(request).getRecords().get(); + + Assert.assertEquals("4111111111111111", records.get(0).getFields().get().get("card_number")); + Assert.assertEquals("token-for-row-0", records.get(0).getTokens().get().get("card_number")); + Assert.assertEquals("5111111111111111", records.get(1).getFields().get().get("card_number")); + Assert.assertEquals("token-for-row-1", records.get(1).getTokens().get().get("card_number")); + } + + @Test + public void testInsertBulk_recordOrderIsPreserved() { + VaultClient client = newVaultClient(); + ArrayList> values = new ArrayList<>(); + for (int i = 0; i < 5; i++) { + HashMap row = new HashMap<>(); + row.put("position", i); + values.add(row); + } + + InsertRequest request = InsertRequest.builder().table("cards").values(values).build(); + List records = client.getBulkInsertRequestBody(request).getRecords().get(); + + Assert.assertEquals(5, records.size()); + for (int i = 0; i < 5; i++) { + Assert.assertEquals("record order must be preserved", i, records.get(i).getFields().get().get("position")); + } + } + + @Test + public void testInsertBulk_fewerTokensThanValuesLeavesTrailingRecordsWithoutTokens() { + VaultClient client = newVaultClient(); + + HashMap value0 = new HashMap<>(); + value0.put("card_number", "4111111111111111"); + HashMap value1 = new HashMap<>(); + value1.put("card_number", "5111111111111111"); + HashMap token0 = new HashMap<>(); + token0.put("card_number", "token-0"); + + InsertRequest request = InsertRequest.builder() + .table("cards") + .values(rows(value0, value1)) + .tokens(rows(token0)) + .tokenMode(TokenMode.ENABLE) + .build(); + + List records = client.getBulkInsertRequestBody(request).getRecords().get(); + Assert.assertEquals(2, records.size()); + Assert.assertEquals("token-0", records.get(0).getTokens().get().get("card_number")); + Assert.assertFalse("record without a matching token entry gets no tokens", + records.get(1).getTokens().isPresent()); + } + + // ================================================================== + // insert — batch branch (continueOnError == true) + // ================================================================== + + @Test + public void testInsertBatch_everyBuilderValueReachesGeneratedBody() { + VaultClient client = newVaultClient(); + + HashMap row1 = richRow(); + HashMap row2 = new HashMap<>(); + row2.put("name", "second record"); + + InsertRequest request = InsertRequest.builder() + .table("cards") + .values(rows(row1, row2)) + .returnTokens(true) + .upsert("email") + .tokenMode(TokenMode.ENABLE) + .continueOnError(true) + .build(); + + RecordServiceBatchOperationBody body = client.getBatchInsertRequestBody(request); + + Assert.assertTrue("batch body always sends continueOnError=true", body.getContinueOnError().get()); + Assert.assertEquals(V1Byot.ENABLE, body.getByot().get()); + + List records = body.getRecords().get(); + Assert.assertEquals(2, records.size()); + + V1BatchRecord first = records.get(0); + Assert.assertEquals("table maps to per-record tableName", "cards", first.getTableName().get()); + Assert.assertEquals(BatchRecordMethod.POST, first.getMethod().get()); + Assert.assertEquals("email", first.getUpsert().get()); + Assert.assertTrue(first.getTokenization().get()); + Assert.assertEquals("whole values map must be carried verbatim", row1, first.getFields().get()); + Assert.assertEquals(NON_ASCII_NAME, first.getFields().get().get("name")); + + Assert.assertEquals("cards", records.get(1).getTableName().get()); + Assert.assertEquals("second record", records.get(1).getFields().get().get("name")); + } + + @Test + public void testInsertBatch_tokensPairPositionallyAndOrderIsPreserved() { + VaultClient client = newVaultClient(); + + ArrayList> values = new ArrayList<>(); + ArrayList> tokens = new ArrayList<>(); + for (int i = 0; i < 4; i++) { + HashMap row = new HashMap<>(); + row.put("position", i); + values.add(row); + + HashMap token = new HashMap<>(); + token.put("position", "token-" + i); + tokens.add(token); + } + + InsertRequest request = InsertRequest.builder() + .table("cards") + .values(values) + .tokens(tokens) + .tokenMode(TokenMode.ENABLE_STRICT) + .continueOnError(true) + .build(); + + List records = client.getBatchInsertRequestBody(request).getRecords().get(); + Assert.assertEquals(4, records.size()); + for (int i = 0; i < 4; i++) { + Assert.assertEquals(i, records.get(i).getFields().get().get("position")); + Assert.assertEquals("token-" + i, records.get(i).getTokens().get().get("position")); + } + } + + /** + * KNOWN GAP: {@code homogeneous} is silently dropped when {@code continueOnError(true)} routes + * the insert down the batch path — the batch body has no field able to carry it. + */ + @Test + public void testInsertBatch_homogeneousIsDropped_knownGap() { + VaultClient client = newVaultClient(); + + InsertRequest bulkRequest = InsertRequest.builder() + .table("cards").values(rows(richRow())).homogeneous(true).continueOnError(false).build(); + RecordServiceInsertRecordBody bulkBody = client.getBulkInsertRequestBody(bulkRequest); + Assert.assertTrue("bulk branch DOES send homogeneous", bulkBody.getHomogeneous().get()); + Assert.assertTrue(bulkBody.toString().contains("homogeneous")); + + InsertRequest batchRequest = InsertRequest.builder() + .table("cards").values(rows(richRow())).homogeneous(true).continueOnError(true).build(); + RecordServiceBatchOperationBody batchBody = client.getBatchInsertRequestBody(batchRequest); + + for (Method method : RecordServiceBatchOperationBody.class.getMethods()) { + Assert.assertFalse("batch body has no homogeneous accessor", + method.getName().toLowerCase(Locale.ROOT).contains("homogeneous")); + } + for (Method method : V1BatchRecord.class.getMethods()) { + Assert.assertFalse("batch record has no homogeneous accessor", + method.getName().toLowerCase(Locale.ROOT).contains("homogeneous")); + } + Assert.assertTrue(batchBody.getAdditionalProperties().isEmpty()); + Assert.assertFalse("homogeneous never reaches the wire on the batch branch", + batchBody.toString().contains("homogeneous")); + } + + @Test + public void testInsert_continueOnErrorFalseUsesBulkEndpointWithTableAsPathParam() throws Exception { + ApiClient mockApi = Mockito.mock(ApiClient.class); + RecordsClient mockRecords = Mockito.mock(RecordsClient.class); + when(mockApi.records()).thenReturn(mockRecords); + when(mockRecords.recordServiceInsertRecord(anyString(), anyString(), any())) + .thenReturn(V1InsertRecordResponse.builder().build()); + + VaultController controller = newControllerWithMockApi(mockApi); + InsertRequest request = InsertRequest.builder() + .table("cards").values(rows(richRow())).returnTokens(true).continueOnError(false).build(); + controller.insert(request); + + ArgumentCaptor bodyCaptor = + ArgumentCaptor.forClass(RecordServiceInsertRecordBody.class); + Mockito.verify(mockRecords).recordServiceInsertRecord(eq(VAULT_ID), eq("cards"), bodyCaptor.capture()); + Assert.assertTrue(bodyCaptor.getValue().getTokenization().get()); + Mockito.verify(mockRecords, Mockito.never()).withRawResponse(); + } + + @Test + public void testInsert_continueOnErrorTrueUsesBatchEndpoint() throws Exception { + ApiClient mockApi = Mockito.mock(ApiClient.class); + RecordsClient mockRecords = Mockito.mock(RecordsClient.class); + RawRecordsClient mockRawRecords = Mockito.mock(RawRecordsClient.class); + when(mockApi.records()).thenReturn(mockRecords); + when(mockRecords.withRawResponse()).thenReturn(mockRawRecords); + when(mockRawRecords.recordServiceBatchOperation(anyString(), any(), any())) + .thenReturn(new com.skyflow.generated.rest.core.ApiClientHttpResponse<>( + V1BatchOperationResponse.builder().build(), buildOkHttpResponse())); + + VaultController controller = newControllerWithMockApi(mockApi); + InsertRequest request = InsertRequest.builder() + .table("cards").values(rows(richRow())).continueOnError(true).build(); + controller.insert(request); + + ArgumentCaptor bodyCaptor = + ArgumentCaptor.forClass(RecordServiceBatchOperationBody.class); + Mockito.verify(mockRawRecords).recordServiceBatchOperation(eq(VAULT_ID), bodyCaptor.capture(), any()); + Assert.assertTrue(bodyCaptor.getValue().getContinueOnError().get()); + Assert.assertEquals("cards", bodyCaptor.getValue().getRecords().get().get(0).getTableName().get()); + Mockito.verify(mockRecords, Mockito.never()).recordServiceInsertRecord(anyString(), anyString(), any()); + } + + // ================================================================== + // update + // ================================================================== + + @Test + public void testUpdate_everyBuilderValueReachesGeneratedRequest() throws Exception { + ApiClient mockApi = Mockito.mock(ApiClient.class); + RecordsClient mockRecords = Mockito.mock(RecordsClient.class); + when(mockApi.records()).thenReturn(mockRecords); + when(mockRecords.recordServiceUpdateRecord(anyString(), anyString(), anyString(), any(), any())) + .thenReturn(V1UpdateRecordResponse.builder().skyflowId("sky-id-1").build()); + + VaultController controller = newControllerWithMockApi(mockApi); + + HashMap data = new HashMap<>(); + data.put("skyflowId", "sky-id-1"); + data.put("name", NON_ASCII_NAME); + data.put("notes", MULTILINE_NOTE); + data.put("age", 42); + + HashMap tokens = new HashMap<>(); + tokens.put("name", "token-for-name"); + + UpdateRequest request = UpdateRequest.builder() + .table("cards") + .data(data) + .tokens(tokens) + .returnTokens(true) + .tokenMode(TokenMode.ENABLE) + .build(); + + controller.update(request); + + ArgumentCaptor tableCaptor = ArgumentCaptor.forClass(String.class); + ArgumentCaptor idCaptor = ArgumentCaptor.forClass(String.class); + ArgumentCaptor bodyCaptor = + ArgumentCaptor.forClass(RecordServiceUpdateRecordBody.class); + Mockito.verify(mockRecords).recordServiceUpdateRecord( + eq(VAULT_ID), tableCaptor.capture(), idCaptor.capture(), bodyCaptor.capture(), any()); + + Assert.assertEquals("cards", tableCaptor.getValue()); + Assert.assertEquals("skyflowId becomes the path id", "sky-id-1", idCaptor.getValue()); + + RecordServiceUpdateRecordBody body = bodyCaptor.getValue(); + Assert.assertTrue(body.getTokenization().get()); + Assert.assertEquals(V1Byot.ENABLE, body.getByot().get()); + + Map fields = body.getRecord().get().getFields().get(); + Assert.assertFalse("skyflowId must be stripped from fields", fields.containsKey("skyflowId")); + Assert.assertEquals(NON_ASCII_NAME, fields.get("name")); + Assert.assertEquals(MULTILINE_NOTE, fields.get("notes")); + Assert.assertEquals(42, fields.get("age")); + Assert.assertEquals(3, fields.size()); + + Assert.assertEquals("token-for-name", body.getRecord().get().getTokens().get().get("name")); + } + + @Test + public void testUpdate_deprecatedSkyflowIdKeyBecomesPathIdAndIsStripped() throws Exception { + ApiClient mockApi = Mockito.mock(ApiClient.class); + RecordsClient mockRecords = Mockito.mock(RecordsClient.class); + when(mockApi.records()).thenReturn(mockRecords); + when(mockRecords.recordServiceUpdateRecord(anyString(), anyString(), anyString(), any(), any())) + .thenReturn(V1UpdateRecordResponse.builder().skyflowId("snake-id").build()); + + VaultController controller = newControllerWithMockApi(mockApi); + + HashMap data = new HashMap<>(); + data.put("skyflow_id", "snake-id"); + data.put("name", NON_ASCII_NAME); + + controller.update(UpdateRequest.builder().table("cards").data(data).build()); + + ArgumentCaptor idCaptor = ArgumentCaptor.forClass(String.class); + ArgumentCaptor bodyCaptor = + ArgumentCaptor.forClass(RecordServiceUpdateRecordBody.class); + Mockito.verify(mockRecords).recordServiceUpdateRecord( + anyString(), anyString(), idCaptor.capture(), bodyCaptor.capture(), any()); + + Assert.assertEquals("snake-id", idCaptor.getValue()); + Map fields = bodyCaptor.getValue().getRecord().get().getFields().get(); + Assert.assertFalse(fields.containsKey("skyflow_id")); + Assert.assertEquals(NON_ASCII_NAME, fields.get("name")); + } + + @Test + public void testUpdate_defaultsReachGeneratedRequest() throws Exception { + ApiClient mockApi = Mockito.mock(ApiClient.class); + RecordsClient mockRecords = Mockito.mock(RecordsClient.class); + when(mockApi.records()).thenReturn(mockRecords); + when(mockRecords.recordServiceUpdateRecord(anyString(), anyString(), anyString(), any(), any())) + .thenReturn(V1UpdateRecordResponse.builder().skyflowId("sky-id-1").build()); + + VaultController controller = newControllerWithMockApi(mockApi); + + HashMap data = new HashMap<>(); + data.put("skyflowId", "sky-id-1"); + data.put("name", "plain"); + + controller.update(UpdateRequest.builder().table("cards").data(data).build()); + + ArgumentCaptor bodyCaptor = + ArgumentCaptor.forClass(RecordServiceUpdateRecordBody.class); + Mockito.verify(mockRecords).recordServiceUpdateRecord( + anyString(), anyString(), anyString(), bodyCaptor.capture(), any()); + + Assert.assertFalse("returnTokens defaults to false", bodyCaptor.getValue().getTokenization().get()); + Assert.assertEquals(V1Byot.DISABLE, bodyCaptor.getValue().getByot().get()); + Assert.assertFalse("no tokens supplied -> tokens absent", + bodyCaptor.getValue().getRecord().get().getTokens().isPresent()); + } + + /** + * KNOWN GAP: {@code update()} removes {@code skyflowId} from the caller's own data map, so the + * same request object cannot be reused — a second call fails validation. + */ + @Test + public void testUpdate_mutatesCallersDataMap_knownGap() throws Exception { + ApiClient mockApi = Mockito.mock(ApiClient.class); + RecordsClient mockRecords = Mockito.mock(RecordsClient.class); + when(mockApi.records()).thenReturn(mockRecords); + when(mockRecords.recordServiceUpdateRecord(anyString(), anyString(), anyString(), any(), any())) + .thenReturn(V1UpdateRecordResponse.builder().skyflowId("sky-id-1").build()); + + VaultController controller = newControllerWithMockApi(mockApi); + + HashMap data = new HashMap<>(); + data.put("skyflowId", "sky-id-1"); + data.put("name", NON_ASCII_NAME); + + UpdateRequest request = UpdateRequest.builder().table("cards").data(data).build(); + controller.update(request); + + Assert.assertFalse("caller's map is mutated: skyflowId removed", data.containsKey("skyflowId")); + Assert.assertSame("request still hands back the same (now-mutated) map", data, request.getData()); + + try { + controller.update(request); + Assert.fail("second update with the same request object should fail"); + } catch (SkyflowException e) { + Assert.assertTrue("fails because skyflowId is gone from the caller's map: " + e.getMessage(), + e.getMessage().contains("'skyflow_id' is missing from the data payload")); + } + } + + // ================================================================== + // get + // ================================================================== + + private static VaultController mockGetController(RecordsClient mockRecords) throws Exception { + ApiClient mockApi = Mockito.mock(ApiClient.class); + when(mockApi.records()).thenReturn(mockRecords); + when(mockRecords.recordServiceBulkGetRecord(anyString(), anyString(), any(), any())) + .thenReturn(V1BulkGetRecordResponse.builder() + .records(Collections.emptyList()) + .build()); + return newControllerWithMockApi(mockApi); + } + + @Test + public void testGet_idsFieldsRedactionAndPagingReachGeneratedRequest() throws Exception { + RecordsClient mockRecords = Mockito.mock(RecordsClient.class); + VaultController controller = mockGetController(mockRecords); + + GetRequest request = GetRequest.builder() + .table("cards") + .ids(list("id-1", "id-2", "id-3")) + .fields(list("name", "card_number")) + .redactionType(RedactionType.PLAIN_TEXT) + .offset("10") + .limit("25") + .downloadUrl(false) + .orderBy("DESCENDING") + .build(); + + controller.get(request); + + ArgumentCaptor tableCaptor = ArgumentCaptor.forClass(String.class); + ArgumentCaptor requestCaptor = + ArgumentCaptor.forClass(RecordServiceBulkGetRecordRequest.class); + Mockito.verify(mockRecords).recordServiceBulkGetRecord( + eq(VAULT_ID), tableCaptor.capture(), requestCaptor.capture(), any()); + + Assert.assertEquals("cards", tableCaptor.getValue()); + RecordServiceBulkGetRecordRequest generated = requestCaptor.getValue(); + Assert.assertEquals("ids map to skyflowIds, order preserved", + Arrays.asList("id-1", "id-2", "id-3"), generated.getSkyflowIds().get()); + Assert.assertEquals(Arrays.asList("name", "card_number"), generated.getFields().get()); + Assert.assertEquals(RecordServiceBulkGetRecordRequestRedaction.PLAIN_TEXT, generated.getRedaction().get()); + Assert.assertEquals("10", generated.getOffset().get()); + Assert.assertEquals("25", generated.getLimit().get()); + Assert.assertFalse("downloadUrl(false) must reach downloadURL", generated.getDownloadUrl().get()); + Assert.assertEquals(RecordServiceBulkGetRecordRequestOrderBy.DESCENDING, generated.getOrderBy().get()); + Assert.assertFalse(generated.getColumnName().isPresent()); + Assert.assertFalse(generated.getColumnValues().isPresent()); + } + + @Test + public void testGet_columnNameAndColumnValuesReachGeneratedRequest() throws Exception { + RecordsClient mockRecords = Mockito.mock(RecordsClient.class); + VaultController controller = mockGetController(mockRecords); + + GetRequest request = GetRequest.builder() + .table("cards") + .columnName("email") + .columnValues(list("a@example.com", NON_ASCII_NAME, "value with spaces")) + .redactionType(RedactionType.MASKED) + .build(); + + controller.get(request); + + ArgumentCaptor requestCaptor = + ArgumentCaptor.forClass(RecordServiceBulkGetRecordRequest.class); + Mockito.verify(mockRecords).recordServiceBulkGetRecord( + anyString(), anyString(), requestCaptor.capture(), any()); + + RecordServiceBulkGetRecordRequest generated = requestCaptor.getValue(); + Assert.assertEquals("email", generated.getColumnName().get()); + Assert.assertEquals(Arrays.asList("a@example.com", NON_ASCII_NAME, "value with spaces"), + generated.getColumnValues().get()); + Assert.assertEquals(RecordServiceBulkGetRecordRequestRedaction.MASKED, generated.getRedaction().get()); + Assert.assertFalse(generated.getSkyflowIds().isPresent()); + } + + @Test + public void testGet_returnTokensMapsToTokenization() throws Exception { + RecordsClient mockRecords = Mockito.mock(RecordsClient.class); + VaultController controller = mockGetController(mockRecords); + + controller.get(GetRequest.builder().table("cards").ids(list("id-1")).returnTokens(true).build()); + + ArgumentCaptor requestCaptor = + ArgumentCaptor.forClass(RecordServiceBulkGetRecordRequest.class); + Mockito.verify(mockRecords).recordServiceBulkGetRecord( + anyString(), anyString(), requestCaptor.capture(), any()); + + Assert.assertTrue(requestCaptor.getValue().getTokenization().get()); + Assert.assertFalse("no redactionType -> redaction absent", requestCaptor.getValue().getRedaction().isPresent()); + } + + @Test + public void testGet_defaultOrderByAndDownloadUrlReachGeneratedRequest() throws Exception { + RecordsClient mockRecords = Mockito.mock(RecordsClient.class); + VaultController controller = mockGetController(mockRecords); + + controller.get(GetRequest.builder().table("cards").ids(list("id-1")).build()); + + ArgumentCaptor requestCaptor = + ArgumentCaptor.forClass(RecordServiceBulkGetRecordRequest.class); + Mockito.verify(mockRecords).recordServiceBulkGetRecord( + anyString(), anyString(), requestCaptor.capture(), any()); + + Assert.assertEquals(RecordServiceBulkGetRecordRequestOrderBy.ASCENDING, + requestCaptor.getValue().getOrderBy().get()); + Assert.assertTrue("downloadUrl defaults to true", requestCaptor.getValue().getDownloadUrl().get()); + } + + @Test + public void testGet_orderByAcceptsAllThreeGeneratedEnumNames() throws Exception { + for (String orderBy : new String[]{"ASCENDING", "DESCENDING", "NONE"}) { + RecordsClient mockRecords = Mockito.mock(RecordsClient.class); + VaultController controller = mockGetController(mockRecords); + + controller.get(GetRequest.builder().table("cards").ids(list("id-1")).orderBy(orderBy).build()); + + ArgumentCaptor requestCaptor = + ArgumentCaptor.forClass(RecordServiceBulkGetRecordRequest.class); + Mockito.verify(mockRecords).recordServiceBulkGetRecord( + anyString(), anyString(), requestCaptor.capture(), any()); + Assert.assertEquals(RecordServiceBulkGetRecordRequestOrderBy.valueOf(orderBy), + requestCaptor.getValue().getOrderBy().get()); + } + } + + /** + * KNOWN GAP: {@code orderBy} accepts any String but is mapped with {@code Enum.valueOf}, so an + * unrecognised value escapes as a raw {@link IllegalArgumentException} instead of a + * {@link SkyflowException}. + */ + @Test + public void testGet_orderByInvalidValueThrowsRawIllegalArgumentException_knownGap() throws Exception { + for (String orderBy : new String[]{"DESC", "descending", "asc"}) { + RecordsClient mockRecords = Mockito.mock(RecordsClient.class); + VaultController controller = mockGetController(mockRecords); + try { + controller.get(GetRequest.builder().table("cards").ids(list("id-1")).orderBy(orderBy).build()); + Assert.fail("expected an exception for orderBy=" + orderBy); + } catch (SkyflowException e) { + Assert.fail("orderBy=" + orderBy + " should NOT surface as SkyflowException (known gap)"); + } catch (IllegalArgumentException e) { + Assert.assertTrue(e.getMessage().contains(orderBy)); + } + Mockito.verify(mockRecords, Mockito.never()) + .recordServiceBulkGetRecord(anyString(), anyString(), any(), any()); + } + } + + /** + * KNOWN GAP: {@code downloadUrl(null)} silently coerces to {@code true}, while + * {@code returnTokens(null)} keeps the null (and therefore disappears from the request). + */ + @Test + public void testGet_downloadUrlNullCoercesToTrueButReturnTokensNullDoesNot_knownGap() throws Exception { + GetRequest request = GetRequest.builder() + .table("cards") + .ids(list("id-1")) + .downloadUrl(null) + .returnTokens(null) + .build(); + + Assert.assertTrue("downloadUrl(null) coerces to true", request.getDownloadUrl()); + Assert.assertNull("returnTokens(null) stays null", request.getReturnTokens()); + + RecordsClient mockRecords = Mockito.mock(RecordsClient.class); + VaultController controller = mockGetController(mockRecords); + controller.get(request); + + ArgumentCaptor requestCaptor = + ArgumentCaptor.forClass(RecordServiceBulkGetRecordRequest.class); + Mockito.verify(mockRecords).recordServiceBulkGetRecord( + anyString(), anyString(), requestCaptor.capture(), any()); + + Assert.assertTrue("downloadURL=true is sent even though the user passed null", + requestCaptor.getValue().getDownloadUrl().get()); + Assert.assertFalse("tokenization is omitted for a null returnTokens", + requestCaptor.getValue().getTokenization().isPresent()); + } + + // ================================================================== + // delete + // ================================================================== + + @Test + public void testDelete_tableAndIdsReachGeneratedRequest() throws Exception { + ApiClient mockApi = Mockito.mock(ApiClient.class); + RecordsClient mockRecords = Mockito.mock(RecordsClient.class); + when(mockApi.records()).thenReturn(mockRecords); + when(mockRecords.recordServiceBulkDeleteRecord(anyString(), anyString(), any(), any())) + .thenReturn(V1BulkDeleteRecordResponse.builder().build()); + + VaultController controller = newControllerWithMockApi(mockApi); + controller.delete(DeleteRequest.builder() + .table("cards") + .ids(list("id-3", "id-1", "id-2")) + .build()); + + ArgumentCaptor tableCaptor = ArgumentCaptor.forClass(String.class); + ArgumentCaptor bodyCaptor = + ArgumentCaptor.forClass(RecordServiceBulkDeleteRecordBody.class); + Mockito.verify(mockRecords).recordServiceBulkDeleteRecord( + eq(VAULT_ID), tableCaptor.capture(), bodyCaptor.capture(), any()); + + Assert.assertEquals("cards", tableCaptor.getValue()); + Assert.assertEquals("ids map to skyflowIds, order preserved", + Arrays.asList("id-3", "id-1", "id-2"), bodyCaptor.getValue().getSkyflowIds().get()); + } + + // ================================================================== + // query + // ================================================================== + + @Test + public void testQuery_queryStringReachesGeneratedBodyVerbatim() throws Exception { + ApiClient mockApi = Mockito.mock(ApiClient.class); + QueryClient mockQuery = Mockito.mock(QueryClient.class); + when(mockApi.query()).thenReturn(mockQuery); + when(mockQuery.queryServiceExecuteQuery(anyString(), any(), any())) + .thenReturn(V1GetQueryResponse.builder().build()); + + VaultController controller = newControllerWithMockApi(mockApi); + + String sql = "SELECT name, \"card number\"\nFROM cards\n" + + "WHERE name = 'Smith''s' AND city = '" + NON_ASCII_CITY + "'\nLIMIT 10"; + controller.query(QueryRequest.builder().query(sql).build()); + + ArgumentCaptor bodyCaptor = + ArgumentCaptor.forClass(QueryServiceExecuteQueryBody.class); + Mockito.verify(mockQuery).queryServiceExecuteQuery(eq(VAULT_ID), bodyCaptor.capture(), any()); + + Assert.assertEquals("query with quotes and newlines must be sent unchanged", + sql, bodyCaptor.getValue().getQuery().get()); + } + + @Test + public void testQuery_simpleQueryReachesGeneratedBody() throws Exception { + ApiClient mockApi = Mockito.mock(ApiClient.class); + QueryClient mockQuery = Mockito.mock(QueryClient.class); + when(mockApi.query()).thenReturn(mockQuery); + when(mockQuery.queryServiceExecuteQuery(anyString(), any(), any())) + .thenReturn(V1GetQueryResponse.builder().build()); + + VaultController controller = newControllerWithMockApi(mockApi); + controller.query(QueryRequest.builder().query("SELECT * FROM cards LIMIT 1").build()); + + ArgumentCaptor bodyCaptor = + ArgumentCaptor.forClass(QueryServiceExecuteQueryBody.class); + Mockito.verify(mockQuery).queryServiceExecuteQuery(anyString(), bodyCaptor.capture(), any()); + Assert.assertEquals("SELECT * FROM cards LIMIT 1", bodyCaptor.getValue().getQuery().get()); + } + + // ================================================================== + // tokenize + // ================================================================== + + @Test + public void testTokenize_columnValuesReachPayloadInOrder() { + VaultClient client = newVaultClient(); + + List columnValues = Arrays.asList( + ColumnValue.builder().value("4111111111111111").columnGroup("cg_cards").build(), + ColumnValue.builder().value(NON_ASCII_NAME).columnGroup("cg_names").build(), + ColumnValue.builder().value("value with spaces").columnGroup("cg_misc").build()); + + V1TokenizePayload payload = client.getTokenizePayload( + TokenizeRequest.builder().values(columnValues).build()); + + List parameters = payload.getTokenizationParameters().get(); + Assert.assertEquals(3, parameters.size()); + Assert.assertEquals("4111111111111111", parameters.get(0).getValue().get()); + Assert.assertEquals("cg_cards", parameters.get(0).getColumnGroup().get()); + Assert.assertEquals(NON_ASCII_NAME, parameters.get(1).getValue().get()); + Assert.assertEquals("cg_names", parameters.get(1).getColumnGroup().get()); + Assert.assertEquals("value with spaces", parameters.get(2).getValue().get()); + Assert.assertEquals("cg_misc", parameters.get(2).getColumnGroup().get()); + } + + @Test + public void testTokenize_payloadReachesGeneratedApiCall() throws Exception { + ApiClient mockApi = Mockito.mock(ApiClient.class); + com.skyflow.generated.rest.resources.tokens.TokensClient mockTokens = + Mockito.mock(com.skyflow.generated.rest.resources.tokens.TokensClient.class); + when(mockApi.tokens()).thenReturn(mockTokens); + when(mockTokens.recordServiceTokenize(anyString(), any(), any())) + .thenReturn(V1TokenizeResponse.builder().build()); + + VaultController controller = newControllerWithMockApi(mockApi); + controller.tokenize(TokenizeRequest.builder().values(Collections.singletonList( + ColumnValue.builder().value(NON_ASCII_NAME).columnGroup("cg_names").build())).build()); + + ArgumentCaptor payloadCaptor = ArgumentCaptor.forClass(V1TokenizePayload.class); + Mockito.verify(mockTokens).recordServiceTokenize(eq(VAULT_ID), payloadCaptor.capture(), any()); + + V1TokenizeRecordRequest parameter = payloadCaptor.getValue().getTokenizationParameters().get().get(0); + Assert.assertEquals(NON_ASCII_NAME, parameter.getValue().get()); + Assert.assertEquals("cg_names", parameter.getColumnGroup().get()); + } + + // ================================================================== + // detokenize + // ================================================================== + + @Test + public void testDetokenize_eachTokenKeepsItsOwnRedactionType() { + VaultClient client = newVaultClient(); + + ArrayList detokenizeData = new ArrayList<>(Arrays.asList( + new DetokenizeData("tok-default", RedactionType.DEFAULT), + new DetokenizeData("tok-plain", RedactionType.PLAIN_TEXT), + new DetokenizeData("tok-masked", RedactionType.MASKED), + new DetokenizeData("tok-redacted", RedactionType.REDACTED))); + + V1DetokenizePayload payload = client.getDetokenizePayload( + DetokenizeRequest.builder().detokenizeData(detokenizeData).build()); + + List parameters = payload.getDetokenizationParameters().get(); + Assert.assertEquals(4, parameters.size()); + + Assert.assertEquals("tok-default", parameters.get(0).getToken().get()); + Assert.assertEquals(RedactionEnumRedaction.DEFAULT, parameters.get(0).getRedaction().get()); + Assert.assertEquals("tok-plain", parameters.get(1).getToken().get()); + Assert.assertEquals(RedactionEnumRedaction.PLAIN_TEXT, parameters.get(1).getRedaction().get()); + Assert.assertEquals("tok-masked", parameters.get(2).getToken().get()); + Assert.assertEquals(RedactionEnumRedaction.MASKED, parameters.get(2).getRedaction().get()); + Assert.assertEquals("tok-redacted", parameters.get(3).getToken().get()); + Assert.assertEquals(RedactionEnumRedaction.REDACTED, parameters.get(3).getRedaction().get()); + } + + @Test + public void testDetokenize_tokenWithoutRedactionDefaultsToDefault() { + VaultClient client = newVaultClient(); + ArrayList detokenizeData = new ArrayList<>(Arrays.asList( + new DetokenizeData("tok-a"), + new DetokenizeData("tok-b", null))); + + V1DetokenizePayload payload = client.getDetokenizePayload( + DetokenizeRequest.builder().detokenizeData(detokenizeData).build()); + + List parameters = payload.getDetokenizationParameters().get(); + Assert.assertEquals(RedactionEnumRedaction.DEFAULT, parameters.get(0).getRedaction().get()); + Assert.assertEquals(RedactionEnumRedaction.DEFAULT, parameters.get(1).getRedaction().get()); + } + + @Test + public void testDetokenize_continueOnErrorAndDownloadUrlReachPayload() { + VaultClient client = newVaultClient(); + ArrayList detokenizeData = + new ArrayList<>(Collections.singletonList(new DetokenizeData("tok-a"))); + + V1DetokenizePayload payload = client.getDetokenizePayload(DetokenizeRequest.builder() + .detokenizeData(detokenizeData) + .continueOnError(true) + .downloadUrl(true) + .build()); + Assert.assertTrue(payload.getContinueOnError().get()); + Assert.assertTrue(payload.getDownloadUrl().get()); + + V1DetokenizePayload defaults = client.getDetokenizePayload(DetokenizeRequest.builder() + .detokenizeData(detokenizeData) + .build()); + Assert.assertFalse("continueOnError defaults to false", defaults.getContinueOnError().get()); + Assert.assertFalse("downloadUrl defaults to false", defaults.getDownloadUrl().get()); + } + + @Test + public void testDetokenize_tokenOrderIsPreserved() { + VaultClient client = newVaultClient(); + ArrayList detokenizeData = new ArrayList<>(); + for (int i = 0; i < 5; i++) { + detokenizeData.add(new DetokenizeData("tok-" + i, RedactionType.MASKED)); + } + + List parameters = client.getDetokenizePayload( + DetokenizeRequest.builder().detokenizeData(detokenizeData).build()) + .getDetokenizationParameters().get(); + + for (int i = 0; i < 5; i++) { + Assert.assertEquals("tok-" + i, parameters.get(i).getToken().get()); + } + } + + // ================================================================== + // uploadFile + // ================================================================== + + private static VaultController mockUploadController(RecordsClient mockRecords) throws Exception { + ApiClient mockApi = Mockito.mock(ApiClient.class); + when(mockApi.records()).thenReturn(mockRecords); + when(mockRecords.uploadFileV2(anyString(), any(File.class), any(), any())) + .thenReturn(UploadFileV2Response.builder().build()); + return newControllerWithMockApi(mockApi); + } + + @Test + public void testUploadFile_metadataFieldsAndFilePathReachGeneratedRequest() throws Exception { + File tempFile = File.createTempFile("fidelity-upload", ".txt"); + tempFile.deleteOnExit(); + Files.write(tempFile.toPath(), "hello".getBytes("UTF-8")); + + RecordsClient mockRecords = Mockito.mock(RecordsClient.class); + VaultController controller = mockUploadController(mockRecords); + + controller.uploadFile(FileUploadRequest.builder() + .table("cards") + .columnName("file_column") + .skyflowId("sky-id-9") + .filePath(tempFile.getAbsolutePath()) + .build()); + + ArgumentCaptor fileCaptor = ArgumentCaptor.forClass(File.class); + ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(UploadFileV2Request.class); + Mockito.verify(mockRecords).uploadFileV2( + eq(VAULT_ID), fileCaptor.capture(), requestCaptor.capture(), any()); + + UploadFileV2Request generated = requestCaptor.getValue(); + Assert.assertEquals("table maps to tableName", "cards", generated.getTableName()); + Assert.assertEquals("file_column", generated.getColumnName()); + Assert.assertEquals("sky-id-9", generated.getSkyflowId().get()); + Assert.assertFalse("returnFileMetadata is hard-coded to false", generated.getReturnFileMetadata().get()); + Assert.assertEquals(tempFile.getAbsolutePath(), fileCaptor.getValue().getPath()); + } + + @Test + public void testUploadFile_base64AndFileNameProduceMatchingFile() throws Exception { + File tempDir = Files.createTempDirectory("fidelity-b64").toFile(); + tempDir.deleteOnExit(); + File target = new File(tempDir, "decoded-upload.txt"); + target.deleteOnExit(); + + byte[] payload = "café ☕".getBytes("UTF-8"); + String base64 = Base64.getEncoder().encodeToString(payload); + + RecordsClient mockRecords = Mockito.mock(RecordsClient.class); + VaultController controller = mockUploadController(mockRecords); + + controller.uploadFile(FileUploadRequest.builder() + .table("cards") + .columnName("file_column") + .skyflowId("sky-id-b64") + .base64(base64) + .fileName(target.getAbsolutePath()) + .build()); + + ArgumentCaptor fileCaptor = ArgumentCaptor.forClass(File.class); + ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(UploadFileV2Request.class); + Mockito.verify(mockRecords).uploadFileV2( + anyString(), fileCaptor.capture(), requestCaptor.capture(), any()); + + Assert.assertEquals(target.getAbsolutePath(), fileCaptor.getValue().getPath()); + Assert.assertArrayEquals("base64 must be decoded into the named file", + payload, Files.readAllBytes(fileCaptor.getValue().toPath())); + Assert.assertEquals("sky-id-b64", requestCaptor.getValue().getSkyflowId().get()); + } + + @Test + public void testUploadFile_fileObjectIsPassedThroughUnchanged() throws Exception { + File tempFile = File.createTempFile("fidelity-object", ".bin"); + tempFile.deleteOnExit(); + Files.write(tempFile.toPath(), new byte[]{1, 2, 3}); + + RecordsClient mockRecords = Mockito.mock(RecordsClient.class); + VaultController controller = mockUploadController(mockRecords); + + controller.uploadFile(FileUploadRequest.builder() + .table("cards") + .columnName("file_column") + .fileObject(tempFile) + .build()); + + ArgumentCaptor fileCaptor = ArgumentCaptor.forClass(File.class); + ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(UploadFileV2Request.class); + Mockito.verify(mockRecords).uploadFileV2( + anyString(), fileCaptor.capture(), requestCaptor.capture(), any()); + + Assert.assertSame("the caller's File instance must be forwarded", tempFile, fileCaptor.getValue()); + Assert.assertFalse("no skyflowId supplied -> absent", requestCaptor.getValue().getSkyflowId().isPresent()); + } + + @Test + public void testUploadFile_filePathTakesPrecedenceOverOtherSources() throws Exception { + File pathFile = File.createTempFile("fidelity-precedence-path", ".txt"); + pathFile.deleteOnExit(); + Files.write(pathFile.toPath(), "from-path".getBytes("UTF-8")); + + File objectFile = File.createTempFile("fidelity-precedence-object", ".txt"); + objectFile.deleteOnExit(); + Files.write(objectFile.toPath(), "from-object".getBytes("UTF-8")); + + RecordsClient mockRecords = Mockito.mock(RecordsClient.class); + VaultController controller = mockUploadController(mockRecords); + + controller.uploadFile(FileUploadRequest.builder() + .table("cards") + .columnName("file_column") + .filePath(pathFile.getAbsolutePath()) + .fileObject(objectFile) + .build()); + + ArgumentCaptor fileCaptor = ArgumentCaptor.forClass(File.class); + Mockito.verify(mockRecords).uploadFileV2(anyString(), fileCaptor.capture(), any(), any()); + Assert.assertEquals(pathFile.getAbsolutePath(), fileCaptor.getValue().getPath()); + } + + // ================================================================== + // invoke — URL construction (com.skyflow.utils.Utils) + // ================================================================== + + private static ConnectionConfig connectionConfig(String url) { + ConnectionConfig config = new ConnectionConfig(); + config.setConnectionId("conn123"); + config.setConnectionUrl(url); + config.setCredentials(apiKeyCredentials()); + return config; + } + + @Test + public void testInvoke_pathParamsAreSubstitutedIntoTheUrl() { + Map pathParams = new LinkedHashMap<>(); + pathParams.put("resource", "cards"); + + String url = Utils.constructConnectionURL(connectionConfig(CONNECTION_URL), + InvokeConnectionRequest.builder().pathParams(pathParams).build()); + + Assert.assertEquals("https://conn.example.com/api/cards/details", url); + } + + @Test + public void testInvoke_queryParamsAreAppendedInOrderWithNoTrailingAmpersand() { + Map queryParams = new LinkedHashMap<>(); + queryParams.put("limit", "10"); + queryParams.put("offset", "20"); + + String url = Utils.constructConnectionURL(connectionConfig("https://conn.example.com/api"), + InvokeConnectionRequest.builder().queryParams(queryParams).build()); + + Assert.assertEquals("https://conn.example.com/api?limit=10&offset=20", url); + } + + /** + * KNOWN GAP: query-param values are concatenated raw, so a value containing {@code &} or + * {@code =} re-partitions the query string into extra parameters. + */ + @Test + public void testInvoke_queryParamValuesAreNotPercentEncoded_knownGap() { + Map queryParams = new LinkedHashMap<>(); + queryParams.put("q", "a&b=c"); + + String url = Utils.constructConnectionURL(connectionConfig("https://conn.example.com/api"), + InvokeConnectionRequest.builder().queryParams(queryParams).build()); + + Assert.assertEquals("value is injected raw", "https://conn.example.com/api?q=a&b=c", url); + + String queryString = url.substring(url.indexOf('?') + 1); + Assert.assertEquals("one user param has become two wire params", 2, queryString.split("&").length); + Assert.assertEquals("q=a", queryString.split("&")[0]); + Assert.assertEquals("b=c", queryString.split("&")[1]); + } + + /** + * KNOWN GAP: a query-param value containing a space is injected raw, producing an invalid URI. + */ + @Test + public void testInvoke_queryParamValueWithSpaceIsNotEncoded_knownGap() { + Map queryParams = new LinkedHashMap<>(); + queryParams.put("name", "John Doe"); + + String url = Utils.constructConnectionURL(connectionConfig("https://conn.example.com/api"), + InvokeConnectionRequest.builder().queryParams(queryParams).build()); + + Assert.assertEquals("https://conn.example.com/api?name=John Doe", url); + Assert.assertFalse("no %20 encoding is applied", url.contains("%20")); + try { + java.net.URI.create(url); + Assert.fail("raw space should make this an invalid URI"); + } catch (IllegalArgumentException expected) { + Assert.assertTrue(expected.getMessage().contains("Illegal character")); + } + } + + /** + * KNOWN GAP: path-param values are substituted raw, so a value containing {@code /} silently + * changes the URL's path structure. + */ + @Test + public void testInvoke_pathParamValuesAreNotPercentEncoded_knownGap() { + Map pathParams = new LinkedHashMap<>(); + pathParams.put("resource", "cards/123"); + + String url = Utils.constructConnectionURL(connectionConfig(CONNECTION_URL), + InvokeConnectionRequest.builder().pathParams(pathParams).build()); + + Assert.assertEquals("https://conn.example.com/api/cards/123/details", url); + Assert.assertFalse("no %2F encoding is applied", url.toUpperCase(Locale.ROOT).contains("%2F")); + Assert.assertEquals("the path gained a segment", 5, java.net.URI.create(url).getPath().split("/").length); + } + + /** + * KNOWN GAP: a path-param entry with no matching {@code {placeholder}} is discarded silently — + * no error, no log, and the URL is unchanged. + */ + @Test + public void testInvoke_pathParamWithoutMatchingPlaceholderIsSilentlyDiscarded_knownGap() { + Map pathParams = new LinkedHashMap<>(); + pathParams.put("resource", "cards"); + pathParams.put("thisKeyIsNotInTheUrl", "ignored-value"); + + String url = Utils.constructConnectionURL(connectionConfig(CONNECTION_URL), + InvokeConnectionRequest.builder().pathParams(pathParams).build()); + + Assert.assertEquals("https://conn.example.com/api/cards/details", url); + Assert.assertFalse(url.contains("ignored-value")); + Assert.assertFalse(url.contains("thisKeyIsNotInTheUrl")); + } + + @Test + public void testInvoke_unfilledPlaceholderRemainsInTheUrl() { + String url = Utils.constructConnectionURL(connectionConfig(CONNECTION_URL), + InvokeConnectionRequest.builder().build()); + Assert.assertEquals("https://conn.example.com/api/{resource}/details", url); + } + + // ================================================================== + // invoke — header construction (com.skyflow.utils.Utils) + // ================================================================== + + @Test + public void testInvoke_headerKeysAreLowercasedAndValuesPreserved() { + Map headers = new LinkedHashMap<>(); + headers.put("Content-Type", "application/json"); + headers.put("X-Custom-Header", NON_ASCII_NAME); + + Map constructed = Utils.constructConnectionHeadersMap(headers); + + Assert.assertEquals(2, constructed.size()); + Assert.assertEquals("application/json", constructed.get("content-type")); + Assert.assertEquals(NON_ASCII_NAME, constructed.get("x-custom-header")); + } + + /** + * KNOWN GAP: header keys are lowercased without a {@link Locale}, and two keys differing only + * in case collapse into a single entry (last writer wins). + */ + @Test + public void testInvoke_headerKeysDifferingOnlyInCaseCollapse_knownGap() { + Map headers = new LinkedHashMap<>(); + headers.put("X-Custom", "first-value"); + headers.put("x-custom", "second-value"); + headers.put("X-CUSTOM", "third-value"); + + Map constructed = Utils.constructConnectionHeadersMap(headers); + + Assert.assertEquals("three user headers collapse into one", 1, constructed.size()); + Assert.assertEquals("last writer wins", "third-value", constructed.get("x-custom")); + } + + // ================================================================== + // invoke — body encoding (com.skyflow.utils.HttpUtility) + // ================================================================== + + @Test + public void testInvoke_formEncodedBodyEncodesKeysValuesAndNestedObjects() { + JsonObject body = new JsonObject(); + body.addProperty("name", "John Doe"); + JsonObject nested = new JsonObject(); + nested.addProperty("city", "New York"); + body.add("address", nested); + + String encoded = HttpUtility.formatJsonToFormEncodedString(body); + + Assert.assertTrue(encoded.contains("name=John+Doe")); + Assert.assertTrue("nested objects are flattened to key[subkey]", + encoded.contains("address%5Bcity%5D=New+York")); + } + + /** + * KNOWN GAP: with {@code application/x-www-form-urlencoded}, a 1-element JSON array silently + * loses its array-ness and is sent as a bare scalar. + */ + @Test + public void testInvoke_formEncodedSingleElementArrayLosesArrayness_knownGap() { + JsonObject body = new JsonObject(); + JsonArray items = new JsonArray(); + items.add("only-item"); + body.add("items", items); + + Assert.assertEquals("items=only-item", HttpUtility.formatJsonToFormEncodedString(body)); + } + + /** + * KNOWN GAP: with {@code application/x-www-form-urlencoded}, an array of 2+ elements blows up + * with a raw {@link IllegalStateException}. + */ + @Test + public void testInvoke_formEncodedMultiElementArrayThrowsIllegalState_knownGap() { + JsonObject body = new JsonObject(); + JsonArray items = new JsonArray(); + items.add("first"); + items.add("second"); + body.add("items", items); + + try { + HttpUtility.formatJsonToFormEncodedString(body); + Assert.fail("expected IllegalStateException for a multi-element array"); + } catch (IllegalStateException expected) { + Assert.assertTrue(expected.getMessage().contains("2")); + } + } + + /** + * KNOWN GAP: with {@code application/x-www-form-urlencoded}, a JSON null blows up with a raw + * {@link UnsupportedOperationException}. + */ + @Test + public void testInvoke_formEncodedJsonNullThrowsUnsupportedOperation_knownGap() { + JsonObject body = new JsonObject(); + body.add("maybe", JsonNull.INSTANCE); + + try { + HttpUtility.formatJsonToFormEncodedString(body); + Assert.fail("expected UnsupportedOperationException for a JSON null"); + } catch (UnsupportedOperationException expected) { + Assert.assertEquals("JsonNull", expected.getMessage()); + } + } + + /** + * KNOWN GAP: the multipart encoder shares {@code convertJsonToMap} and therefore has exactly + * the same array/null defects. + */ + @Test + public void testInvoke_multipartBodyHasTheSameArrayAndNullDefects_knownGap() { + JsonObject single = new JsonObject(); + JsonArray oneItem = new JsonArray(); + oneItem.add("only-item"); + single.add("items", oneItem); + Assert.assertTrue("array-ness is lost", + HttpUtility.formatJsonToMultiPartFormDataString(single, "bnd").contains("only-item")); + + JsonObject many = new JsonObject(); + JsonArray twoItems = new JsonArray(); + twoItems.add("first"); + twoItems.add("second"); + many.add("items", twoItems); + try { + HttpUtility.formatJsonToMultiPartFormDataString(many, "bnd"); + Assert.fail("expected IllegalStateException for a multi-element array"); + } catch (IllegalStateException expected) { + Assert.assertNotNull(expected.getMessage()); + } + + JsonObject withNull = new JsonObject(); + withNull.add("maybe", JsonNull.INSTANCE); + try { + HttpUtility.formatJsonToMultiPartFormDataString(withNull, "bnd"); + Assert.fail("expected UnsupportedOperationException for a JSON null"); + } catch (UnsupportedOperationException expected) { + Assert.assertEquals("JsonNull", expected.getMessage()); + } + } + + // ================================================================== + // invoke — transport + // ================================================================== + + /** + * KNOWN GAP: {@link RequestMethod#PATCH} can be built into a request but never reaches the + * wire — {@code HttpURLConnection.setRequestMethod} rejects it, surfacing as a + * {@link SkyflowException} wrapping a {@link ProtocolException}. + */ + @Test + public void testInvoke_patchMethodCannotBeSent_knownGap() { + ConnectionController controller = + new ConnectionController(connectionConfig("https://conn.example.com/api"), apiKeyCredentials()); + + try { + controller.invoke(InvokeConnectionRequest.builder().method(RequestMethod.PATCH).build()); + Assert.fail("PATCH should not be sendable"); + } catch (SkyflowException e) { + Assert.assertTrue("cause must be a ProtocolException, got: " + e.getCause(), + e.getCause() instanceof ProtocolException); + Assert.assertTrue(e.getCause().getMessage().contains("PATCH")); + } + } +} diff --git a/v2/src/test/java/com/skyflow/vault/controller/ConnectionRequestFidelityTests.java b/v2/src/test/java/com/skyflow/vault/controller/ConnectionRequestFidelityTests.java new file mode 100644 index 00000000..b44c3310 --- /dev/null +++ b/v2/src/test/java/com/skyflow/vault/controller/ConnectionRequestFidelityTests.java @@ -0,0 +1,267 @@ +package com.skyflow.vault.controller; + +import com.google.gson.JsonObject; +import com.skyflow.config.ConnectionConfig; +import com.skyflow.config.Credentials; +import com.skyflow.enums.RequestMethod; +import com.skyflow.utils.Constants; +import com.skyflow.utils.HttpUtility; +import com.skyflow.vault.connection.InvokeConnectionRequest; +import org.junit.Assert; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.powermock.api.mockito.PowerMockito; +import org.powermock.core.classloader.annotations.PrepareForTest; +import org.powermock.modules.junit4.PowerMockRunner; + +import java.net.URL; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.Map; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.when; + +/** + * Request-fidelity tests for {@code ConnectionController.invoke} at the transport boundary: every + * value the user sets on {@link InvokeConnectionRequest} is captured as it is handed to + * {@link HttpUtility#sendRequest}. + * + *

Tests suffixed {@code _knownGap} pin confirmed defects and assert the CURRENT behaviour. + */ +@RunWith(PowerMockRunner.class) +@PrepareForTest({HttpUtility.class}) +public class ConnectionRequestFidelityTests { + + private static final String API_KEY = "sky-ab123-abcd1234cdef1234abcd4321cdef4321"; // gitleaks:allow + private static final String CONNECTION_URL = "https://conn.example.com/api/{resource}/details"; + private static final String NON_ASCII_NAME = "日本語 テスト"; + + private static ConnectionConfig connectionConfig; + private static Credentials credentials; + private ConnectionController controller; + + @BeforeClass + public static void setupClass() { + credentials = new Credentials(); + credentials.setApiKey(API_KEY); + + connectionConfig = new ConnectionConfig(); + connectionConfig.setConnectionId("conn123"); + connectionConfig.setConnectionUrl(CONNECTION_URL); + connectionConfig.setCredentials(credentials); + } + + @Before + public void setup() throws Exception { + controller = new ConnectionController(connectionConfig, credentials); + PowerMockito.mockStatic(HttpUtility.class); + when(HttpUtility.sendRequest(anyString(), any(URL.class), any(), any())).thenReturn("{}"); + when(HttpUtility.getRequestID()).thenReturn("req-fidelity-1"); + } + + private static final class Captured { + private String method; + private URL url; + private JsonObject body; + private Map headers; + } + + @SuppressWarnings("unchecked") + private static Captured capture() throws Exception { + ArgumentCaptor methodCaptor = ArgumentCaptor.forClass(String.class); + ArgumentCaptor urlCaptor = ArgumentCaptor.forClass(URL.class); + ArgumentCaptor bodyCaptor = ArgumentCaptor.forClass(JsonObject.class); + ArgumentCaptor headersCaptor = ArgumentCaptor.forClass(Map.class); + + PowerMockito.verifyStatic(HttpUtility.class); + HttpUtility.sendRequest(methodCaptor.capture(), urlCaptor.capture(), + bodyCaptor.capture(), headersCaptor.capture()); + + Captured captured = new Captured(); + captured.method = methodCaptor.getValue(); + captured.url = urlCaptor.getValue(); + captured.body = bodyCaptor.getValue(); + captured.headers = headersCaptor.getValue(); + return captured; + } + + @Test + public void testInvoke_methodPathParamsQueryParamsHeadersAndBodyAllReachTransport() throws Exception { + Map pathParams = new LinkedHashMap<>(); + pathParams.put("resource", "cards"); + + Map queryParams = new LinkedHashMap<>(); + queryParams.put("limit", "10"); + queryParams.put("offset", "20"); + + Map requestHeaders = new LinkedHashMap<>(); + requestHeaders.put("X-Custom-Header", NON_ASCII_NAME); + requestHeaders.put("content-type", "application/json"); + + Map nested = new LinkedHashMap<>(); + nested.put("city", "北京市 朝阳区"); + nested.put("zip", "100000"); + + Map requestBody = new LinkedHashMap<>(); + requestBody.put("name", NON_ASCII_NAME); + requestBody.put("age", 42); + requestBody.put("active", true); + requestBody.put("address", nested); + + controller.invoke(InvokeConnectionRequest.builder() + .method(RequestMethod.PUT) + .pathParams(pathParams) + .queryParams(queryParams) + .requestHeaders(requestHeaders) + .requestBody(requestBody) + .build()); + + Captured captured = capture(); + + Assert.assertEquals("PUT", captured.method); + Assert.assertEquals("https://conn.example.com/api/cards/details?limit=10&offset=20", + captured.url.toString()); + + Assert.assertEquals(NON_ASCII_NAME, captured.headers.get("x-custom-header")); + Assert.assertEquals("application/json", captured.headers.get("content-type")); + Assert.assertEquals("auth header carries the api key", + API_KEY, captured.headers.get(Constants.SDK_AUTH_HEADER_KEY)); + + Assert.assertEquals(NON_ASCII_NAME, captured.body.get("name").getAsString()); + Assert.assertEquals(42, captured.body.get("age").getAsInt()); + Assert.assertTrue(captured.body.get("active").getAsBoolean()); + Assert.assertEquals("北京市 朝阳区", + captured.body.getAsJsonObject("address").get("city").getAsString()); + Assert.assertEquals("100000", + captured.body.getAsJsonObject("address").get("zip").getAsString()); + } + + @Test + public void testInvoke_defaultMethodIsPost() throws Exception { + controller.invoke(InvokeConnectionRequest.builder().build()); + Assert.assertEquals("POST", capture().method); + } + + @Test + public void testInvoke_eachRequestMethodIsForwardedByName() throws Exception { + for (RequestMethod method : new RequestMethod[]{ + RequestMethod.GET, RequestMethod.POST, RequestMethod.PUT, RequestMethod.DELETE}) { + ConnectionController freshController = new ConnectionController(connectionConfig, credentials); + PowerMockito.mockStatic(HttpUtility.class); + when(HttpUtility.sendRequest(anyString(), any(URL.class), any(), any())).thenReturn("{}"); + when(HttpUtility.getRequestID()).thenReturn("req-fidelity-1"); + + freshController.invoke(InvokeConnectionRequest.builder().method(method).build()); + Assert.assertEquals(method.name(), capture().method); + } + } + + /** + * NEW FINDING (not one of the 11 audited gaps): a non-object {@code requestBody} — a String, + * a number, a List — never reaches the transport. {@code Validations} calls + * {@code gson.toJsonTree(body).getAsJsonObject()} and blows up with a raw + * {@link IllegalStateException} (not a {@code SkyflowException}), which also makes + * {@code ConnectionController.convertObjectToJson}'s "wrap scalars under a value key" branch + * unreachable for scalars. + */ + @Test + public void testInvoke_scalarRequestBodyThrowsRawIllegalStateException_knownGap() throws Exception { + for (Object body : new Object[]{"a plain string", 42, java.util.Arrays.asList("a", "b")}) { + try { + controller.invoke(InvokeConnectionRequest.builder() + .method(RequestMethod.POST) + .requestBody(body) + .build()); + Assert.fail("expected an exception for a non-object request body: " + body); + } catch (IllegalStateException expected) { + Assert.assertTrue(expected.getMessage().contains("Not a JSON Object")); + } + } + PowerMockito.verifyStatic(HttpUtility.class, org.mockito.Mockito.never()); + HttpUtility.sendRequest(anyString(), any(URL.class), any(), any()); + } + + /** + * KNOWN GAP: a user-supplied {@code sky-metadata} header is unconditionally overwritten by the + * SDK's own metrics blob. + */ + @Test + public void testInvoke_userSuppliedSkyMetadataHeaderIsOverwritten_knownGap() throws Exception { + Map requestHeaders = new HashMap<>(); + requestHeaders.put(Constants.SDK_METRICS_HEADER_KEY, "user-supplied-value"); + + controller.invoke(InvokeConnectionRequest.builder() + .method(RequestMethod.GET) + .requestHeaders(requestHeaders) + .build()); + + String sent = capture().headers.get(Constants.SDK_METRICS_HEADER_KEY); + Assert.assertNotEquals("user value is discarded", "user-supplied-value", sent); + Assert.assertTrue("replaced by the SDK metrics blob", + sent.contains(Constants.SDK_METRIC_NAME_VERSION)); + } + + /** + * KNOWN GAP: two request headers differing only in case collapse into one entry before they + * reach the transport (last writer wins), so one of the user's headers is lost. + */ + @Test + public void testInvoke_headersDifferingOnlyInCaseCollapse_knownGap() throws Exception { + Map requestHeaders = new LinkedHashMap<>(); + requestHeaders.put("X-Trace", "first-value"); + requestHeaders.put("x-trace", "second-value"); + + controller.invoke(InvokeConnectionRequest.builder() + .method(RequestMethod.GET) + .requestHeaders(requestHeaders) + .build()); + + Map headers = capture().headers; + Assert.assertEquals("second-value", headers.get("x-trace")); + Assert.assertFalse("the original-cased key is gone", headers.containsKey("X-Trace")); + } + + /** + * KNOWN GAP: query-param values are not percent-encoded, so a value containing {@code &} and + * {@code =} re-partitions the query string on the wire. + */ + @Test + public void testInvoke_queryParamValuesReachTransportUnencoded_knownGap() throws Exception { + Map pathParams = new LinkedHashMap<>(); + pathParams.put("resource", "cards"); + Map queryParams = new LinkedHashMap<>(); + queryParams.put("q", "a&b=c"); + + controller.invoke(InvokeConnectionRequest.builder() + .method(RequestMethod.GET) + .pathParams(pathParams) + .queryParams(queryParams) + .build()); + + URL url = capture().url; + Assert.assertEquals("q=a&b=c", url.getQuery()); + Assert.assertEquals("one user param became two wire params", 2, url.getQuery().split("&").length); + } + + /** + * KNOWN GAP: path-param values are not percent-encoded, so a {@code /} in the value changes the + * request path on the wire. + */ + @Test + public void testInvoke_pathParamValuesReachTransportUnencoded_knownGap() throws Exception { + Map pathParams = new LinkedHashMap<>(); + pathParams.put("resource", "cards/123"); + + controller.invoke(InvokeConnectionRequest.builder() + .method(RequestMethod.GET) + .pathParams(pathParams) + .build()); + + Assert.assertEquals("/api/cards/123/details", capture().url.getPath()); + } +}