diff --git a/.cspell.json b/.cspell.json index abe743cb..757f0c48 100644 --- a/.cspell.json +++ b/.cspell.json @@ -92,7 +92,13 @@ "behaviours", "sanitisation", "recognised", - "unrecognised" + "unrecognised", + "nocreds", + "nodir", + "detok", + "qhdmceurtnlz", + "ngrok", + "obac" ], "languageSettings": [ { diff --git a/docs/migrate_to_v2.md b/docs/migrate_to_v2.md index 747076be..671210fd 100644 --- a/docs/migrate_to_v2.md +++ b/docs/migrate_to_v2.md @@ -277,6 +277,7 @@ The following instance methods have been renamed for consistency. The old names |---|---| | `skyflowClient.updateLogLevel(logLevel)` | `skyflowClient.setLogLevel(logLevel)` | | `TokenMode.getBYOT()` | `TokenMode.getByot()` | +| `DetokenizeRequest.builder().downloadURL(b)` | `DetokenizeRequest.builder().downloadUrl(b)` | --- diff --git a/pom.xml b/pom.xml index df4103e4..05124b67 100644 --- a/pom.xml +++ b/pom.xml @@ -184,6 +184,14 @@ 3.2.5 false + + @{argLine} + --add-opens java.base/java.lang=ALL-UNNAMED + --add-opens java.base/java.net=ALL-UNNAMED + --add-opens java.base/java.util=ALL-UNNAMED + --add-opens java.base/java.io=ALL-UNNAMED + --add-opens java.base/sun.net.www.protocol.https=ALL-UNNAMED + **/*Test.java **/*Tests.java diff --git a/samples/src/main/java/com/example/vault/deprecated/DetokenizeExample.java b/samples/src/main/java/com/example/vault/deprecated/DetokenizeExample.java new file mode 100644 index 00000000..f0971407 --- /dev/null +++ b/samples/src/main/java/com/example/vault/deprecated/DetokenizeExample.java @@ -0,0 +1,69 @@ +package com.example.vault.deprecated; + +import com.skyflow.Skyflow; +import com.skyflow.config.Credentials; +import com.skyflow.config.VaultConfig; +import com.skyflow.enums.Env; +import com.skyflow.enums.LogLevel; +import com.skyflow.enums.RedactionType; +import com.skyflow.errors.SkyflowException; +import com.skyflow.vault.tokens.DetokenizeData; +import com.skyflow.vault.tokens.DetokenizeRequest; +import com.skyflow.vault.tokens.DetokenizeResponse; + +import java.util.ArrayList; + +/** + * @deprecated Pre-v2.1 pattern. The {@code downloadURL()} builder method is deprecated. + * Use {@code downloadUrl()} instead (see {@link com.example.vault.DetokenizeExample}). + * + * This example is retained for reference during the deprecation window. + * {@code downloadURL()} still works but emits a runtime warning and will be removed in a future release. + */ +@Deprecated +public class DetokenizeExample { + @SuppressWarnings("deprecation") + public static void main(String[] args) throws SkyflowException { + // Step 1: Set up Skyflow credentials + Credentials credentials = new Credentials(); + credentials.setToken(""); // Replace with the actual bearer token + + // Step 2: Configure the vault + VaultConfig vaultConfig = new VaultConfig(); + vaultConfig.setVaultId(""); + vaultConfig.setClusterId(""); + vaultConfig.setEnv(Env.PROD); + vaultConfig.setCredentials(credentials); + + // Step 3: Set up credentials for the Skyflow client + Credentials skyflowCredentials = new Credentials(); + skyflowCredentials.setCredentialsString(""); + + // Step 4: Create a Skyflow client + Skyflow skyflowClient = Skyflow.builder() + .setLogLevel(LogLevel.ERROR) + .addVaultConfig(vaultConfig) + .addSkyflowCredentials(skyflowCredentials) + .build(); + + // Step 5: Detokenize with deprecated downloadURL() + // DEPRECATED: use downloadUrl(true) instead of downloadURL(true) + try { + ArrayList detokenizeData = new ArrayList<>(); + detokenizeData.add(new DetokenizeData("", RedactionType.MASKED)); + detokenizeData.add(new DetokenizeData("")); + + DetokenizeRequest request = DetokenizeRequest.builder() + .detokenizeData(detokenizeData) + .continueOnError(true) + .downloadURL(true) // @deprecated — use downloadUrl(true) + .build(); + + DetokenizeResponse response = skyflowClient.vault().detokenize(request); + System.out.println("Detokenize Response: " + response); + } catch (SkyflowException e) { + System.out.println("Error during detokenization:"); + e.printStackTrace(); + } + } +} diff --git a/samples/src/main/java/com/example/vault/deprecated/GetExample.java b/samples/src/main/java/com/example/vault/deprecated/GetExample.java new file mode 100644 index 00000000..50e2e5fa --- /dev/null +++ b/samples/src/main/java/com/example/vault/deprecated/GetExample.java @@ -0,0 +1,76 @@ +package com.example.vault.deprecated; + +import com.skyflow.Skyflow; +import com.skyflow.config.Credentials; +import com.skyflow.config.VaultConfig; +import com.skyflow.enums.Env; +import com.skyflow.enums.LogLevel; +import com.skyflow.enums.RedactionType; +import com.skyflow.errors.SkyflowException; +import com.skyflow.vault.data.GetRequest; +import com.skyflow.vault.data.GetResponse; + +import java.util.ArrayList; +import java.util.HashMap; + +/** + * @deprecated Pre-v2.1 pattern. The {@code "skyflow_id"} key in the response record map is deprecated. + * Use {@code "skyflowId"} instead (see {@link com.example.vault.GetExample}). + * + * This example is retained for reference during the deprecation window. + * Both {@code "skyflow_id"} and {@code "skyflowId"} are present in the response map until + * {@code "skyflow_id"} is removed in a future release. + */ +@Deprecated +public class GetExample { + public static void main(String[] args) throws SkyflowException { + // Step 1: Set up credentials + Credentials credentials = new Credentials(); + credentials.setCredentialsString(""); + + // Step 2: Configure the vault + VaultConfig vaultConfig = new VaultConfig(); + vaultConfig.setVaultId(""); + vaultConfig.setClusterId(""); + vaultConfig.setEnv(Env.PROD); + vaultConfig.setCredentials(credentials); + + // Step 3: Set up credentials for the Skyflow client + Credentials skyflowCredentials = new Credentials(); + skyflowCredentials.setCredentialsString(""); + + // Step 4: Create a Skyflow client + Skyflow skyflowClient = Skyflow.builder() + .setLogLevel(LogLevel.ERROR) + .addVaultConfig(vaultConfig) + .addSkyflowCredentials(skyflowCredentials) + .build(); + + // Example: Fetch records and read the Skyflow ID using the deprecated "skyflow_id" key + // DEPRECATED: the response map contains both "skyflow_id" and "skyflowId". + // Access "skyflowId" instead — "skyflow_id" will be removed in a future release. + try { + ArrayList ids = new ArrayList<>(); + ids.add(""); + + GetRequest request = GetRequest.builder() + .ids(ids) + .table("") + .redactionType(RedactionType.PLAIN_TEXT) + .build(); + + GetResponse response = skyflowClient.vault().get(request); + + // DEPRECATED: reading "skyflow_id" from the response map + for (HashMap record : response.getData()) { + String deprecatedId = (String) record.get("skyflow_id"); // @deprecated — use "skyflowId" + String preferredId = (String) record.get("skyflowId"); // preferred + System.out.println("skyflow_id (deprecated): " + deprecatedId); + System.out.println("skyflowId (preferred) : " + preferredId); + } + } catch (SkyflowException e) { + System.out.println("Error during fetch:"); + e.printStackTrace(); + } + } +} diff --git a/samples/src/main/java/com/example/vault/deprecated/UpdateExample.java b/samples/src/main/java/com/example/vault/deprecated/UpdateExample.java index aaead19c..f33a7606 100644 --- a/samples/src/main/java/com/example/vault/deprecated/UpdateExample.java +++ b/samples/src/main/java/com/example/vault/deprecated/UpdateExample.java @@ -13,14 +13,18 @@ import java.util.HashMap; /** - * @deprecated Pre-v2.1 pattern. The "skyflow_id" key in the data map is deprecated. - * Use "skyflowId" instead (see {@link com.example.vault.UpdateExample}). + * @deprecated Pre-v2.1 pattern. Demonstrates two deprecated APIs: + *
    + *
  • The {@code "skyflow_id"} key in the data map — use {@code "skyflowId"} instead.
  • + *
  • {@code updateLogLevel()} on the Skyflow client — use {@code setLogLevel()} instead.
  • + *
+ * See {@link com.example.vault.UpdateExample} for the current pattern. * - * This example is retained for reference during the deprecation window. - * "skyflow_id" still works but emits a runtime warning and will be removed in a future release. + * Both still work but emit runtime warnings and will be removed in a future release. */ @Deprecated public class UpdateExample { + @SuppressWarnings("deprecation") public static void main(String[] args) throws SkyflowException { // Step 1: Set up credentials for the first vault configuration Credentials credentials = new Credentials(); @@ -38,11 +42,12 @@ public static void main(String[] args) throws SkyflowException { skyflowCredentials.setCredentialsString(""); // Replace with the actual credentials string // Step 4: Create a Skyflow client and add vault configurations + // DEPRECATED: use setLogLevel() instead of updateLogLevel() Skyflow skyflowClient = Skyflow.builder() - .setLogLevel(LogLevel.ERROR) // Enable debugging for detailed logs - .addVaultConfig(vaultConfig) // Add the vault configuration - .addSkyflowCredentials(skyflowCredentials) // Add general Skyflow credentials + .addVaultConfig(vaultConfig) + .addSkyflowCredentials(skyflowCredentials) .build(); + skyflowClient.updateLogLevel(LogLevel.ERROR); // @deprecated — use setLogLevel(LogLevel.ERROR) // Step 5: Update records with TokenMode enabled // DEPRECATED: use "skyflowId" key instead of "skyflow_id" diff --git a/src/main/java/com/skyflow/ConnectionClient.java b/src/main/java/com/skyflow/ConnectionClient.java index 7cb10713..d67122ad 100644 --- a/src/main/java/com/skyflow/ConnectionClient.java +++ b/src/main/java/com/skyflow/ConnectionClient.java @@ -86,6 +86,8 @@ private void prioritiseCredentials() throws SkyflowException { } catch (DotenvException e) { throw new SkyflowException(ErrorCode.INVALID_INPUT.getCode(), ErrorMessage.EmptyCredentials.getMessage()); + } catch (SkyflowException e) { + throw e; } catch (Exception e) { throw new RuntimeException(e); } diff --git a/src/main/java/com/skyflow/VaultClient.java b/src/main/java/com/skyflow/VaultClient.java index b9cdfd98..1d5e5d74 100644 --- a/src/main/java/com/skyflow/VaultClient.java +++ b/src/main/java/com/skyflow/VaultClient.java @@ -876,6 +876,8 @@ private void prioritiseCredentials() throws SkyflowException { } catch (DotenvException e) { throw new SkyflowException(ErrorCode.INVALID_INPUT.getCode(), ErrorMessage.EmptyCredentials.getMessage()); + } catch (SkyflowException e) { + throw e; } catch (Exception e) { throw new RuntimeException(e); } diff --git a/src/main/java/com/skyflow/logs/InfoLogs.java b/src/main/java/com/skyflow/logs/InfoLogs.java index af16698a..e747bfa2 100644 --- a/src/main/java/com/skyflow/logs/InfoLogs.java +++ b/src/main/java/com/skyflow/logs/InfoLogs.java @@ -101,7 +101,10 @@ public enum InfoLogs { DEPRECATED_SKYFLOW_ID_REQUEST_KEY("[DEPRECATED] Request data key 'skyflow_id' is deprecated and will be removed in an upcoming release. Use 'skyflowId' instead."), DEPRECATED_DOWNLOAD_URL("[DEPRECATED] Method 'downloadURL()' is deprecated and will be removed in an upcoming release. Use 'downloadUrl()' instead."), DEPRECATED_GET_BYOT("[DEPRECATED] Method 'getBYOT()' is deprecated and will be removed in an upcoming release. Use 'getByot()' instead."), - DEPRECATED_UPDATE_LOG_LEVEL("[DEPRECATED] Method 'updateLogLevel()' is deprecated and will be removed in an upcoming release. Use 'setLogLevel()' instead."); + DEPRECATED_UPDATE_LOG_LEVEL("[DEPRECATED] Method 'updateLogLevel()' is deprecated and will be removed in an upcoming release. Use 'setLogLevel()' instead."), + DEPRECATED_CREDENTIAL_CLIENT_ID("[DEPRECATED] Credential field 'clientID' is deprecated and will be removed in an upcoming release. Use 'clientId' instead."), + DEPRECATED_CREDENTIAL_KEY_ID("[DEPRECATED] Credential field 'keyID' is deprecated and will be removed in an upcoming release. Use 'keyId' instead."), + DEPRECATED_CREDENTIAL_TOKEN_URI("[DEPRECATED] Credential field 'tokenURI' is deprecated and will be removed in an upcoming release. Use 'tokenUri' instead."); diff --git a/src/main/java/com/skyflow/serviceaccount/util/BearerToken.java b/src/main/java/com/skyflow/serviceaccount/util/BearerToken.java index d2a57fb4..ad7cae30 100644 --- a/src/main/java/com/skyflow/serviceaccount/util/BearerToken.java +++ b/src/main/java/com/skyflow/serviceaccount/util/BearerToken.java @@ -108,6 +108,9 @@ private static V1GetAuthTokenResponse getBearerTokenFromCredentials( JsonElement clientId = credentials.get("clientId"); if (clientId == null) { clientId = credentials.get("clientID"); + if (clientId != null) { + LogUtil.printWarningLog(InfoLogs.DEPRECATED_CREDENTIAL_CLIENT_ID.getLog()); + } } if (clientId == null) { LogUtil.printErrorLog(ErrorLogs.CLIENT_ID_IS_REQUIRED.getLog()); @@ -117,6 +120,9 @@ private static V1GetAuthTokenResponse getBearerTokenFromCredentials( JsonElement keyId = credentials.get("keyId"); if (keyId == null) { keyId = credentials.get("keyID"); + if (keyId != null) { + LogUtil.printWarningLog(InfoLogs.DEPRECATED_CREDENTIAL_KEY_ID.getLog()); + } } if (keyId == null) { LogUtil.printErrorLog(ErrorLogs.KEY_ID_IS_REQUIRED.getLog()); @@ -126,6 +132,9 @@ private static V1GetAuthTokenResponse getBearerTokenFromCredentials( JsonElement tokenUri = credentials.get("tokenUri"); if (tokenUri == null) { tokenUri = credentials.get("tokenURI"); + if (tokenUri != null) { + LogUtil.printWarningLog(InfoLogs.DEPRECATED_CREDENTIAL_TOKEN_URI.getLog()); + } } if (tokenUri == null) { LogUtil.printErrorLog(ErrorLogs.TOKEN_URI_IS_REQUIRED.getLog()); diff --git a/src/main/java/com/skyflow/serviceaccount/util/SignedDataTokens.java b/src/main/java/com/skyflow/serviceaccount/util/SignedDataTokens.java index efb6600e..b909e45b 100644 --- a/src/main/java/com/skyflow/serviceaccount/util/SignedDataTokens.java +++ b/src/main/java/com/skyflow/serviceaccount/util/SignedDataTokens.java @@ -109,6 +109,9 @@ private static List generateSignedTokensFromCredentials JsonElement clientId = credentials.get("clientId"); if (clientId == null) { clientId = credentials.get("clientID"); + if (clientId != null) { + LogUtil.printWarningLog(InfoLogs.DEPRECATED_CREDENTIAL_CLIENT_ID.getLog()); + } } if (clientId == null) { LogUtil.printErrorLog(ErrorLogs.CLIENT_ID_IS_REQUIRED.getLog()); @@ -118,6 +121,9 @@ private static List generateSignedTokensFromCredentials JsonElement keyId = credentials.get("keyId"); if (keyId == null) { keyId = credentials.get("keyID"); + if (keyId != null) { + LogUtil.printWarningLog(InfoLogs.DEPRECATED_CREDENTIAL_KEY_ID.getLog()); + } } if (keyId == null) { LogUtil.printErrorLog(ErrorLogs.KEY_ID_IS_REQUIRED.getLog()); diff --git a/src/main/java/com/skyflow/utils/validations/Validations.java b/src/main/java/com/skyflow/utils/validations/Validations.java index 3bd75626..0c36445c 100644 --- a/src/main/java/com/skyflow/utils/validations/Validations.java +++ b/src/main/java/com/skyflow/utils/validations/Validations.java @@ -968,7 +968,7 @@ public static void validateDeidentifyFileRequest(DeidentifyFileRequest request) if (request.getWaitTime() != null && request.getWaitTime() <= 0) { throw new SkyflowException(ErrorCode.INVALID_INPUT.getCode(), ErrorMessage.InvalidWaitTime.getMessage()); } - if(request.getWaitTime() > 64) { + if(request.getWaitTime() != null && request.getWaitTime() > 64) { throw new SkyflowException(ErrorCode.INVALID_INPUT.getCode(), ErrorMessage.WaitTimeExceedsLimit.getMessage()); } } diff --git a/src/main/java/com/skyflow/vault/controller/VaultController.java b/src/main/java/com/skyflow/vault/controller/VaultController.java index 980760ae..1812b83b 100644 --- a/src/main/java/com/skyflow/vault/controller/VaultController.java +++ b/src/main/java/com/skyflow/vault/controller/VaultController.java @@ -238,7 +238,7 @@ public InsertResponse insert(InsertRequest insertRequest) throws SkyflowExceptio return new InsertResponse(null, errorFields.isEmpty() ? null : errorFields); } if (errorFields.isEmpty()) { - return new InsertResponse(insertedFields.isEmpty() ? null : insertedFields, null); + return new InsertResponse(insertedFields, null); } return new InsertResponse(insertedFields, errorFields); } diff --git a/src/test/java/com/skyflow/ConnectionClientDotenvTests.java b/src/test/java/com/skyflow/ConnectionClientDotenvTests.java new file mode 100644 index 00000000..4916f628 --- /dev/null +++ b/src/test/java/com/skyflow/ConnectionClientDotenvTests.java @@ -0,0 +1,83 @@ +package com.skyflow; + +import com.skyflow.config.ConnectionConfig; +import com.skyflow.errors.ErrorMessage; +import com.skyflow.errors.SkyflowException; +import com.skyflow.utils.Constants; +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; + +import java.io.File; +import java.io.FileWriter; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Paths; + +/** + * Tests for ConnectionClient's prioritiseCredentials dotenv path. + * + * These tests write a temporary .env file to exercise the code path where + * no ConnectionConfig credentials and no common credentials are set, so the + * code falls through to read from a .env file. + */ +public class ConnectionClientDotenvTests { + + private static final String ENV_FILE = ".env"; + private byte[] originalEnvContent; + + @Before + public void saveEnvFileState() throws IOException { + File f = new File(ENV_FILE); + originalEnvContent = f.exists() ? Files.readAllBytes(Paths.get(ENV_FILE)) : null; + } + + @After + public void restoreEnvFile() throws IOException { + if (originalEnvContent != null) { + Files.write(Paths.get(ENV_FILE), originalEnvContent); + } else { + Files.deleteIfExists(Paths.get(ENV_FILE)); + } + } + + private ConnectionClient buildClientWithNoCreds(String id) { + ConnectionConfig config = new ConnectionConfig(); + config.setConnectionId(id); + config.setConnectionUrl("https://test.dotenv.url"); + // No credentials on config, no commonCredentials + return new ConnectionClient(config, null); + } + + @Test + public void testPrioritiseCredentials_dotenvReturnsCredentials_setsCredentials() throws Exception { + // Write a .env file with a valid credentials string value + try (FileWriter fw = new FileWriter(ENV_FILE)) { + fw.write(Constants.ENV_CREDENTIALS_KEY_NAME + "={\"token\":\"env-token-value\"}\n"); + } + + ConnectionClient client = buildClientWithNoCreds("dotenv-valid-1"); + // updateConnectionConfig calls prioritiseCredentials which reads from .env + client.updateConnectionConfig(client.getConnectionConfig()); + } + + @Test + public void testPrioritiseCredentials_dotenvReturnsNullKey_throwsSkyflowException() throws Exception { + // Write a .env file WITHOUT the SKYFLOW_CREDENTIALS key + try (FileWriter fw = new FileWriter(ENV_FILE)) { + fw.write("SOME_OTHER_KEY=some_value\n"); + } + + ConnectionClient client = buildClientWithNoCreds("dotenv-null-1"); + // Null sysCredentials → SkyflowException thrown directly + try { + client.updateConnectionConfig(client.getConnectionConfig()); + Assert.fail("Should have thrown SkyflowException"); + } catch (SkyflowException e) { + Assert.assertTrue(e.getMessage().contains(ErrorMessage.EmptyCredentials.getMessage())); + } catch (RuntimeException e) { + Assert.fail("Expected direct SkyflowException, not RuntimeException wrapping it"); + } + } +} diff --git a/src/test/java/com/skyflow/ConnectionClientTests.java b/src/test/java/com/skyflow/ConnectionClientTests.java index 4a69120c..c24bb20a 100644 --- a/src/test/java/com/skyflow/ConnectionClientTests.java +++ b/src/test/java/com/skyflow/ConnectionClientTests.java @@ -2,6 +2,7 @@ import com.skyflow.config.ConnectionConfig; import com.skyflow.config.Credentials; +import com.skyflow.errors.SkyflowException; import io.github.cdimascio.dotenv.Dotenv; import org.junit.Assert; import org.junit.BeforeClass; @@ -92,4 +93,96 @@ public void testSetBearerTokenWithEnvCredentials() { Assert.fail(INVALID_EXCEPTION_THROWN); } } + + @Test + public void testSetBearerToken_withApiKey_setsAndReusesApiKey() { + try { + Credentials creds = new Credentials(); + creds.setApiKey("sky-ab123-abcd1234cdef1234abcd4321cdef4321"); + ConnectionConfig config = new ConnectionConfig(); + config.setConnectionId("isolated-apikey-1"); + config.setConnectionUrl("https://test.isolated.url"); + config.setCredentials(creds); + ConnectionClient client = new ConnectionClient(config, null); + + // First call: apiKey == null → setApiKey() sets it + client.setBearerToken(); + Assert.assertEquals("sky-ab123-abcd1234cdef1234abcd4321cdef4321", client.apiKey); + + // Second call: apiKey != null → setApiKey() logs REUSE_API_KEY (line 60) + client.setBearerToken(); + Assert.assertEquals("sky-ab123-abcd1234cdef1234abcd4321cdef4321", client.apiKey); + } catch (Exception e) { + Assert.fail(INVALID_EXCEPTION_THROWN); + } + } + + @Test + public void testSetBearerToken_withValidNonExpiredToken_reusesBearerToken() { + try { + // far-future JWT: base64({"exp":9999999999}) = eyJleHAiOjk5OTk5OTk5OTl9 — never expires + Credentials creds = new Credentials(); + creds.setToken("x.eyJleHAiOjk5OTk5OTk5OTl9.y"); + ConnectionConfig config = new ConnectionConfig(); + config.setConnectionId("isolated-token-1"); + config.setConnectionUrl("https://test.isolated.url"); + config.setCredentials(creds); + ConnectionClient client = new ConnectionClient(config, null); + + // First call: this.token == null → Token.isExpired(null)=true → generates token from creds.getToken() + client.setBearerToken(); + Assert.assertEquals("x.eyJleHAiOjk5OTk5OTk5OTl9.y", client.token); + + // Second call: token not null, not empty, not expired → REUSE_BEARER_TOKEN else branch (line 52) + client.setBearerToken(); + Assert.assertEquals("x.eyJleHAiOjk5OTk5OTk5OTl9.y", client.token); + } catch (Exception e) { + Assert.fail(INVALID_EXCEPTION_THROWN); + } + } + + @Test + public void testPrioritiseCredentials_credentialChange_resetsToken() { + try { + Credentials credentialsA = new Credentials(); + credentialsA.setToken("x.eyJleHAiOjk5OTk5OTk5OTl9.y"); + ConnectionConfig config = new ConnectionConfig(); + config.setConnectionId("isolated-change-1"); + config.setConnectionUrl("https://test.isolated.url"); + config.setCredentials(credentialsA); + ConnectionClient client = new ConnectionClient(config, null); + + client.updateConnectionConfig(config); // sets finalCredentials = credentialsA (original=null → no reset) + client.token = "cached-token-value"; // simulate previously obtained bearer token + + // Change to different credentials object + Credentials credentialsB = new Credentials(); + credentialsB.setToken("different-token"); + config.setCredentials(credentialsB); + + client.updateConnectionConfig(config); // original=A, new=B → !A.equals(B) → reset (lines 83-84) + Assert.assertNull(client.token); + Assert.assertNull(client.apiKey); + } catch (Exception e) { + Assert.fail(INVALID_EXCEPTION_THROWN); + } + } + + @Test + public void testSetBearerToken_noCredentials_throwsEmptyCredentials() { + ConnectionConfig config = new ConnectionConfig(); + config.setConnectionId("isolated-nocreds-1"); + config.setConnectionUrl("https://test.isolated.url"); + // No credentials on config, no commonCredentials + ConnectionClient client = new ConnectionClient(config, null); + try { + client.setBearerToken(); + Assert.fail("Should have thrown SkyflowException"); + } catch (SkyflowException e) { + // SkyflowException expected — message varies by environment + // (EmptyCredentials when no .env, or credential error when .env provides creds) + } catch (Exception e) { + Assert.fail("Expected SkyflowException, got: " + e.getClass().getName()); + } + } } \ No newline at end of file diff --git a/src/test/java/com/skyflow/SkyflowTests.java b/src/test/java/com/skyflow/SkyflowTests.java index 12e83e06..983419a7 100644 --- a/src/test/java/com/skyflow/SkyflowTests.java +++ b/src/test/java/com/skyflow/SkyflowTests.java @@ -685,4 +685,163 @@ public void testDetectMethodWithInvalidVaultId() { Assert.assertEquals(ErrorMessage.VaultIdNotInConfigList.getMessage(), e.getMessage()); } } + + @Test + public void testUpdateVaultConfig_withNewClusterIdAndCredentials_updatesAllFields() { + try { + VaultConfig config = new VaultConfig(); + config.setVaultId(vaultID); + config.setClusterId(clusterID); + config.setEnv(Env.DEV); + Credentials creds = new Credentials(); + creds.setToken(token); + config.setCredentials(creds); + Skyflow skyflowClient = Skyflow.builder().addVaultConfig(config).build(); + + // Update with a new non-null clusterId and new non-null credentials — covers + // the non-null (true) branches for all three ternaries in findAndUpdateVaultConfig + Credentials newCreds = new Credentials(); + newCreds.setToken("updated-token-value"); + VaultConfig update = new VaultConfig(); + update.setVaultId(vaultID); + update.setClusterId(newClusterID); + update.setEnv(Env.PROD); + update.setCredentials(newCreds); + skyflowClient.updateVaultConfig(update); + Assert.assertEquals(newClusterID, skyflowClient.getVaultConfig(vaultID).getClusterId()); + Assert.assertEquals(Env.PROD, skyflowClient.getVaultConfig(vaultID).getEnv()); + Assert.assertEquals("updated-token-value", skyflowClient.getVaultConfig(vaultID).getCredentials().getToken()); + } catch (SkyflowException e) { + Assert.fail(INVALID_EXCEPTION_THROWN); + } + } + + @Test + public void testUpdateConnectionConfig_withNewCredentials_updatesCredentials() { + try { + ConnectionConfig config = new ConnectionConfig(); + config.setConnectionId(connectionID); + config.setConnectionUrl(connectionURL); + Credentials oldCreds = new Credentials(); + oldCreds.setToken(token); + config.setCredentials(oldCreds); + Skyflow skyflowClient = Skyflow.builder().addConnectionConfig(config).build(); + + // Update with new non-null credentials and new non-null connectionUrl — covers + // the non-null (true) branches for both ternaries in findAndUpdateConnectionConfig + Credentials newCreds = new Credentials(); + newCreds.setToken("new-token-value"); + ConnectionConfig update = new ConnectionConfig(); + update.setConnectionId(connectionID); + update.setConnectionUrl(newConnectionURL); + update.setCredentials(newCreds); + skyflowClient.updateConnectionConfig(update); + Assert.assertEquals("new-token-value", skyflowClient.getConnectionConfig(connectionID).getCredentials().getToken()); + Assert.assertEquals(newConnectionURL, skyflowClient.getConnectionConfig(connectionID).getConnectionUrl()); + } catch (SkyflowException e) { + Assert.fail(INVALID_EXCEPTION_THROWN); + } + } + + @Test + public void testUpdateVaultConfig_withNullEnv_fallsBackToPreviousEnv() { + // VaultConfig's constructor defaults env=PROD so getEnv() is never null via normal API. + // Use an anonymous subclass to make getEnv() return null, exercising the false branch + // of `vaultConfig.getEnv() != null` in findAndUpdateVaultConfig. + try { + Credentials creds = new Credentials(); + creds.setToken(token); + VaultConfig initial = new VaultConfig(); + initial.setVaultId(vaultID); + initial.setClusterId(clusterID); + initial.setEnv(Env.SANDBOX); + initial.setCredentials(creds); + Skyflow skyflowClient = Skyflow.builder().addVaultConfig(initial).build(); + + VaultConfig updateWithNullEnv = new VaultConfig() { + @Override public Env getEnv() { return null; } + }; + updateWithNullEnv.setVaultId(vaultID); + updateWithNullEnv.setClusterId(clusterID); + updateWithNullEnv.setCredentials(creds); + + skyflowClient.updateVaultConfig(updateWithNullEnv); + // env falls back to previous (SANDBOX) + Assert.assertEquals(Env.SANDBOX, skyflowClient.getVaultConfig(vaultID).getEnv()); + } catch (SkyflowException e) { + Assert.fail(INVALID_EXCEPTION_THROWN); + } + } + + @Test + public void testFindAndUpdateVaultConfig_withNullClusterId_fallsBackToPreviousClusterId() { + // Validation enforces non-null clusterId, so the false branch of + // `vaultConfig.getClusterId() != null` in findAndUpdateVaultConfig is unreachable + // via the normal flow. Call the private method directly via reflection. + try { + Credentials creds = new Credentials(); + creds.setToken(token); + VaultConfig initial = new VaultConfig(); + initial.setVaultId(vaultID); + initial.setClusterId(clusterID); + initial.setEnv(Env.DEV); + initial.setCredentials(creds); + Skyflow skyflowClient = Skyflow.builder().addVaultConfig(initial).build(); + + java.lang.reflect.Field builderField = Skyflow.class.getDeclaredField("builder"); + builderField.setAccessible(true); + Object builder = builderField.get(skyflowClient); + + VaultConfig nullClusterConfig = new VaultConfig(); + nullClusterConfig.setVaultId(vaultID); + // Override clusterId field to null via reflection (setter enforces non-null) + java.lang.reflect.Field clusterIdField = VaultConfig.class.getDeclaredField("clusterId"); + clusterIdField.setAccessible(true); + clusterIdField.set(nullClusterConfig, null); + + java.lang.reflect.Method method = builder.getClass().getDeclaredMethod( + "findAndUpdateVaultConfig", VaultConfig.class); + method.setAccessible(true); + VaultConfig result = (VaultConfig) method.invoke(builder, nullClusterConfig); + + Assert.assertEquals(clusterID, result.getClusterId()); + } catch (SkyflowException e) { + Assert.fail(INVALID_EXCEPTION_THROWN); + } catch (Exception e) { + Assert.fail("Reflection failed: " + e.getMessage()); + } + } + + @Test + public void testFindAndUpdateConnectionConfig_withNullConnectionUrl_fallsBackToPreviousUrl() { + // `findAndUpdateConnectionConfig` has a ternary for connectionUrl that falls back + // to previousConfig.getConnectionUrl() when the incoming url is null. + // Since validation enforces non-null url, we call the private method directly + // via reflection to cover the false branch. + try { + ConnectionConfig initial = new ConnectionConfig(); + initial.setConnectionId(connectionID); + initial.setConnectionUrl(connectionURL); + Skyflow skyflowClient = Skyflow.builder().addConnectionConfig(initial).build(); + + java.lang.reflect.Field builderField = Skyflow.class.getDeclaredField("builder"); + builderField.setAccessible(true); + Object builder = builderField.get(skyflowClient); + + ConnectionConfig nullUrlConfig = new ConnectionConfig(); + nullUrlConfig.setConnectionId(connectionID); + // connectionUrl not set → remains null + + java.lang.reflect.Method method = builder.getClass().getDeclaredMethod( + "findAndUpdateConnectionConfig", ConnectionConfig.class); + method.setAccessible(true); + ConnectionConfig result = (ConnectionConfig) method.invoke(builder, nullUrlConfig); + + Assert.assertEquals(connectionURL, result.getConnectionUrl()); + } catch (SkyflowException e) { + Assert.fail(INVALID_EXCEPTION_THROWN); + } catch (Exception e) { + Assert.fail("Reflection failed: " + e.getMessage()); + } + } } diff --git a/src/test/java/com/skyflow/VaultClientDotenvTests.java b/src/test/java/com/skyflow/VaultClientDotenvTests.java new file mode 100644 index 00000000..1a54b13d --- /dev/null +++ b/src/test/java/com/skyflow/VaultClientDotenvTests.java @@ -0,0 +1,103 @@ +package com.skyflow; + +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.utils.Constants; +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; + +import java.io.File; +import java.io.FileWriter; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Paths; + +/** + * Tests for VaultClient's prioritiseCredentials dotenv path. + * + * These tests write a temporary .env file to exercise the code path where + * no VaultConfig credentials and no common credentials are set, so the code + * falls through to read from a .env file. + */ +public class VaultClientDotenvTests { + + private static final String ENV_FILE = ".env"; + private byte[] originalEnvContent; + + @Before + public void saveEnvFileState() throws IOException { + File f = new File(ENV_FILE); + originalEnvContent = f.exists() ? Files.readAllBytes(Paths.get(ENV_FILE)) : null; + } + + @After + public void restoreEnvFile() throws IOException { + if (originalEnvContent != null) { + Files.write(Paths.get(ENV_FILE), originalEnvContent); + } else { + Files.deleteIfExists(Paths.get(ENV_FILE)); + } + } + + private VaultClient buildClientWithNoCreds(String vaultId, String clusterId) { + VaultConfig config = new VaultConfig(); + config.setVaultId(vaultId); + config.setClusterId(clusterId); + config.setEnv(Env.DEV); + // No credentials set + return new VaultClient(config, null); + } + + /** + * Covers the dotenv success path: Dotenv.load() succeeds and returns a + * non-null credentials string, so finalCredentials is set via + * credentialsString. Lines ~862-870 of VaultClient.java. + */ + @Test + public void testPrioritiseCredentials_dotenvReturnsCredentials_setsCredentials() throws Exception { + // Write a .env file with a valid credentials string value + try (FileWriter fw = new FileWriter(ENV_FILE)) { + fw.write(Constants.ENV_CREDENTIALS_KEY_NAME + "={\"token\":\"env-token-value\"}\n"); + } + + VaultClient client = buildClientWithNoCreds("dotenv-vault-1", "cluster1"); + // updateVaultConfig() calls prioritiseCredentials() which reads from .env + // Should not throw since sysCredentials is non-null + client.updateVaultConfig(); + + // finalCredentials should be set with credentials string + java.lang.reflect.Field field = VaultClient.class.getDeclaredField("finalCredentials"); + field.setAccessible(true); + Credentials finalCreds = (Credentials) field.get(client); + Assert.assertNotNull(finalCreds); + Assert.assertEquals("{\"token\":\"env-token-value\"}", finalCreds.getCredentialsString()); + } + + /** + * Covers the path where dotenv loads but the key is absent (returns null), + * causing SkyflowException(EmptyCredentials) to be thrown directly. + * Lines ~864-876 of VaultClient.java. + */ + @Test + public void testPrioritiseCredentials_dotenvKeyMissing_throwsSkyflowException() throws Exception { + // Write a .env file WITHOUT the SKYFLOW_CREDENTIALS key + try (FileWriter fw = new FileWriter(ENV_FILE)) { + fw.write("SOME_OTHER_KEY=some_value\n"); + } + + VaultClient client = buildClientWithNoCreds("dotenv-vault-2", "cluster2"); + try { + client.updateVaultConfig(); + Assert.fail("Should have thrown SkyflowException"); + } catch (SkyflowException e) { + Assert.assertTrue(e.getMessage().contains(ErrorMessage.EmptyCredentials.getMessage())); + } catch (RuntimeException e) { + Assert.fail("Expected direct SkyflowException, not RuntimeException wrapping it"); + } + } +} diff --git a/src/test/java/com/skyflow/VaultClientTests.java b/src/test/java/com/skyflow/VaultClientTests.java index f3e9f738..d98f5964 100644 --- a/src/test/java/com/skyflow/VaultClientTests.java +++ b/src/test/java/com/skyflow/VaultClientTests.java @@ -29,13 +29,23 @@ import com.skyflow.vault.tokens.DetokenizeData; import com.skyflow.vault.tokens.DetokenizeRequest; import com.skyflow.vault.tokens.TokenizeRequest; +import com.skyflow.vault.data.FileUploadRequest; import io.github.cdimascio.dotenv.Dotenv; +import okhttp3.MediaType; +import okhttp3.OkHttpClient; +import okhttp3.Protocol; +import okhttp3.Request; +import okhttp3.Response; +import okhttp3.ResponseBody; import org.junit.Assert; import org.junit.BeforeClass; import org.junit.Test; +import org.mockito.Mockito; import java.io.File; import java.util.*; +import java.util.Arrays; +import java.util.Collections; public class VaultClientTests { private static final String INVALID_EXCEPTION_THROWN = "Should not have thrown any exception"; @@ -168,26 +178,6 @@ public void testGetDetokenizePayload() { } } - @Test - public void testGetDetokenizePayloadWithNewDownloadUrl() { - try { - DetokenizeData detokenizeDataRecord1 = new DetokenizeData(token); - detokenizeData.clear(); - detokenizeData.add(detokenizeDataRecord1); - DetokenizeRequest detokenizeRequest = DetokenizeRequest.builder() - .detokenizeData(detokenizeData) - .downloadUrl(true) // new form - .continueOnError(false) - .build(); - V1DetokenizePayload payload = vaultClient.getDetokenizePayload(detokenizeRequest); - Assert.assertTrue("new downloadUrl() should be reflected in payload", payload.getDownloadUrl().get()); - Assert.assertTrue("new getDownloadUrl() should return true", detokenizeRequest.getDownloadUrl()); - Assert.assertTrue("deprecated getDownloadURL() should return same value", detokenizeRequest.getDownloadURL()); - } catch (Exception e) { - Assert.fail(INVALID_EXCEPTION_THROWN); - } - } - @Test public void testGetBulkInsertRequestBody() { try { @@ -948,4 +938,317 @@ private void setPrivateField(Object obj, String fieldName, Object value) throws field.setAccessible(true); field.set(obj, value); } + + @Test + public void testGetFileForFileUpload_withFileObject() { + try { + java.io.File fileObj = java.io.File.createTempFile("upload-test", ".txt"); + fileObj.deleteOnExit(); + FileUploadRequest request = FileUploadRequest.builder() + .fileObject(fileObj) + .table("test_table") + .columnName("test_col") + .build(); + java.io.File result = vaultClient.getFileForFileUpload(request); + Assert.assertEquals(fileObj, result); + } catch (Exception e) { + Assert.fail("Should not have thrown: " + e.getMessage()); + } + } + + @Test + public void testSetBearerToken_validNonExpiredToken_reusesToken() { + try { + // far-future JWT: header.payload.sig where payload base64 decodes to {"exp":9999999999} + Credentials creds = new Credentials(); + creds.setToken("x.eyJleHAiOjk5OTk5OTk5OTl9.y"); + VaultConfig config = new VaultConfig(); + config.setVaultId(vaultID); + config.setClusterId(clusterID); + config.setEnv(com.skyflow.enums.Env.DEV); + config.setCredentials(creds); + VaultClient freshClient = new VaultClient(config, null); + + // First call: token=null → generates from creds.getToken() + freshClient.setBearerToken(); + Assert.assertEquals("x.eyJleHAiOjk5OTk5OTk5OTl9.y", getPrivateField(freshClient, "token")); + + // Second call: token valid, not expired → REUSE_BEARER_TOKEN else branch + freshClient.setBearerToken(); + Assert.assertEquals("x.eyJleHAiOjk5OTk5OTk5OTl9.y", getPrivateField(freshClient, "token")); + } catch (Exception e) { + Assert.fail("Should not have thrown: " + e.getMessage()); + } + } + + @Test + public void testGetDeidentifyImageRequest_withMaskingMethod() { + try { + java.io.File file = new java.io.File("test.jpg"); + FileInput fileInput = FileInput.builder().file(file).build(); + List entities = Arrays.asList(DetectEntities.NAME); + TokenFormat tokenFormat = TokenFormat.builder().entityOnly(entities).build(); + + DeidentifyFileRequest request = DeidentifyFileRequest.builder() + .file(fileInput) + .entities(entities) + .tokenFormat(tokenFormat) + .maskingMethod(MaskingMethod.BLACKBOX) + .outputProcessedImage(true) + .build(); + + DeidentifyFileImageRequestDeidentifyImage imageRequest = + vaultClient.getDeidentifyImageRequest(request, vaultID, "base64content", "jpg"); + + Assert.assertNotNull(imageRequest); + Assert.assertTrue(imageRequest.getMaskingMethod().isPresent()); + } catch (Exception e) { + Assert.fail("Should not have thrown: " + e.getMessage()); + } + } + + @Test + public void testGetDeIdentifyTextResponse_withEntityScores() { + Locations location = Locations.builder() + .startIndex(0) + .endIndex(5) + .startIndexProcessed(0) + .endIndexProcessed(5) + .build(); + + Map scores = new HashMap<>(); + scores.put("EMAIL_ADDRESS", 0.95); + + StringResponseEntities entity = StringResponseEntities.builder() + .location(location) + .token("tok") + .value("val") + .entityType("EMAIL_ADDRESS") + .entityScores(scores) + .build(); + + DeidentifyStringResponse deidentifyResponse = DeidentifyStringResponse.builder() + .entities(Collections.singletonList(entity)) + .processedText("processed text") + .wordCount(2) + .characterCount(13) + .build(); + + DeidentifyTextResponse result = vaultClient.getDeIdentifyTextResponse(deidentifyResponse); + + Assert.assertNotNull(result); + Assert.assertEquals(1, result.getEntities().size()); + // Entity scores map lambda was invoked → getScores() should have the score + Assert.assertEquals(0.95, result.getEntities().get(0).getScores().get("EMAIL_ADDRESS"), 0.001); + } + + @Test + public void testPrioritiseCredentials_credentialChange_resetsTokenAndApiKey() { + try { + Credentials credentialsA = new Credentials(); + credentialsA.setToken("x.eyJleHAiOjk5OTk5OTk5OTl9.y"); + VaultConfig config = new VaultConfig(); + config.setVaultId("isolated-vault-change"); + config.setClusterId(clusterID); + config.setEnv(com.skyflow.enums.Env.DEV); + config.setCredentials(credentialsA); + VaultClient freshClient = new VaultClient(config, null); + + freshClient.updateVaultConfig(); // sets finalCredentials = credentialsA + setPrivateField(freshClient, "token", "cached-token"); // simulate prior auth + + Credentials credentialsB = new Credentials(); + credentialsB.setToken("other-token"); + config.setCredentials(credentialsB); + + freshClient.updateVaultConfig(); // original=A, new=B → different → reset token/apiKey + Assert.assertNull(getPrivateField(freshClient, "token")); + Assert.assertNull(getPrivateField(freshClient, "apiKey")); + } catch (Exception e) { + Assert.fail("Should not have thrown: " + e.getMessage()); + } + } + + @Test + public void testSetBearerToken_noCredentials_throwsEmptyCredentials() { + VaultConfig config = new VaultConfig(); + config.setVaultId("isolated-vault-nocreds"); + config.setClusterId(clusterID); + config.setEnv(com.skyflow.enums.Env.DEV); + // No credentials — will hit dotenv path → DotenvException → SkyflowException(EmptyCredentials) + VaultClient freshClient = new VaultClient(config, null); + try { + freshClient.setBearerToken(); + Assert.fail("Should have thrown SkyflowException"); + } catch (SkyflowException e) { + // SkyflowException expected — message varies by environment + // (EmptyCredentials when no .env, or credential error when .env provides creds) + } catch (Exception e) { + Assert.fail("Expected SkyflowException, got: " + e.getClass().getName() + ": " + e.getMessage()); + } + } + + @Test + public void testUpdateExecutorInHTTP_interceptorAddsAuthorizationHeader() { + try { + Credentials creds = new Credentials(); + creds.setToken("x.eyJleHAiOjk5OTk5OTk5OTl9.y"); + VaultConfig config = new VaultConfig(); + config.setVaultId("isolated-vault-http"); + config.setClusterId(clusterID); + config.setEnv(com.skyflow.enums.Env.DEV); + config.setCredentials(creds); + VaultClient freshClient = new VaultClient(config, null); + + freshClient.setBearerToken(); // triggers updateExecutorInHTTP → creates sharedHttpClient with interceptor + + // Access sharedHttpClient via reflection + java.lang.reflect.Field field = VaultClient.class.getDeclaredField("sharedHttpClient"); + field.setAccessible(true); + OkHttpClient httpClient = (OkHttpClient) field.get(freshClient); + Assert.assertNotNull(httpClient); + Assert.assertFalse(httpClient.interceptors().isEmpty()); + + // Get the interceptor (our lambda) + okhttp3.Interceptor interceptor = httpClient.interceptors().get(0); + + // Mock Chain and invoke the interceptor + okhttp3.Interceptor.Chain mockChain = Mockito.mock(okhttp3.Interceptor.Chain.class); + Request mockRequest = new Request.Builder().url("https://example.com").build(); + Mockito.when(mockChain.request()).thenReturn(mockRequest); + + Response mockResponse = new Response.Builder() + .request(mockRequest) + .protocol(Protocol.HTTP_1_1) + .code(200) + .message("OK") + .body(ResponseBody.create("", MediaType.get("application/json"))) + .build(); + Mockito.when(mockChain.proceed(Mockito.any(Request.class))).thenReturn(mockResponse); + + // Invoke the lambda — this covers lambda$updateExecutorInHTTP$21 + Response response = interceptor.intercept(mockChain); + Assert.assertNotNull(response); + + // Verify the interceptor added the Authorization header + Mockito.verify(mockChain).proceed(Mockito.argThat(req -> + req.header("Authorization") != null && + req.header("Authorization").startsWith("Bearer ") + )); + } catch (Exception e) { + Assert.fail("Should not have thrown: " + e.getMessage()); + } + } + + @Test + public void testGetFileForFileUpload_withNoFileInput_returnsNull() { + try { + FileUploadRequest request = FileUploadRequest.builder() + .table("test_table") + .columnName("test_col") + .build(); + java.io.File result = vaultClient.getFileForFileUpload(request); + Assert.assertNull(result); + } catch (Exception e) { + Assert.fail("Should not have thrown: " + e.getMessage()); + } + } + + @Test + public void testGetDeidentifyImageRequest_withEntityOnlyNull_andEntityUniqueCounterNonEmpty() { + try { + java.io.File file = new java.io.File("test.jpg"); + FileInput fileInput = FileInput.builder().file(file).build(); + List entities = Arrays.asList(DetectEntities.NAME); + TokenFormat tokenFormat = TokenFormat.builder() + .entityUniqueCounter(entities) + .build(); + + DeidentifyFileRequest request = DeidentifyFileRequest.builder() + .file(fileInput) + .entities(entities) + .tokenFormat(tokenFormat) + .build(); + + DeidentifyFileImageRequestDeidentifyImage imageRequest = + vaultClient.getDeidentifyImageRequest(request, vaultID, "base64content", "jpg"); + + Assert.assertNotNull(imageRequest); + } catch (Exception e) { + Assert.fail("Should not have thrown: " + e.getMessage()); + } + } + + @Test + public void testGetDeidentifyImageRequest_withEmptyEntityOnlyList() { + try { + java.io.File file = new java.io.File("test.jpg"); + FileInput fileInput = FileInput.builder().file(file).build(); + List entities = Arrays.asList(DetectEntities.NAME); + TokenFormat tokenFormat = TokenFormat.builder() + .entityOnly(Collections.emptyList()) + .entityUniqueCounter(Collections.emptyList()) + .build(); + + DeidentifyFileRequest request = DeidentifyFileRequest.builder() + .file(fileInput) + .entities(entities) + .tokenFormat(tokenFormat) + .build(); + + DeidentifyFileImageRequestDeidentifyImage imageRequest = + vaultClient.getDeidentifyImageRequest(request, vaultID, "base64content", "jpg"); + Assert.assertNotNull(imageRequest); + } catch (Exception e) { + Assert.fail("Should not have thrown: " + e.getMessage()); + } + } + + @Test + public void testGetDeidentifyGenericFileRequest_withEmptyEntityLists() { + try { + java.io.File file = new java.io.File("test.pdf"); + FileInput fileInput = FileInput.builder().file(file).build(); + List entities = Arrays.asList(DetectEntities.NAME); + TokenFormat tokenFormat = TokenFormat.builder() + .entityOnly(Collections.emptyList()) + .entityUniqueCounter(Collections.emptyList()) + .build(); + + DeidentifyFileRequest request = DeidentifyFileRequest.builder() + .file(fileInput) + .entities(entities) + .tokenFormat(tokenFormat) + .build(); + + com.skyflow.generated.rest.resources.files.requests.DeidentifyFileRequest result = + vaultClient.getDeidentifyGenericFileRequest(request, vaultID, "base64content", "pdf"); + Assert.assertNotNull(result); + } catch (Exception e) { + Assert.fail("Should not have thrown: " + e.getMessage()); + } + } + + @Test + public void testGetDeidentifyGenericFileRequest_withNullFileExtension() { + // Covers the `fileExtension != null ? ... : null` false branch at line 779. + // The ternary evaluates null, which is then passed to FileData.builder().dataFormat(null) + // which throws — confirming the null branch of the ternary was exercised. + java.io.File file = new java.io.File("test.pdf"); + FileInput fileInput = FileInput.builder().file(file).build(); + List entities = Arrays.asList(DetectEntities.NAME); + + DeidentifyFileRequest request = DeidentifyFileRequest.builder() + .file(fileInput) + .entities(entities) + .build(); + + try { + vaultClient.getDeidentifyGenericFileRequest(request, vaultID, "base64content", null); + Assert.fail("Expected exception from null dataFormat"); + } catch (Exception e) { + // null fileExtension → ternary false branch → null passed to dataFormat() → throws + Assert.assertNotNull(e.getMessage()); + } + } } diff --git a/src/test/java/com/skyflow/serviceaccount/util/TokenTests.java b/src/test/java/com/skyflow/serviceaccount/util/TokenTests.java index ed5c72b2..88887681 100644 --- a/src/test/java/com/skyflow/serviceaccount/util/TokenTests.java +++ b/src/test/java/com/skyflow/serviceaccount/util/TokenTests.java @@ -52,4 +52,16 @@ public void testExpiredTokenForIsExpiredToken() { Assert.fail(INVALID_EXCEPTION_THROWN); } } + + @Test + public void testExpiredJwtTokenForIsExpiredToken() { + // 3-part fake JWT: middle is base64({"exp":1}) = eyJleHAiOjF9, exp=1970 → always expired + Assert.assertTrue(Token.isExpired("x.eyJleHAiOjF9.y")); + } + + @Test + public void testValidJwtTokenForIsExpiredToken() { + // 3-part fake JWT: middle is base64({"exp":9999999999}) = eyJleHAiOjk5OTk5OTk5OTl9, far-future + Assert.assertFalse(Token.isExpired("x.eyJleHAiOjk5OTk5OTk5OTl9.y")); + } } diff --git a/src/test/java/com/skyflow/utils/HttpUtilityTests.java b/src/test/java/com/skyflow/utils/HttpUtilityTests.java index 46be8f6a..0dbdf00b 100644 --- a/src/test/java/com/skyflow/utils/HttpUtilityTests.java +++ b/src/test/java/com/skyflow/utils/HttpUtilityTests.java @@ -189,4 +189,40 @@ public void testSendRequestFormURLEncodedWithSpecialCharacters() { fail(INVALID_EXCEPTION_THROWN); } } + + @Test + public void testAppendRequestId_withNonNullRequestId() { + String result = HttpUtility.appendRequestId("base message", "req-123"); + Assert.assertEquals("base message - requestId: req-123", result); + } + + @Test + public void testAppendRequestId_withNullRequestId() { + String result = HttpUtility.appendRequestId("base message", null); + Assert.assertEquals("base message", result); + } + + @Test + public void testAppendRequestId_withEmptyRequestId() { + String result = HttpUtility.appendRequestId("base message", ""); + Assert.assertEquals("base message", result); + } + + @Test + @PrepareForTest({URL.class, HttpURLConnection.class}) + public void testSendRequestWithNestedJsonBody() { + try { + given(mockConnection.getRequestProperty("content-type")).willReturn("application/json"); + Map headers = new HashMap<>(); + headers.put("content-type", "application/json"); + JsonObject nested = new JsonObject(); + nested.addProperty("inner", "value"); + JsonObject params = new JsonObject(); + params.add("outer", nested); + String response = httpUtility.sendRequest("POST", url, params, headers); + Assert.assertEquals(expected, response); + } catch (Exception e) { + fail(INVALID_EXCEPTION_THROWN); + } + } } diff --git a/src/test/java/com/skyflow/vault/BinAuditTests.java b/src/test/java/com/skyflow/vault/BinAuditTests.java new file mode 100644 index 00000000..25da7fc8 --- /dev/null +++ b/src/test/java/com/skyflow/vault/BinAuditTests.java @@ -0,0 +1,34 @@ +package com.skyflow.vault; + +import com.skyflow.vault.audit.ListEventRequest; +import com.skyflow.vault.audit.ListEventResponse; +import com.skyflow.vault.bin.GetBinRequest; +import com.skyflow.vault.bin.GetBinResponse; +import org.junit.Assert; +import org.junit.Test; + +public class BinAuditTests { + @Test + public void testGetBinRequestConstructor() { + GetBinRequest req = new GetBinRequest(); + Assert.assertNotNull(req); + } + + @Test + public void testGetBinResponseConstructor() { + GetBinResponse resp = new GetBinResponse(); + Assert.assertNotNull(resp); + } + + @Test + public void testListEventRequestConstructor() { + ListEventRequest req = new ListEventRequest(); + Assert.assertNotNull(req); + } + + @Test + public void testListEventResponseConstructor() { + ListEventResponse resp = new ListEventResponse(); + Assert.assertNotNull(resp); + } +} diff --git a/src/test/java/com/skyflow/vault/controller/ConnectionControllerTests.java b/src/test/java/com/skyflow/vault/controller/ConnectionControllerTests.java index b121280a..6c858b45 100644 --- a/src/test/java/com/skyflow/vault/controller/ConnectionControllerTests.java +++ b/src/test/java/com/skyflow/vault/controller/ConnectionControllerTests.java @@ -4,38 +4,64 @@ import com.skyflow.config.ConnectionConfig; import com.skyflow.config.Credentials; import com.skyflow.enums.LogLevel; +import com.skyflow.enums.RequestMethod; import com.skyflow.errors.ErrorCode; import com.skyflow.errors.ErrorMessage; import com.skyflow.errors.SkyflowException; +import com.skyflow.utils.HttpUtility; import com.skyflow.vault.connection.InvokeConnectionRequest; +import com.skyflow.vault.connection.InvokeConnectionResponse; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; import org.junit.Assert; +import org.junit.Before; import org.junit.BeforeClass; import org.junit.Test; +import org.junit.runner.RunWith; +import org.powermock.api.mockito.PowerMockito; +import org.powermock.core.classloader.annotations.PrepareForTest; +import org.powermock.modules.junit4.PowerMockRunner; +import java.io.IOException; +import java.net.URL; import java.util.HashMap; +import java.util.Map; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.when; + +@RunWith(PowerMockRunner.class) +@PrepareForTest({HttpUtility.class}) public class ConnectionControllerTests { private static final String INVALID_EXCEPTION_THROWN = "Should not have thrown any exception"; private static final String EXCEPTION_NOT_THROWN = "Should have thrown an exception"; - private static String connectionID = null; - private static String connectionURL = null; - private static ConnectionConfig connectionConfig = null; - private static Skyflow skyflowClient = null; + private static final String API_KEY = "sky-ab123-abcd1234cdef1234abcd4321cdef4321"; + private static final String REQUEST_ID = "req-test-123"; - @BeforeClass - public static void setup() { - connectionID = "vault123"; - connectionURL = "https://test.connection.url"; + private static ConnectionConfig connectionConfig; + private static Credentials credentials; + private ConnectionController controller; - Credentials credentials = new Credentials(); - credentials.setToken("valid-token"); + @BeforeClass + public static void setupClass() { + credentials = new Credentials(); + credentials.setApiKey(API_KEY); connectionConfig = new ConnectionConfig(); - connectionConfig.setConnectionId(connectionID); - connectionConfig.setConnectionUrl(connectionURL); + connectionConfig.setConnectionId("conn123"); + connectionConfig.setConnectionUrl("https://test.connection.url"); connectionConfig.setCredentials(credentials); } + @Before + public void setup() { + controller = new ConnectionController(connectionConfig, credentials); + PowerMockito.mockStatic(HttpUtility.class); + } + + // --- existing validation test (kept) --- + @Test public void testInvalidRequestInInvokeConnectionMethod() { try { @@ -49,4 +75,336 @@ public void testInvalidRequestInInvokeConnectionMethod() { Assert.assertEquals(ErrorMessage.EmptyRequestBody.getMessage(), e.getMessage()); } } + + // --- happy-path tests --- + + @Test + public void testInvoke_successWithDefaultRequest() throws Exception { + when(HttpUtility.sendRequest(anyString(), any(URL.class), any(), any())) + .thenReturn("{\"data\":\"test-value\"}"); + when(HttpUtility.getRequestID()).thenReturn(REQUEST_ID); + + InvokeConnectionRequest request = InvokeConnectionRequest.builder().build(); + InvokeConnectionResponse response = controller.invoke(request); + + Assert.assertNotNull(response); + Assert.assertNotNull(response.getData()); + } + + @Test + public void testInvoke_successWithGetMethod() throws Exception { + when(HttpUtility.sendRequest(anyString(), any(URL.class), any(), any())) + .thenReturn("{\"result\":\"ok\"}"); + when(HttpUtility.getRequestID()).thenReturn(REQUEST_ID); + + InvokeConnectionRequest request = InvokeConnectionRequest.builder() + .method(RequestMethod.GET) + .build(); + InvokeConnectionResponse response = controller.invoke(request); + + Assert.assertNotNull(response); + } + + @Test + public void testInvoke_successWithDeleteMethod() throws Exception { + when(HttpUtility.sendRequest(anyString(), any(URL.class), any(), any())) + .thenReturn("{\"deleted\":true}"); + when(HttpUtility.getRequestID()).thenReturn(REQUEST_ID); + + InvokeConnectionRequest request = InvokeConnectionRequest.builder() + .method(RequestMethod.DELETE) + .build(); + InvokeConnectionResponse response = controller.invoke(request); + + Assert.assertNotNull(response); + } + + @Test + public void testInvoke_successWithPutMethod() throws Exception { + when(HttpUtility.sendRequest(anyString(), any(URL.class), any(), any())) + .thenReturn("{\"updated\":true}"); + when(HttpUtility.getRequestID()).thenReturn(REQUEST_ID); + + Map body = new HashMap<>(); + body.put("field", "value"); + + InvokeConnectionRequest request = InvokeConnectionRequest.builder() + .method(RequestMethod.PUT) + .requestBody(body) + .build(); + InvokeConnectionResponse response = controller.invoke(request); + + Assert.assertNotNull(response); + } + + @Test + public void testInvoke_successWithStringBodyAndJsonContentType() throws Exception { + when(HttpUtility.sendRequest(anyString(), any(URL.class), any(), any())) + .thenReturn("{\"parsed\":true}"); + when(HttpUtility.getRequestID()).thenReturn(REQUEST_ID); + + Map headers = new HashMap<>(); + headers.put("content-type", "application/json"); + + InvokeConnectionRequest request = InvokeConnectionRequest.builder() + .method(RequestMethod.POST) + .requestHeaders(headers) + .requestBody("{\"key\":\"value\"}") + .build(); + InvokeConnectionResponse response = controller.invoke(request); + + Assert.assertNotNull(response); + } + + @Test + public void testInvoke_successWithStringBodyAndNonJsonContentType() throws Exception { + when(HttpUtility.sendRequest(anyString(), any(URL.class), any(), any())) + .thenReturn("ok"); + when(HttpUtility.getRequestID()).thenReturn(REQUEST_ID); + + Map headers = new HashMap<>(); + headers.put("content-type", "text/plain"); + + InvokeConnectionRequest request = InvokeConnectionRequest.builder() + .method(RequestMethod.POST) + .requestHeaders(headers) + .requestBody("raw body content") + .build(); + InvokeConnectionResponse response = controller.invoke(request); + + Assert.assertNotNull(response); + } + + @Test + public void testInvoke_successWithObjectBody() throws Exception { + when(HttpUtility.sendRequest(anyString(), any(URL.class), any(), any())) + .thenReturn("{\"result\":\"ok\"}"); + when(HttpUtility.getRequestID()).thenReturn(REQUEST_ID); + + Map body = new HashMap<>(); + body.put("card_number", "4111111111111111"); + + InvokeConnectionRequest request = InvokeConnectionRequest.builder() + .method(RequestMethod.POST) + .requestBody(body) + .build(); + InvokeConnectionResponse response = controller.invoke(request); + + Assert.assertNotNull(response); + } + + @Test + public void testInvoke_withPathParams() throws Exception { + when(HttpUtility.sendRequest(anyString(), any(URL.class), any(), any())) + .thenReturn("{\"data\":\"ok\"}"); + when(HttpUtility.getRequestID()).thenReturn(REQUEST_ID); + + Map pathParams = new HashMap<>(); + pathParams.put("id", "record-123"); + + InvokeConnectionRequest request = InvokeConnectionRequest.builder() + .method(RequestMethod.GET) + .pathParams(pathParams) + .build(); + InvokeConnectionResponse response = controller.invoke(request); + + Assert.assertNotNull(response); + } + + @Test + public void testInvoke_withQueryParams() throws Exception { + when(HttpUtility.sendRequest(anyString(), any(URL.class), any(), any())) + .thenReturn("{\"data\":\"ok\"}"); + when(HttpUtility.getRequestID()).thenReturn(REQUEST_ID); + + Map queryParams = new HashMap<>(); + queryParams.put("limit", "10"); + + InvokeConnectionRequest request = InvokeConnectionRequest.builder() + .method(RequestMethod.GET) + .queryParams(queryParams) + .build(); + InvokeConnectionResponse response = controller.invoke(request); + + Assert.assertNotNull(response); + } + + @Test + public void testInvoke_withRequestHeaders() throws Exception { + when(HttpUtility.sendRequest(anyString(), any(URL.class), any(), any())) + .thenReturn("{\"ok\":true}"); + when(HttpUtility.getRequestID()).thenReturn(REQUEST_ID); + + Map headers = new HashMap<>(); + headers.put("x-custom-header", "custom-value"); + + InvokeConnectionRequest request = InvokeConnectionRequest.builder() + .method(RequestMethod.GET) + .requestHeaders(headers) + .build(); + InvokeConnectionResponse response = controller.invoke(request); + + Assert.assertNotNull(response); + } + + @Test + public void testInvoke_nonJsonResponseWrappedUnderResponseKey() throws Exception { + when(HttpUtility.sendRequest(anyString(), any(URL.class), any(), any())) + .thenReturn("plain-text-response"); + when(HttpUtility.getRequestID()).thenReturn(REQUEST_ID); + + InvokeConnectionRequest request = InvokeConnectionRequest.builder().build(); + InvokeConnectionResponse response = controller.invoke(request); + + Assert.assertNotNull(response); + Assert.assertNotNull(response.getData()); + JsonObject data = JsonParser.parseString(response.getData().toString()).getAsJsonObject(); + Assert.assertTrue(data.has("response")); + Assert.assertEquals("plain-text-response", data.get("response").getAsString()); + } + + @Test + public void testInvoke_responseContainsRequestId() throws Exception { + when(HttpUtility.sendRequest(anyString(), any(URL.class), any(), any())) + .thenReturn("{\"data\":\"ok\"}"); + when(HttpUtility.getRequestID()).thenReturn(REQUEST_ID); + + InvokeConnectionRequest request = InvokeConnectionRequest.builder().build(); + InvokeConnectionResponse response = controller.invoke(request); + + Assert.assertNotNull(response); + Assert.assertNotNull(response.getMetadata()); + Assert.assertEquals(REQUEST_ID, response.getMetadata().get("requestId")); + } + + @Test + public void testInvoke_errorsNullOnSuccess() throws Exception { + when(HttpUtility.sendRequest(anyString(), any(URL.class), any(), any())) + .thenReturn("{\"data\":\"ok\"}"); + when(HttpUtility.getRequestID()).thenReturn(REQUEST_ID); + + InvokeConnectionRequest request = InvokeConnectionRequest.builder().build(); + InvokeConnectionResponse response = controller.invoke(request); + + Assert.assertNotNull(response); + Assert.assertNull(response.getErrors()); + } + + // --- error / validation-failure tests --- + + @Test + public void testInvoke_ioExceptionThrowsSkyflowException() throws Exception { + when(HttpUtility.sendRequest(anyString(), any(URL.class), any(), any())) + .thenThrow(new IOException("connection refused")); + when(HttpUtility.getRequestID()).thenReturn(REQUEST_ID); + + try { + InvokeConnectionRequest request = InvokeConnectionRequest.builder().build(); + controller.invoke(request); + Assert.fail(EXCEPTION_NOT_THROWN); + } catch (SkyflowException e) { + Assert.assertNotNull(e.getMessage()); + } + } + + @Test + public void testInvoke_skyflowExceptionFromSendRequestPropagates() throws Exception { + when(HttpUtility.sendRequest(anyString(), any(URL.class), any(), any())) + .thenThrow(new SkyflowException("upstream error", new RuntimeException())); + when(HttpUtility.getRequestID()).thenReturn(REQUEST_ID); + + try { + InvokeConnectionRequest request = InvokeConnectionRequest.builder().build(); + controller.invoke(request); + Assert.fail(EXCEPTION_NOT_THROWN); + } catch (SkyflowException e) { + Assert.assertNotNull(e.getMessage()); + } + } + + @Test + public void testInvoke_emptyRequestHeadersThrowsSkyflowException() { + try { + InvokeConnectionRequest request = InvokeConnectionRequest.builder() + .requestHeaders(new HashMap<>()) + .build(); + controller.invoke(request); + Assert.fail(EXCEPTION_NOT_THROWN); + } catch (SkyflowException e) { + Assert.assertEquals(ErrorCode.INVALID_INPUT.getCode(), e.getHttpCode()); + Assert.assertEquals(ErrorMessage.EmptyRequestHeaders.getMessage(), e.getMessage()); + } + } + + @Test + public void testInvoke_emptyPathParamsThrowsSkyflowException() { + try { + InvokeConnectionRequest request = InvokeConnectionRequest.builder() + .pathParams(new HashMap<>()) + .build(); + controller.invoke(request); + Assert.fail(EXCEPTION_NOT_THROWN); + } catch (SkyflowException e) { + Assert.assertEquals(ErrorCode.INVALID_INPUT.getCode(), e.getHttpCode()); + Assert.assertEquals(ErrorMessage.EmptyPathParams.getMessage(), e.getMessage()); + } + } + + @Test + public void testInvoke_emptyQueryParamsThrowsSkyflowException() { + try { + InvokeConnectionRequest request = InvokeConnectionRequest.builder() + .queryParams(new HashMap<>()) + .build(); + controller.invoke(request); + Assert.fail(EXCEPTION_NOT_THROWN); + } catch (SkyflowException e) { + Assert.assertEquals(ErrorCode.INVALID_INPUT.getCode(), e.getHttpCode()); + Assert.assertEquals(ErrorMessage.EmptyQueryParams.getMessage(), e.getMessage()); + } + } + + @Test + public void testInvoke_emptyStringBodyThrowsSkyflowException() { + try { + InvokeConnectionRequest request = InvokeConnectionRequest.builder() + .requestBody(" ") + .build(); + controller.invoke(request); + Assert.fail(EXCEPTION_NOT_THROWN); + } catch (SkyflowException e) { + Assert.assertEquals(ErrorCode.INVALID_INPUT.getCode(), e.getHttpCode()); + Assert.assertEquals(ErrorMessage.EmptyRequestBody.getMessage(), e.getMessage()); + } + } + + @Test + public void testInvoke_emptyHashMapBodyThrowsSkyflowException() { + try { + InvokeConnectionRequest request = InvokeConnectionRequest.builder() + .requestBody(new HashMap<>()) + .build(); + controller.invoke(request); + Assert.fail(EXCEPTION_NOT_THROWN); + } catch (SkyflowException e) { + Assert.assertEquals(ErrorCode.INVALID_INPUT.getCode(), e.getHttpCode()); + Assert.assertEquals(ErrorMessage.EmptyRequestBody.getMessage(), e.getMessage()); + } + } + + @Test + public void testInvoke_nullHeaderValueThrowsSkyflowException() { + try { + Map headers = new HashMap<>(); + headers.put("x-header", null); + InvokeConnectionRequest request = InvokeConnectionRequest.builder() + .requestHeaders(headers) + .build(); + controller.invoke(request); + Assert.fail(EXCEPTION_NOT_THROWN); + } catch (SkyflowException e) { + Assert.assertEquals(ErrorCode.INVALID_INPUT.getCode(), e.getHttpCode()); + Assert.assertEquals(ErrorMessage.InvalidRequestHeaders.getMessage(), e.getMessage()); + } + } } diff --git a/src/test/java/com/skyflow/vault/controller/DetectControllerTests.java b/src/test/java/com/skyflow/vault/controller/DetectControllerTests.java index aae713b1..52bb2be4 100644 --- a/src/test/java/com/skyflow/vault/controller/DetectControllerTests.java +++ b/src/test/java/com/skyflow/vault/controller/DetectControllerTests.java @@ -1,30 +1,66 @@ package com.skyflow.vault.controller; import com.skyflow.Skyflow; +import com.skyflow.VaultClient; import com.skyflow.config.Credentials; import com.skyflow.config.VaultConfig; +import com.skyflow.enums.DetectEntities; import com.skyflow.enums.Env; import com.skyflow.enums.LogLevel; import com.skyflow.errors.ErrorMessage; import com.skyflow.errors.HttpStatus; import com.skyflow.errors.SkyflowException; +import com.skyflow.generated.rest.ApiClient; +import com.skyflow.generated.rest.core.ApiClientApiException; +import com.skyflow.generated.rest.resources.files.FilesClient; +import com.skyflow.generated.rest.resources.files.requests.GetRunRequest; +import com.skyflow.generated.rest.resources.strings.StringsClient; +import com.skyflow.generated.rest.types.DeidentifiedFileOutput; +import com.skyflow.generated.rest.types.DeidentifiedFileOutputProcessedFileExtension; +import com.skyflow.generated.rest.types.DeidentifyStringResponse; +import com.skyflow.generated.rest.types.DetectRunsResponse; +import com.skyflow.generated.rest.types.DetectRunsResponseOutputType; +import com.skyflow.generated.rest.types.DetectRunsResponseStatus; +import com.skyflow.generated.rest.types.IdentifyResponse; +import com.skyflow.generated.rest.types.WordCharacterCount; import com.skyflow.utils.Constants; import com.skyflow.utils.Utils; import com.skyflow.vault.detect.DeidentifyTextRequest; +import com.skyflow.vault.detect.DeidentifyTextResponse; +import com.skyflow.vault.detect.DeidentifyFileResponse; +import com.skyflow.vault.detect.GetDetectRunRequest; import com.skyflow.vault.detect.ReidentifyTextRequest; +import com.skyflow.vault.detect.ReidentifyTextResponse; import org.junit.Assert; import org.junit.BeforeClass; import org.junit.Test; +import org.mockito.Mockito; + +import java.io.File; +import java.lang.reflect.Field; +import java.nio.file.Files; +import java.util.Base64; +import java.util.Collections; + +import com.skyflow.generated.rest.core.RequestOptions; +import com.skyflow.vault.detect.DeidentifyFileRequest; +import com.skyflow.vault.detect.FileInput; +import com.skyflow.vault.detect.TokenFormat; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.when; public class DetectControllerTests { 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"; private static String vaultID = null; private static String clusterID = null; private static VaultConfig vaultConfig = null; private static Skyflow skyflowClient = null; @BeforeClass - public static void setup() throws SkyflowException, NoSuchMethodException { + public static void setup() throws SkyflowException { vaultID = "vault123"; clusterID = "cluster123"; @@ -37,13 +73,31 @@ public static void setup() throws SkyflowException, NoSuchMethodException { vaultConfig.setEnv(Env.DEV); vaultConfig.setCredentials(credentials); - skyflowClient = Skyflow.builder() .setLogLevel(LogLevel.DEBUG) .addVaultConfig(vaultConfig) .build(); } + // ─── helper: build a DetectController with a mocked ApiClient ───────────── + + private static DetectController createDetectControllerWithMock(ApiClient mockApiClient) throws Exception { + Credentials creds = new Credentials(); + creds.setApiKey("sky-ab123-abcd1234cdef1234abcd4321cdef4321"); + + VaultConfig config = new VaultConfig(); + config.setVaultId(vaultID); + config.setClusterId(clusterID); + config.setEnv(Env.DEV); + + DetectController controller = new DetectController(config, creds); + Field f = VaultClient.class.getDeclaredField("apiClient"); + f.setAccessible(true); + f.set(controller, mockApiClient); + return controller; + } + + // ─── deidentifyText — validation ────────────────────────────────────────── @Test public void testNullTextInRequestInDeidentifyStringMethod() { @@ -81,6 +135,82 @@ public void testEmptyTextInRequestInDeidentifyStringMethod() { } } + // ─── deidentifyText — happy path ────────────────────────────────────────── + + @Test + public void testDeidentifyTextHappyPath() throws Exception { + StringsClient mockStringsClient = Mockito.mock(StringsClient.class); + ApiClient mockApiClient = Mockito.mock(ApiClient.class); + when(mockApiClient.strings()).thenReturn(mockStringsClient); + + DeidentifyStringResponse fakeResponse = DeidentifyStringResponse.builder() + .processedText("hello [REDACTED]") + .wordCount(2) + .characterCount(16) + .build(); + + when(mockStringsClient.deidentifyString(any(), any())).thenReturn(fakeResponse); + + DetectController controller = createDetectControllerWithMock(mockApiClient); + DeidentifyTextRequest request = DeidentifyTextRequest.builder().text("hello world").build(); + + try { + DeidentifyTextResponse response = controller.deidentifyText(request); + Assert.assertNotNull(response); + Assert.assertEquals("hello [REDACTED]", response.getProcessedText()); + Assert.assertEquals(2, response.getWordCount()); + Assert.assertEquals(16, response.getCharCount()); + } catch (SkyflowException e) { + Assert.fail(INVALID_EXCEPTION_THROWN + ": " + e.getMessage()); + } + } + + // ─── deidentifyText — API error path ────────────────────────────────────── + + @Test + public void testDeidentifyTextApiError() throws Exception { + StringsClient mockStringsClient = Mockito.mock(StringsClient.class); + ApiClient mockApiClient = Mockito.mock(ApiClient.class); + when(mockApiClient.strings()).thenReturn(mockStringsClient); + + int expectedStatusCode = 403; + when(mockStringsClient.deidentifyString(any(), any())) + .thenThrow(new ApiClientApiException("Forbidden", expectedStatusCode, "access denied")); + + DetectController controller = createDetectControllerWithMock(mockApiClient); + DeidentifyTextRequest request = DeidentifyTextRequest.builder().text("hello world").build(); + + try { + controller.deidentifyText(request); + Assert.fail(EXCEPTION_NOT_THROWN); + } catch (SkyflowException e) { + Assert.assertEquals(expectedStatusCode, e.getHttpCode()); + } + } + + @Test + public void testDeidentifyTextApiError500() throws Exception { + StringsClient mockStringsClient = Mockito.mock(StringsClient.class); + ApiClient mockApiClient = Mockito.mock(ApiClient.class); + when(mockApiClient.strings()).thenReturn(mockStringsClient); + + int expectedStatusCode = 500; + when(mockStringsClient.deidentifyString(any(), any())) + .thenThrow(new ApiClientApiException("Internal Server Error", expectedStatusCode, "server error body")); + + DetectController controller = createDetectControllerWithMock(mockApiClient); + DeidentifyTextRequest request = DeidentifyTextRequest.builder().text("some text").build(); + + try { + controller.deidentifyText(request); + Assert.fail(EXCEPTION_NOT_THROWN); + } catch (SkyflowException e) { + Assert.assertEquals(expectedStatusCode, e.getHttpCode()); + } + } + + // ─── reidentifyText — validation ────────────────────────────────────────── + @Test public void testNullTextInRequestInReidentifyStringMethod() { try { @@ -117,5 +247,701 @@ public void testEmptyTextInRequestInReidentifyStringMethod() { } } -} + // ─── reidentifyText — happy path ────────────────────────────────────────── + + @Test + public void testReidentifyTextHappyPath() throws Exception { + StringsClient mockStringsClient = Mockito.mock(StringsClient.class); + ApiClient mockApiClient = Mockito.mock(ApiClient.class); + when(mockApiClient.strings()).thenReturn(mockStringsClient); + + IdentifyResponse fakeResponse = IdentifyResponse.builder().text("original text").build(); + when(mockStringsClient.reidentifyString(any(), any())).thenReturn(fakeResponse); + + DetectController controller = createDetectControllerWithMock(mockApiClient); + ReidentifyTextRequest request = ReidentifyTextRequest.builder().text("tokenized text").build(); + + try { + ReidentifyTextResponse response = controller.reidentifyText(request); + Assert.assertNotNull(response); + Assert.assertEquals("original text", response.getProcessedText()); + } catch (SkyflowException e) { + Assert.fail(INVALID_EXCEPTION_THROWN + ": " + e.getMessage()); + } + } + + // ─── reidentifyText — API error path ────────────────────────────────────── + + @Test + public void testReidentifyTextApiError() throws Exception { + StringsClient mockStringsClient = Mockito.mock(StringsClient.class); + ApiClient mockApiClient = Mockito.mock(ApiClient.class); + when(mockApiClient.strings()).thenReturn(mockStringsClient); + + int expectedStatusCode = 401; + when(mockStringsClient.reidentifyString(any(), any())) + .thenThrow(new ApiClientApiException("Unauthorized", expectedStatusCode, "unauthorized body")); + + DetectController controller = createDetectControllerWithMock(mockApiClient); + ReidentifyTextRequest request = ReidentifyTextRequest.builder().text("some text").build(); + + try { + controller.reidentifyText(request); + Assert.fail(EXCEPTION_NOT_THROWN); + } catch (SkyflowException e) { + Assert.assertEquals(expectedStatusCode, e.getHttpCode()); + } + } + + @Test + public void testReidentifyTextApiError500() throws Exception { + StringsClient mockStringsClient = Mockito.mock(StringsClient.class); + ApiClient mockApiClient = Mockito.mock(ApiClient.class); + when(mockApiClient.strings()).thenReturn(mockStringsClient); + + int expectedStatusCode = 500; + when(mockStringsClient.reidentifyString(any(), any())) + .thenThrow(new ApiClientApiException("Internal Server Error", expectedStatusCode, "server error body")); + + DetectController controller = createDetectControllerWithMock(mockApiClient); + ReidentifyTextRequest request = ReidentifyTextRequest.builder().text("some text").build(); + + try { + controller.reidentifyText(request); + Assert.fail(EXCEPTION_NOT_THROWN); + } catch (SkyflowException e) { + Assert.assertEquals(expectedStatusCode, e.getHttpCode()); + } + } + + // ─── getDetectRun — validation ──────────────────────────────────────────── + + @Test + public void testNullRunIdInGetDetectRunRequest() { + try { + GetDetectRunRequest request = GetDetectRunRequest.builder().runId(null).build(); + skyflowClient = Skyflow.builder().setLogLevel(LogLevel.DEBUG).addVaultConfig(vaultConfig).build(); + skyflowClient.detect(vaultID).getDetectRun(request); + Assert.fail(EXCEPTION_NOT_THROWN); + } catch (SkyflowException e) { + Assert.assertEquals(HttpStatus.BAD_REQUEST.getHttpStatus(), e.getHttpStatus()); + } + } + + @Test + public void testEmptyRunIdInGetDetectRunRequest() { + try { + GetDetectRunRequest request = GetDetectRunRequest.builder().runId("").build(); + skyflowClient = Skyflow.builder().setLogLevel(LogLevel.DEBUG).addVaultConfig(vaultConfig).build(); + skyflowClient.detect(vaultID).getDetectRun(request); + Assert.fail(EXCEPTION_NOT_THROWN); + } catch (SkyflowException e) { + Assert.assertEquals(HttpStatus.BAD_REQUEST.getHttpStatus(), e.getHttpStatus()); + } + } + + // ─── getDetectRun — happy path (no output list) ─────────────────────────── + + @Test + public void testGetDetectRunHappyPathNoOutput() throws Exception { + FilesClient mockFilesClient = Mockito.mock(FilesClient.class); + ApiClient mockApiClient = Mockito.mock(ApiClient.class); + when(mockApiClient.files()).thenReturn(mockFilesClient); + + DetectRunsResponse fakeRunsResponse = DetectRunsResponse.builder() + .status(DetectRunsResponseStatus.SUCCESS) + .outputType(DetectRunsResponseOutputType.BASE_64) + .size(10.5f) + .duration(1.2f) + .build(); + + when(mockFilesClient.getRun(anyString(), any(GetRunRequest.class))) + .thenReturn(fakeRunsResponse); + + DetectController controller = createDetectControllerWithMock(mockApiClient); + GetDetectRunRequest request = GetDetectRunRequest.builder().runId("run-123").build(); + + try { + DeidentifyFileResponse response = controller.getDetectRun(request); + Assert.assertNotNull(response); + Assert.assertEquals("run-123", response.getRunId()); + } catch (SkyflowException e) { + Assert.fail(INVALID_EXCEPTION_THROWN + ": " + e.getMessage()); + } + } + + // ─── getDetectRun — API error path ──────────────────────────────────────── + + @Test + public void testGetDetectRunApiError() throws Exception { + FilesClient mockFilesClient = Mockito.mock(FilesClient.class); + ApiClient mockApiClient = Mockito.mock(ApiClient.class); + when(mockApiClient.files()).thenReturn(mockFilesClient); + + int expectedStatusCode = 404; + when(mockFilesClient.getRun(anyString(), any(GetRunRequest.class))) + .thenThrow(new ApiClientApiException("Not Found", expectedStatusCode, "run not found")); + + DetectController controller = createDetectControllerWithMock(mockApiClient); + GetDetectRunRequest request = GetDetectRunRequest.builder().runId("run-999").build(); + + try { + controller.getDetectRun(request); + Assert.fail(EXCEPTION_NOT_THROWN); + } catch (SkyflowException e) { + Assert.assertEquals(expectedStatusCode, e.getHttpCode()); + } + } + + // ─── helpers ───────────────────────────────────────────────────────────── + + private static DetectRunsResponse buildSuccessDetectRunsResponse() { + return DetectRunsResponse.builder() + .status(DetectRunsResponseStatus.SUCCESS) + .outputType(DetectRunsResponseOutputType.BASE_64) + .size(1.0f) + .duration(0.5f) + .build(); + } + + private DeidentifyFileResponse runDeidentifyFileForExtension( + String extension, FilesClient mockFilesClient) throws Exception { + return runDeidentifyFileForExtension(extension, mockFilesClient, null); + } + + private DeidentifyFileResponse runDeidentifyFileForExtension( + String extension, FilesClient mockFilesClient, TokenFormat tokenFormat) throws Exception { + File tmpFile = File.createTempFile("test-detect", "." + extension); + tmpFile.deleteOnExit(); + Files.write(tmpFile.toPath(), ("content for " + extension).getBytes()); + + ApiClient mockApiClient = Mockito.mock(ApiClient.class); + when(mockApiClient.files()).thenReturn(mockFilesClient); + when(mockFilesClient.getRun(anyString(), any(GetRunRequest.class), any(RequestOptions.class))) + .thenReturn(buildSuccessDetectRunsResponse()); + + DetectController controller = createDetectControllerWithMock(mockApiClient); + DeidentifyFileRequest.DeidentifyFileRequestBuilder builder = DeidentifyFileRequest.builder() + .file(FileInput.builder().file(tmpFile).build()); + if (tokenFormat != null) { + builder.tokenFormat(tokenFormat); + } + return controller.deidentifyFile(builder.build()); + } + + @Test + public void testGetDetectRunApiError500() throws Exception { + FilesClient mockFilesClient = Mockito.mock(FilesClient.class); + ApiClient mockApiClient = Mockito.mock(ApiClient.class); + when(mockApiClient.files()).thenReturn(mockFilesClient); + + int expectedStatusCode = 500; + when(mockFilesClient.getRun(anyString(), any(GetRunRequest.class))) + .thenThrow(new ApiClientApiException("Internal Server Error", expectedStatusCode, "internal error body")); + + DetectController controller = createDetectControllerWithMock(mockApiClient); + GetDetectRunRequest request = GetDetectRunRequest.builder().runId("run-abc").build(); + + try { + controller.getDetectRun(request); + Assert.fail(EXCEPTION_NOT_THROWN); + } catch (SkyflowException e) { + Assert.assertEquals(expectedStatusCode, e.getHttpCode()); + } + } + + // ─── deidentifyFile — validation ────────────────────────────────────────── + + @Test + public void testDeidentifyFile_nullRequest() { + try { + skyflowClient = Skyflow.builder().setLogLevel(LogLevel.DEBUG).addVaultConfig(vaultConfig).build(); + skyflowClient.detect(vaultID).deidentifyFile(null); + Assert.fail(EXCEPTION_NOT_THROWN); + } catch (SkyflowException e) { + Assert.assertEquals(ErrorMessage.EmptyRequestBody.getMessage(), e.getMessage()); + } + } + + @Test + public void testDeidentifyFile_noFileOrPathProvided() { + try { + skyflowClient = Skyflow.builder().setLogLevel(LogLevel.DEBUG).addVaultConfig(vaultConfig).build(); + DeidentifyFileRequest request = DeidentifyFileRequest.builder() + .file(FileInput.builder().build()) + .build(); + skyflowClient.detect(vaultID).deidentifyFile(request); + Assert.fail(EXCEPTION_NOT_THROWN); + } catch (SkyflowException e) { + Assert.assertEquals(ErrorMessage.EmptyFileAndFilePathInDeIdentifyFile.getMessage(), e.getMessage()); + } + } + + // ─── deidentifyFile — happy path ────────────────────────────────────────── + + @Test + public void testDeidentifyFile_successWithTxtFileObject() throws Exception { + File tmpFile = File.createTempFile("test-detect", ".txt"); + tmpFile.deleteOnExit(); + Files.write(tmpFile.toPath(), "hello world".getBytes()); + FilesClient mockFilesClient = Mockito.mock(FilesClient.class); + ApiClient mockApiClient = Mockito.mock(ApiClient.class); + when(mockApiClient.files()).thenReturn(mockFilesClient); + when(mockFilesClient.deidentifyText(any())).thenReturn( + com.skyflow.generated.rest.types.DeidentifyFileResponse.builder().runId("run-txt-001").build()); + when(mockFilesClient.getRun(anyString(), any(GetRunRequest.class), any(RequestOptions.class))) + .thenReturn(buildSuccessDetectRunsResponse()); + + DetectController controller = createDetectControllerWithMock(mockApiClient); + DeidentifyFileRequest request = DeidentifyFileRequest.builder() + .file(FileInput.builder().file(tmpFile).build()).build(); + + DeidentifyFileResponse response = controller.deidentifyFile(request); + Assert.assertNotNull(response); + } + + @Test + public void testDeidentifyFile_successWithFilePath() throws Exception { + File tmpFile = File.createTempFile("test-detect-path", ".txt"); + tmpFile.deleteOnExit(); + Files.write(tmpFile.toPath(), "content".getBytes()); + + FilesClient mockFilesClient = Mockito.mock(FilesClient.class); + ApiClient mockApiClient = Mockito.mock(ApiClient.class); + when(mockApiClient.files()).thenReturn(mockFilesClient); + when(mockFilesClient.deidentifyText(any())).thenReturn( + com.skyflow.generated.rest.types.DeidentifyFileResponse.builder().runId("run-path-001").build()); + when(mockFilesClient.getRun(anyString(), any(GetRunRequest.class), any(RequestOptions.class))) + .thenReturn(buildSuccessDetectRunsResponse()); + + DetectController controller = createDetectControllerWithMock(mockApiClient); + DeidentifyFileRequest request = DeidentifyFileRequest.builder() + .file(FileInput.builder().filePath(tmpFile.getAbsolutePath()).build()).build(); + + DeidentifyFileResponse response = controller.deidentifyFile(request); + Assert.assertNotNull(response); + } + + @Test + public void testDeidentifyFile_successWithOutputFile() throws Exception { + File tmpFile = File.createTempFile("test-detect-out", ".txt"); + tmpFile.deleteOnExit(); + Files.write(tmpFile.toPath(), "content".getBytes()); + + String b64 = Base64.getEncoder().encodeToString("processed content".getBytes()); + DeidentifiedFileOutput outputItem = DeidentifiedFileOutput.builder() + .processedFile(b64) + .processedFileExtension(DeidentifiedFileOutputProcessedFileExtension.TXT) + .build(); + DetectRunsResponse successWithOutput = DetectRunsResponse.builder() + .status(DetectRunsResponseStatus.SUCCESS) + .outputType(DetectRunsResponseOutputType.BASE_64) + .size(1.0f).duration(0.5f) + .output(Collections.singletonList(outputItem)) + .build(); + + FilesClient mockFilesClient = Mockito.mock(FilesClient.class); + ApiClient mockApiClient = Mockito.mock(ApiClient.class); + when(mockApiClient.files()).thenReturn(mockFilesClient); + when(mockFilesClient.deidentifyText(any())).thenReturn( + com.skyflow.generated.rest.types.DeidentifyFileResponse.builder().runId("run-out-001").build()); + when(mockFilesClient.getRun(anyString(), any(GetRunRequest.class), any(RequestOptions.class))) + .thenReturn(successWithOutput); + + DetectController controller = createDetectControllerWithMock(mockApiClient); + DeidentifyFileRequest request = DeidentifyFileRequest.builder() + .file(FileInput.builder().file(tmpFile).build()) + .outputDirectory(System.getProperty("java.io.tmpdir")) + .build(); + + DeidentifyFileResponse response = controller.deidentifyFile(request); + Assert.assertNotNull(response); + } + + // ─── deidentifyFile — IN_PROGRESS timeout ───────────────────────────────── + + @Test + public void testDeidentifyFile_inProgressTimeout() throws Exception { + File tmpFile = File.createTempFile("test-detect-prog", ".txt"); + tmpFile.deleteOnExit(); + Files.write(tmpFile.toPath(), "content".getBytes()); + + FilesClient mockFilesClient = Mockito.mock(FilesClient.class); + ApiClient mockApiClient = Mockito.mock(ApiClient.class); + when(mockApiClient.files()).thenReturn(mockFilesClient); + when(mockFilesClient.deidentifyText(any())).thenReturn( + com.skyflow.generated.rest.types.DeidentifyFileResponse.builder().runId("run-prog-001").build()); + when(mockFilesClient.getRun(anyString(), any(GetRunRequest.class), any(RequestOptions.class))) + .thenReturn(DetectRunsResponse.builder().status(DetectRunsResponseStatus.IN_PROGRESS).build()); + + DetectController controller = createDetectControllerWithMock(mockApiClient); + DeidentifyFileRequest request = DeidentifyFileRequest.builder() + .file(FileInput.builder().file(tmpFile).build()) + .waitTime(1) + .build(); + + DeidentifyFileResponse response = controller.deidentifyFile(request); + Assert.assertNotNull(response); + Assert.assertEquals("IN_PROGRESS", response.getStatus()); + } + + // ─── deidentifyFile — error paths ───────────────────────────────────────── + + @Test + public void testDeidentifyFile_nonExistentFilePath() { + try { + DetectController controller = createDetectControllerWithMock(Mockito.mock(ApiClient.class)); + DeidentifyFileRequest request = DeidentifyFileRequest.builder() + .file(FileInput.builder().filePath("/nonexistent/path/file.txt").build()) + .build(); + controller.deidentifyFile(request); + Assert.fail(EXCEPTION_NOT_THROWN); + } catch (Exception e) { + Assert.assertNotNull(e.getMessage()); + } + } + + @Test + public void testDeidentifyFile_processFileApiError() throws Exception { + File tmpFile = File.createTempFile("test-detect-err", ".txt"); + tmpFile.deleteOnExit(); + Files.write(tmpFile.toPath(), "content".getBytes()); + + FilesClient mockFilesClient = Mockito.mock(FilesClient.class); + ApiClient mockApiClient = Mockito.mock(ApiClient.class); + when(mockApiClient.files()).thenReturn(mockFilesClient); + when(mockFilesClient.deidentifyText(any())) + .thenThrow(new ApiClientApiException("forbidden", 403, "access denied")); + + DetectController controller = createDetectControllerWithMock(mockApiClient); + DeidentifyFileRequest request = DeidentifyFileRequest.builder() + .file(FileInput.builder().file(tmpFile).build()).build(); + + try { + controller.deidentifyFile(request); + Assert.fail(EXCEPTION_NOT_THROWN); + } catch (SkyflowException e) { + Assert.assertEquals(403, e.getHttpCode()); + } + } + + @Test + public void testDeidentifyFile_pollForResultsApiError() throws Exception { + File tmpFile = File.createTempFile("test-detect-poll", ".txt"); + tmpFile.deleteOnExit(); + Files.write(tmpFile.toPath(), "content".getBytes()); + + FilesClient mockFilesClient = Mockito.mock(FilesClient.class); + ApiClient mockApiClient = Mockito.mock(ApiClient.class); + when(mockApiClient.files()).thenReturn(mockFilesClient); + when(mockFilesClient.deidentifyText(any())).thenReturn( + com.skyflow.generated.rest.types.DeidentifyFileResponse.builder().runId("run-poll-err").build()); + when(mockFilesClient.getRun(anyString(), any(GetRunRequest.class), any(RequestOptions.class))) + .thenThrow(new ApiClientApiException("unavailable", 503, "service unavailable")); + + DetectController controller = createDetectControllerWithMock(mockApiClient); + DeidentifyFileRequest request = DeidentifyFileRequest.builder() + .file(FileInput.builder().file(tmpFile).build()).build(); + + try { + controller.deidentifyFile(request); + Assert.fail(EXCEPTION_NOT_THROWN); + } catch (SkyflowException e) { + Assert.assertEquals(ErrorMessage.PollingForResultsFailed.getMessage(), e.getMessage()); + } + } + + // ─── processFileByType — all extensions ─────────────────────────────────── + + @Test + public void testDeidentifyFile_pdfExtension() throws Exception { + FilesClient mockFilesClient = Mockito.mock(FilesClient.class); + when(mockFilesClient.deidentifyPdf(any())).thenReturn( + com.skyflow.generated.rest.types.DeidentifyFileResponse.builder().runId("run-pdf").build()); + Assert.assertNotNull(runDeidentifyFileForExtension("pdf", mockFilesClient)); + } + + @Test + public void testDeidentifyFile_mp3Extension() throws Exception { + FilesClient mockFilesClient = Mockito.mock(FilesClient.class); + when(mockFilesClient.deidentifyAudio(any())).thenReturn( + com.skyflow.generated.rest.types.DeidentifyFileResponse.builder().runId("run-mp3").build()); + Assert.assertNotNull(runDeidentifyFileForExtension("mp3", mockFilesClient)); + } + + @Test + public void testDeidentifyFile_jpgExtension() throws Exception { + FilesClient mockFilesClient = Mockito.mock(FilesClient.class); + when(mockFilesClient.deidentifyImage(any())).thenReturn( + com.skyflow.generated.rest.types.DeidentifyFileResponse.builder().runId("run-jpg").build()); + Assert.assertNotNull(runDeidentifyFileForExtension("jpg", mockFilesClient)); + } + + @Test + public void testDeidentifyFile_pptExtension() throws Exception { + FilesClient mockFilesClient = Mockito.mock(FilesClient.class); + when(mockFilesClient.deidentifyPresentation(any())).thenReturn( + com.skyflow.generated.rest.types.DeidentifyFileResponse.builder().runId("run-ppt").build()); + Assert.assertNotNull(runDeidentifyFileForExtension("ppt", mockFilesClient)); + } + + @Test + public void testDeidentifyFile_csvExtension() throws Exception { + FilesClient mockFilesClient = Mockito.mock(FilesClient.class); + when(mockFilesClient.deidentifySpreadsheet(any())).thenReturn( + com.skyflow.generated.rest.types.DeidentifyFileResponse.builder().runId("run-csv").build()); + Assert.assertNotNull(runDeidentifyFileForExtension("csv", mockFilesClient)); + } + + @Test + public void testDeidentifyFile_docExtension() throws Exception { + FilesClient mockFilesClient = Mockito.mock(FilesClient.class); + when(mockFilesClient.deidentifyDocument(any())).thenReturn( + com.skyflow.generated.rest.types.DeidentifyFileResponse.builder().runId("run-doc").build()); + Assert.assertNotNull(runDeidentifyFileForExtension("doc", mockFilesClient)); + } + + @Test + public void testDeidentifyFile_jsonExtension() throws Exception { + FilesClient mockFilesClient = Mockito.mock(FilesClient.class); + when(mockFilesClient.deidentifyStructuredText(any())).thenReturn( + com.skyflow.generated.rest.types.DeidentifyFileResponse.builder().runId("run-json").build()); + Assert.assertNotNull(runDeidentifyFileForExtension("json", mockFilesClient)); + } + + @Test + public void testDeidentifyFile_defaultExtension() throws Exception { + FilesClient mockFilesClient = Mockito.mock(FilesClient.class); + when(mockFilesClient.deidentifyFile(any())).thenReturn( + com.skyflow.generated.rest.types.DeidentifyFileResponse.builder().runId("run-dcm").build()); + Assert.assertNotNull(runDeidentifyFileForExtension("dcm", mockFilesClient)); + } + + // ─── parseDeidentifyFileResponse — wordCharacterCount branch L272-273 ───── + + @Test + public void testGetDetectRun_withWordCharacterCount() throws Exception { + FilesClient mockFilesClient = Mockito.mock(FilesClient.class); + ApiClient mockApiClient = Mockito.mock(ApiClient.class); + when(mockApiClient.files()).thenReturn(mockFilesClient); + + DeidentifiedFileOutput outputItem = DeidentifiedFileOutput.builder().build(); + WordCharacterCount wordCharCount = WordCharacterCount.builder() + .wordCount(10) + .characterCount(55) + .build(); + + DetectRunsResponse fakeRunsResponse = DetectRunsResponse.builder() + .status(DetectRunsResponseStatus.SUCCESS) + .outputType(DetectRunsResponseOutputType.BASE_64) + .size(5.0f) + .duration(0.5f) + .output(Collections.singletonList(outputItem)) + .wordCharacterCount(wordCharCount) + .build(); + + when(mockFilesClient.getRun(anyString(), any(GetRunRequest.class))) + .thenReturn(fakeRunsResponse); + + DetectController controller = createDetectControllerWithMock(mockApiClient); + GetDetectRunRequest request = GetDetectRunRequest.builder().runId("run-wc-001").build(); + + try { + DeidentifyFileResponse response = controller.getDetectRun(request); + Assert.assertNotNull(response); + Assert.assertEquals(10, (int) response.getWordCount()); + Assert.assertEquals(55, (int) response.getCharCount()); + } catch (SkyflowException e) { + Assert.fail(INVALID_EXCEPTION_THROWN + ": " + e.getMessage()); + } + } + + // ─── deidentifyFile — processedFile present, no outputDirectory (lines 146, 168) ─── + + @Test + public void testDeidentifyFile_successWithProcessedFileNoOutputDir() throws Exception { + File tmpFile = File.createTempFile("test-detect-nodir", ".txt"); + tmpFile.deleteOnExit(); + Files.write(tmpFile.toPath(), "content".getBytes()); + + String b64 = Base64.getEncoder().encodeToString("processed content".getBytes()); + DeidentifiedFileOutput outputItem = DeidentifiedFileOutput.builder() + .processedFile(b64) + .processedFileExtension(DeidentifiedFileOutputProcessedFileExtension.TXT) + .build(); + DetectRunsResponse successWithOutput = DetectRunsResponse.builder() + .status(DetectRunsResponseStatus.SUCCESS) + .outputType(DetectRunsResponseOutputType.BASE_64) + .size(1.0f).duration(0.5f) + .output(Collections.singletonList(outputItem)) + .build(); + + FilesClient mockFilesClient = Mockito.mock(FilesClient.class); + ApiClient mockApiClient = Mockito.mock(ApiClient.class); + when(mockApiClient.files()).thenReturn(mockFilesClient); + when(mockFilesClient.deidentifyText(any())).thenReturn( + com.skyflow.generated.rest.types.DeidentifyFileResponse.builder().runId("run-nodir-001").build()); + when(mockFilesClient.getRun(anyString(), any(GetRunRequest.class), any(RequestOptions.class))) + .thenReturn(successWithOutput); + + DetectController controller = createDetectControllerWithMock(mockApiClient); + // no outputDirectory → file written via new File(outputFileName) (line 146) + DeidentifyFileRequest request = DeidentifyFileRequest.builder() + .file(FileInput.builder().file(tmpFile).build()) + .build(); + + DeidentifyFileResponse response = controller.deidentifyFile(request); + Assert.assertNotNull(response); + } + + // ─── pollForResults — IN_PROGRESS retry then SUCCESS (lines 218-229) ───────── + + @Test + public void testDeidentifyFile_inProgressThenSuccess() throws Exception { + File tmpFile = File.createTempFile("test-detect-retry", ".txt"); + tmpFile.deleteOnExit(); + Files.write(tmpFile.toPath(), "content".getBytes()); + + FilesClient mockFilesClient = Mockito.mock(FilesClient.class); + ApiClient mockApiClient = Mockito.mock(ApiClient.class); + when(mockApiClient.files()).thenReturn(mockFilesClient); + when(mockFilesClient.deidentifyText(any())).thenReturn( + com.skyflow.generated.rest.types.DeidentifyFileResponse.builder().runId("run-retry-001").build()); + when(mockFilesClient.getRun(anyString(), any(GetRunRequest.class), any(RequestOptions.class))) + .thenReturn(DetectRunsResponse.builder().status(DetectRunsResponseStatus.IN_PROGRESS).build()) + .thenReturn(buildSuccessDetectRunsResponse()); + + DetectController controller = createDetectControllerWithMock(mockApiClient); + // waitTime=2: first poll IN_PROGRESS → sleeps 1s → second poll SUCCESS + DeidentifyFileRequest request = DeidentifyFileRequest.builder() + .file(FileInput.builder().file(tmpFile).build()) + .waitTime(2) + .build(); + + DeidentifyFileResponse response = controller.deidentifyFile(request); + Assert.assertNotNull(response); + Assert.assertFalse("IN_PROGRESS".equals(response.getStatus())); + } + + // ─── pollForResults — IN_PROGRESS retry (else branch, lines 225-226) ──────── + + @Test + public void testDeidentifyFile_inProgressElseBranchThenSuccess() throws Exception { + File tmpFile = File.createTempFile("test-detect-else", ".txt"); + tmpFile.deleteOnExit(); + Files.write(tmpFile.toPath(), "content".getBytes()); + + FilesClient mockFilesClient = Mockito.mock(FilesClient.class); + ApiClient mockApiClient = Mockito.mock(ApiClient.class); + when(mockApiClient.files()).thenReturn(mockFilesClient); + when(mockFilesClient.deidentifyText(any())).thenReturn( + com.skyflow.generated.rest.types.DeidentifyFileResponse.builder().runId("run-else-001").build()); + when(mockFilesClient.getRun(anyString(), any(GetRunRequest.class), any(RequestOptions.class))) + .thenReturn(DetectRunsResponse.builder().status(DetectRunsResponseStatus.IN_PROGRESS).build()) + .thenReturn(buildSuccessDetectRunsResponse()); + + DetectController controller = createDetectControllerWithMock(mockApiClient); + // waitTime=3: currentWaitTime=1, nextWaitTime=2 < 3 → else branch (L225-226), sleep 2s + DeidentifyFileRequest request = DeidentifyFileRequest.builder() + .file(FileInput.builder().file(tmpFile).build()) + .waitTime(3) + .build(); + + DeidentifyFileResponse response = controller.deidentifyFile(request); + Assert.assertNotNull(response); + } + + // ─── parseDeidentifyFileResponse — processedFile present branch L283-291 ── + + @Test + public void testGetDetectRun_withProcessedFile() throws Exception { + FilesClient mockFilesClient = Mockito.mock(FilesClient.class); + ApiClient mockApiClient = Mockito.mock(ApiClient.class); + when(mockApiClient.files()).thenReturn(mockFilesClient); + + String base64Content = Base64.getEncoder().encodeToString("test file content".getBytes()); + DeidentifiedFileOutput outputItem = DeidentifiedFileOutput.builder() + .processedFile(base64Content) + .processedFileExtension(DeidentifiedFileOutputProcessedFileExtension.TXT) + .build(); + + DetectRunsResponse fakeRunsResponse = DetectRunsResponse.builder() + .status(DetectRunsResponseStatus.SUCCESS) + .outputType(DetectRunsResponseOutputType.BASE_64) + .size(1.0f) + .duration(0.1f) + .output(Collections.singletonList(outputItem)) + .build(); + + when(mockFilesClient.getRun(anyString(), any(GetRunRequest.class))) + .thenReturn(fakeRunsResponse); + + DetectController controller = createDetectControllerWithMock(mockApiClient); + GetDetectRunRequest request = GetDetectRunRequest.builder().runId("run-file-001").build(); + + try { + DeidentifyFileResponse response = controller.getDetectRun(request); + Assert.assertNotNull(response); + Assert.assertEquals("run-file-001", response.getRunId()); + } catch (SkyflowException e) { + Assert.fail(INVALID_EXCEPTION_THROWN + ": " + e.getMessage()); + } + } + + // ─── entityUniqueCounter branches in VaultClient request builders ───────── + + private static TokenFormat buildEntityUniqueCounterTokenFormat() { + return TokenFormat.builder() + .entityUniqueCounter(java.util.Collections.singletonList(DetectEntities.EMAIL_ADDRESS)) + .build(); + } + + @Test + public void testDeidentifyFile_txt_withEntityUniqueCounter() throws Exception { + FilesClient mockFilesClient = Mockito.mock(FilesClient.class); + when(mockFilesClient.deidentifyText(any())).thenReturn( + com.skyflow.generated.rest.types.DeidentifyFileResponse.builder().runId("run-euc-txt").build()); + Assert.assertNotNull(runDeidentifyFileForExtension("txt", mockFilesClient, buildEntityUniqueCounterTokenFormat())); + } + + @Test + public void testDeidentifyFile_mp3_withEntityUniqueCounter() throws Exception { + FilesClient mockFilesClient = Mockito.mock(FilesClient.class); + when(mockFilesClient.deidentifyAudio(any())).thenReturn( + com.skyflow.generated.rest.types.DeidentifyFileResponse.builder().runId("run-euc-mp3").build()); + Assert.assertNotNull(runDeidentifyFileForExtension("mp3", mockFilesClient, buildEntityUniqueCounterTokenFormat())); + } + + @Test + public void testDeidentifyFile_pdf_withEntityUniqueCounter() throws Exception { + FilesClient mockFilesClient = Mockito.mock(FilesClient.class); + when(mockFilesClient.deidentifyPdf(any())).thenReturn( + com.skyflow.generated.rest.types.DeidentifyFileResponse.builder().runId("run-euc-pdf").build()); + Assert.assertNotNull(runDeidentifyFileForExtension("pdf", mockFilesClient, buildEntityUniqueCounterTokenFormat())); + } + + @Test + public void testDeidentifyFile_jpg_withEntityUniqueCounter() throws Exception { + FilesClient mockFilesClient = Mockito.mock(FilesClient.class); + when(mockFilesClient.deidentifyImage(any())).thenReturn( + com.skyflow.generated.rest.types.DeidentifyFileResponse.builder().runId("run-euc-jpg").build()); + Assert.assertNotNull(runDeidentifyFileForExtension("jpg", mockFilesClient, buildEntityUniqueCounterTokenFormat())); + } + + @Test + public void testDeidentifyFile_csv_withEntityUniqueCounter() throws Exception { + FilesClient mockFilesClient = Mockito.mock(FilesClient.class); + when(mockFilesClient.deidentifySpreadsheet(any())).thenReturn( + com.skyflow.generated.rest.types.DeidentifyFileResponse.builder().runId("run-euc-csv").build()); + Assert.assertNotNull(runDeidentifyFileForExtension("csv", mockFilesClient, buildEntityUniqueCounterTokenFormat())); + } + + @Test + public void testDeidentifyFile_dcm_withEntityUniqueCounter() throws Exception { + FilesClient mockFilesClient = Mockito.mock(FilesClient.class); + when(mockFilesClient.deidentifyFile(any())).thenReturn( + com.skyflow.generated.rest.types.DeidentifyFileResponse.builder().runId("run-euc-dcm").build()); + Assert.assertNotNull(runDeidentifyFileForExtension("dcm", mockFilesClient, buildEntityUniqueCounterTokenFormat())); + } +} diff --git a/src/test/java/com/skyflow/vault/controller/VaultControllerTests.java b/src/test/java/com/skyflow/vault/controller/VaultControllerTests.java index 77a8ed95..4c1ecb2b 100644 --- a/src/test/java/com/skyflow/vault/controller/VaultControllerTests.java +++ b/src/test/java/com/skyflow/vault/controller/VaultControllerTests.java @@ -1,29 +1,79 @@ package com.skyflow.vault.controller; import com.skyflow.Skyflow; +import com.skyflow.VaultClient; import com.skyflow.config.Credentials; import com.skyflow.config.VaultConfig; import com.skyflow.enums.Env; import com.skyflow.enums.LogLevel; +import com.skyflow.enums.RedactionType; import com.skyflow.errors.ErrorCode; import com.skyflow.errors.ErrorMessage; import com.skyflow.errors.HttpStatus; import com.skyflow.errors.SkyflowException; import com.skyflow.generated.rest.ApiClient; +import com.skyflow.generated.rest.core.ApiClientApiException; +import com.skyflow.generated.rest.core.ApiClientHttpResponse; +import com.skyflow.generated.rest.resources.query.QueryClient; +import com.skyflow.generated.rest.resources.records.RawRecordsClient; +import com.skyflow.generated.rest.resources.records.RecordsClient; +import com.skyflow.generated.rest.resources.tokens.RawTokensClient; +import com.skyflow.generated.rest.resources.tokens.TokensClient; +import com.skyflow.generated.rest.types.UploadFileV2Response; +import com.skyflow.generated.rest.types.V1BatchOperationResponse; +import com.skyflow.generated.rest.types.V1BulkDeleteRecordResponse; +import com.skyflow.generated.rest.types.V1BulkGetRecordResponse; +import com.skyflow.generated.rest.types.V1DetokenizeRecordResponse; +import com.skyflow.generated.rest.types.V1DetokenizeResponse; 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.V1RecordMetaProperties; +import com.skyflow.generated.rest.types.V1TokenizeRecordResponse; +import com.skyflow.generated.rest.types.V1TokenizeResponse; +import com.skyflow.generated.rest.types.V1UpdateRecordResponse; import com.skyflow.utils.Constants; import com.skyflow.utils.Utils; -import com.skyflow.vault.data.*; +import com.skyflow.vault.data.DeleteRequest; +import com.skyflow.vault.data.DeleteResponse; +import com.skyflow.vault.data.FileUploadRequest; +import com.skyflow.vault.data.FileUploadResponse; +import com.skyflow.vault.data.GetRequest; +import com.skyflow.vault.data.GetResponse; +import com.skyflow.vault.data.InsertRequest; +import com.skyflow.vault.data.InsertResponse; +import com.skyflow.vault.data.QueryRequest; +import com.skyflow.vault.data.QueryResponse; +import com.skyflow.vault.data.UpdateRequest; +import com.skyflow.vault.data.UpdateResponse; +import com.skyflow.vault.tokens.ColumnValue; +import com.skyflow.vault.tokens.DetokenizeData; import com.skyflow.vault.tokens.DetokenizeRequest; +import com.skyflow.vault.tokens.DetokenizeResponse; import com.skyflow.vault.tokens.TokenizeRequest; +import com.skyflow.vault.tokens.TokenizeResponse; +import okhttp3.Protocol; +import okhttp3.Request; +import okhttp3.Response; import org.junit.Assert; import org.junit.BeforeClass; import org.junit.Test; +import org.mockito.Mockito; +import java.io.File; +import java.lang.reflect.Field; import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; import java.util.HashMap; +import java.util.List; import java.util.Map; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.when; + public class VaultControllerTests { private static final String INVALID_EXCEPTION_THROWN = "Should not have thrown any exception"; private static final String EXCEPTION_NOT_THROWN = "Should have thrown an exception"; @@ -31,7 +81,6 @@ public class VaultControllerTests { private static String clusterID = null; private static VaultConfig vaultConfig = null; private static Skyflow skyflowClient = null; - private ApiClient mockApiClient; @BeforeClass public static void setup() throws SkyflowException, NoSuchMethodException { @@ -47,14 +96,42 @@ public static void setup() throws SkyflowException, NoSuchMethodException { vaultConfig.setEnv(Env.DEV); vaultConfig.setCredentials(credentials); - skyflowClient = Skyflow.builder() .setLogLevel(LogLevel.DEBUG) .addVaultConfig(vaultConfig) .build(); + } + + // --- helpers --- + + private static VaultController createControllerWithMock(ApiClient mockApiClient) throws Exception { + Credentials creds = new Credentials(); + creds.setApiKey("sky-ab123-abcd1234cdef1234abcd4321cdef4321"); + + VaultConfig config = new VaultConfig(); + config.setVaultId(vaultID); + config.setClusterId(clusterID); + config.setEnv(Env.DEV); + VaultController controller = new VaultController(config, creds); + Field f = VaultClient.class.getDeclaredField("apiClient"); + f.setAccessible(true); + f.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(Constants.REQUEST_ID_HEADER_KEY, "req-test-123") + .build(); + } + + // --- validation failure tests (existing) --- + @Test public void testInvalidRequestInInsertMethod() { try { @@ -190,6 +267,8 @@ public void testInvalidRequestInFileUploadMethod() { } } + // --- getFormattedGetRecord / getFormattedQueryRecord tests --- + @Test public void testGetFormattedGetRecordNormalisesSkyflowId() throws Exception { Map fields = new HashMap<>(); @@ -241,6 +320,8 @@ public void testGetFormattedGetRecordNormalisesSkyflowIdInTokensBranch() throws Assert.assertEquals("other token fields should be preserved", "tok-card-abc", result.get("card_number")); } + // --- downloadUrl tests --- + @Test public void testGetRequestDownloadUrlNewForm() { GetRequest request = GetRequest.builder() @@ -291,7 +372,7 @@ public void testDetokenizeRequestDownloadUrlDefaultIsFalse() { Assert.assertFalse("downloadUrl should be false by default", request.getDownloadUrl()); } - // extractUpdateSkyflowId — all cases + // --- extractUpdateSkyflowId tests --- @Test public void testExtractUpdateSkyflowId_onlyCamelCase() throws Exception { @@ -353,4 +434,918 @@ public void testExtractUpdateSkyflowId_bothKeys_removesBothFromMap() throws Exce Assert.assertTrue("other fields should be preserved", data.containsKey("card_number")); } + // --- insert (bulk) --- + + @Test + public void testInsert_bulkSuccess() throws Exception { + ApiClient mockApi = Mockito.mock(ApiClient.class); + RecordsClient mockRecords = Mockito.mock(RecordsClient.class); + when(mockApi.records()).thenReturn(mockRecords); + + V1RecordMetaProperties meta = V1RecordMetaProperties.builder().skyflowId("id-123").build(); + V1InsertRecordResponse insertResp = V1InsertRecordResponse.builder() + .records(Collections.singletonList(meta)) + .build(); + when(mockRecords.recordServiceInsertRecord(anyString(), anyString(), any())).thenReturn(insertResp); + + VaultController controller = createControllerWithMock(mockApi); + + ArrayList> values = new ArrayList<>(); + HashMap row = new HashMap<>(); + row.put("card_number", "4111111111111111"); + values.add(row); + InsertRequest request = InsertRequest.builder().table("test_table").values(values).build(); + + InsertResponse response = controller.insert(request); + + Assert.assertNotNull(INVALID_EXCEPTION_THROWN, response); + Assert.assertNotNull("insertedFields should not be null", response.getInsertedFields()); + Assert.assertEquals(1, response.getInsertedFields().size()); + Assert.assertEquals("id-123", response.getInsertedFields().get(0).get("skyflowId")); + } + + @Test + public void testInsert_bulkApiErrorThrowsSkyflowException() 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())) + .thenThrow(new ApiClientApiException("insert failed", 400, "bad request body")); + + VaultController controller = createControllerWithMock(mockApi); + + ArrayList> values = new ArrayList<>(); + HashMap row = new HashMap<>(); + row.put("card_number", "4111111111111111"); + values.add(row); + InsertRequest request = InsertRequest.builder().table("test_table").values(values).build(); + + try { + controller.insert(request); + Assert.fail(EXCEPTION_NOT_THROWN); + } catch (SkyflowException e) { + Assert.assertEquals(400, e.getHttpCode()); + } + } + + // --- insert (batch / continueOnError) --- + + @Test + public void testInsert_batchSuccess() 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); + + V1BatchOperationResponse batchBody = V1BatchOperationResponse.builder().build(); + Response rawResp = buildOkHttpResponse(); + ApiClientHttpResponse httpResp = new ApiClientHttpResponse<>(batchBody, rawResp); + when(mockRawRecords.recordServiceBatchOperation(anyString(), any(), any())).thenReturn(httpResp); + + VaultController controller = createControllerWithMock(mockApi); + + ArrayList> values = new ArrayList<>(); + HashMap row = new HashMap<>(); + row.put("card_number", "4111111111111111"); + values.add(row); + InsertRequest request = InsertRequest.builder() + .table("test_table") + .values(values) + .continueOnError(true) + .build(); + + InsertResponse response = controller.insert(request); + Assert.assertNotNull(INVALID_EXCEPTION_THROWN, response); + } + + @Test + public void testInsert_batchApiErrorThrowsSkyflowException() 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())) + .thenThrow(new ApiClientApiException("batch failed", 500, "server error")); + + VaultController controller = createControllerWithMock(mockApi); + + ArrayList> values = new ArrayList<>(); + HashMap row = new HashMap<>(); + row.put("card_number", "4111111111111111"); + values.add(row); + InsertRequest request = InsertRequest.builder() + .table("test_table") + .values(values) + .continueOnError(true) + .build(); + + try { + controller.insert(request); + Assert.fail(EXCEPTION_NOT_THROWN); + } catch (SkyflowException e) { + Assert.assertEquals(500, e.getHttpCode()); + } + } + + // --- detokenize --- + + @Test + public void testDetokenize_success() throws Exception { + ApiClient mockApi = Mockito.mock(ApiClient.class); + TokensClient mockTokens = Mockito.mock(TokensClient.class); + RawTokensClient mockRawTokens = Mockito.mock(RawTokensClient.class); + when(mockApi.tokens()).thenReturn(mockTokens); + when(mockTokens.withRawResponse()).thenReturn(mockRawTokens); + + V1DetokenizeRecordResponse detokRecord = V1DetokenizeRecordResponse.builder() + .token("tok-123") + .build(); + V1DetokenizeResponse detokBody = V1DetokenizeResponse.builder() + .records(Collections.singletonList(detokRecord)) + .build(); + Response rawResp = buildOkHttpResponse(); + ApiClientHttpResponse httpResp = new ApiClientHttpResponse<>(detokBody, rawResp); + when(mockRawTokens.recordServiceDetokenize(anyString(), any(), any())).thenReturn(httpResp); + + VaultController controller = createControllerWithMock(mockApi); + + ArrayList detokenizeDataList = new ArrayList<>(); + detokenizeDataList.add(new DetokenizeData("tok-123")); + DetokenizeRequest request = DetokenizeRequest.builder() + .detokenizeData(detokenizeDataList) + .build(); + + DetokenizeResponse response = controller.detokenize(request); + Assert.assertNotNull(INVALID_EXCEPTION_THROWN, response); + Assert.assertNotNull("detokenizedFields should not be null", response.getDetokenizedFields()); + Assert.assertEquals(1, response.getDetokenizedFields().size()); + } + + @Test + public void testDetokenize_apiErrorThrowsSkyflowException() throws Exception { + ApiClient mockApi = Mockito.mock(ApiClient.class); + TokensClient mockTokens = Mockito.mock(TokensClient.class); + RawTokensClient mockRawTokens = Mockito.mock(RawTokensClient.class); + when(mockApi.tokens()).thenReturn(mockTokens); + when(mockTokens.withRawResponse()).thenReturn(mockRawTokens); + when(mockRawTokens.recordServiceDetokenize(anyString(), any(), any())) + .thenThrow(new ApiClientApiException("detokenize failed", 401, "unauthorized")); + + VaultController controller = createControllerWithMock(mockApi); + + ArrayList detokenizeDataList = new ArrayList<>(); + detokenizeDataList.add(new DetokenizeData("tok-bad")); + DetokenizeRequest request = DetokenizeRequest.builder() + .detokenizeData(detokenizeDataList) + .build(); + + try { + controller.detokenize(request); + Assert.fail(EXCEPTION_NOT_THROWN); + } catch (SkyflowException e) { + Assert.assertEquals(401, e.getHttpCode()); + } + } + + // --- get --- + + @Test + public void testGet_success() throws Exception { + ApiClient mockApi = Mockito.mock(ApiClient.class); + RecordsClient mockRecords = Mockito.mock(RecordsClient.class); + when(mockApi.records()).thenReturn(mockRecords); + + Map fields = new HashMap<>(); + fields.put("skyflow_id", "id-get-001"); + fields.put("card_number", "4111111111111111"); + V1FieldRecords fieldRecords = V1FieldRecords.builder().fields(fields).build(); + V1BulkGetRecordResponse getResp = V1BulkGetRecordResponse.builder() + .records(Collections.singletonList(fieldRecords)) + .build(); + when(mockRecords.recordServiceBulkGetRecord(anyString(), anyString(), any(), any())).thenReturn(getResp); + + VaultController controller = createControllerWithMock(mockApi); + + ArrayList ids = new ArrayList<>(); + ids.add("id-get-001"); + GetRequest request = GetRequest.builder().table("test_table").ids(ids).build(); + + GetResponse response = controller.get(request); + Assert.assertNotNull(INVALID_EXCEPTION_THROWN, response); + Assert.assertNotNull("data should not be null", response.getData()); + Assert.assertEquals(1, response.getData().size()); + Assert.assertEquals("id-get-001", response.getData().get(0).get("skyflowId")); + } + + @Test + public void testGet_apiErrorThrowsSkyflowException() throws Exception { + ApiClient mockApi = Mockito.mock(ApiClient.class); + RecordsClient mockRecords = Mockito.mock(RecordsClient.class); + when(mockApi.records()).thenReturn(mockRecords); + when(mockRecords.recordServiceBulkGetRecord(anyString(), anyString(), any(), any())) + .thenThrow(new ApiClientApiException("get failed", 404, "not found")); + + VaultController controller = createControllerWithMock(mockApi); + + ArrayList ids = new ArrayList<>(); + ids.add("id-missing"); + GetRequest request = GetRequest.builder().table("test_table").ids(ids).build(); + + try { + controller.get(request); + Assert.fail(EXCEPTION_NOT_THROWN); + } catch (SkyflowException e) { + Assert.assertEquals(404, e.getHttpCode()); + } + } + + // --- update --- + + @Test + public void testUpdate_success() throws Exception { + ApiClient mockApi = Mockito.mock(ApiClient.class); + RecordsClient mockRecords = Mockito.mock(RecordsClient.class); + when(mockApi.records()).thenReturn(mockRecords); + + V1UpdateRecordResponse updateResp = V1UpdateRecordResponse.builder().skyflowId("id-upd-001").build(); + when(mockRecords.recordServiceUpdateRecord(anyString(), anyString(), anyString(), any(), any())) + .thenReturn(updateResp); + + VaultController controller = createControllerWithMock(mockApi); + + HashMap data = new HashMap<>(); + data.put("skyflowId", "id-upd-001"); + data.put("card_number", "9999999999999999"); + UpdateRequest request = UpdateRequest.builder().table("test_table").data(data).build(); + + UpdateResponse response = controller.update(request); + Assert.assertNotNull(INVALID_EXCEPTION_THROWN, response); + } + + @Test + public void testUpdate_apiErrorThrowsSkyflowException() 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())) + .thenThrow(new ApiClientApiException("update failed", 403, "forbidden")); + + VaultController controller = createControllerWithMock(mockApi); + + HashMap data = new HashMap<>(); + data.put("skyflowId", "id-upd-bad"); + data.put("card_number", "0000000000000000"); + UpdateRequest request = UpdateRequest.builder().table("test_table").data(data).build(); + + try { + controller.update(request); + Assert.fail(EXCEPTION_NOT_THROWN); + } catch (SkyflowException e) { + Assert.assertEquals(403, e.getHttpCode()); + } + } + + // --- delete --- + + @Test + public void testDelete_success() throws Exception { + ApiClient mockApi = Mockito.mock(ApiClient.class); + RecordsClient mockRecords = Mockito.mock(RecordsClient.class); + when(mockApi.records()).thenReturn(mockRecords); + + V1BulkDeleteRecordResponse deleteResp = V1BulkDeleteRecordResponse.builder() + .recordIdResponse(Collections.singletonList("id-del-001")) + .build(); + when(mockRecords.recordServiceBulkDeleteRecord(anyString(), anyString(), any(), any())) + .thenReturn(deleteResp); + + VaultController controller = createControllerWithMock(mockApi); + + ArrayList ids = new ArrayList<>(); + ids.add("id-del-001"); + DeleteRequest request = DeleteRequest.builder().table("test_table").ids(ids).build(); + + DeleteResponse response = controller.delete(request); + Assert.assertNotNull(INVALID_EXCEPTION_THROWN, response); + Assert.assertNotNull("deletedIds should not be null", response.getDeletedIds()); + Assert.assertEquals(1, response.getDeletedIds().size()); + Assert.assertEquals("id-del-001", response.getDeletedIds().get(0)); + } + + @Test + public void testDelete_apiErrorThrowsSkyflowException() 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())) + .thenThrow(new ApiClientApiException("delete failed", 400, "bad id")); + + VaultController controller = createControllerWithMock(mockApi); + + ArrayList ids = new ArrayList<>(); + ids.add("id-bad"); + DeleteRequest request = DeleteRequest.builder().table("test_table").ids(ids).build(); + + try { + controller.delete(request); + Assert.fail(EXCEPTION_NOT_THROWN); + } catch (SkyflowException e) { + Assert.assertEquals(400, e.getHttpCode()); + } + } + + // --- query --- + + @Test + public void testQuery_success() throws Exception { + ApiClient mockApi = Mockito.mock(ApiClient.class); + QueryClient mockQuery = Mockito.mock(QueryClient.class); + when(mockApi.query()).thenReturn(mockQuery); + + Map fields = new HashMap<>(); + fields.put("skyflow_id", "id-qry-001"); + V1FieldRecords fieldRecords = V1FieldRecords.builder().fields(fields).build(); + V1GetQueryResponse queryResp = V1GetQueryResponse.builder() + .records(Collections.singletonList(fieldRecords)) + .build(); + when(mockQuery.queryServiceExecuteQuery(anyString(), any(), any())).thenReturn(queryResp); + + VaultController controller = createControllerWithMock(mockApi); + + QueryRequest request = QueryRequest.builder().query("SELECT * FROM test_table LIMIT 1").build(); + + QueryResponse response = controller.query(request); + Assert.assertNotNull(INVALID_EXCEPTION_THROWN, response); + Assert.assertNotNull("fields should not be null", response.getFields()); + Assert.assertEquals(1, response.getFields().size()); + Assert.assertEquals("id-qry-001", response.getFields().get(0).get("skyflowId")); + } + + @Test + public void testQuery_apiErrorThrowsSkyflowException() 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())) + .thenThrow(new ApiClientApiException("query failed", 400, "invalid sql")); + + VaultController controller = createControllerWithMock(mockApi); + + QueryRequest request = QueryRequest.builder().query("SELECT * FROM test_table LIMIT 1").build(); + + try { + controller.query(request); + Assert.fail(EXCEPTION_NOT_THROWN); + } catch (SkyflowException e) { + Assert.assertEquals(400, e.getHttpCode()); + } + } + + // --- insert (batch) with actual records — covers getFormattedBatchInsertRecord --- + + @Test + public void testInsert_batchSuccessWithRecords() 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); + + // Build a response item whose Body contains a records array with skyflowId and tokens + Map tokens = new HashMap<>(); + tokens.put("card_number", "tok-card-111"); + + Map recordEntry = new HashMap<>(); + recordEntry.put("skyflowId", "id-batch-001"); + recordEntry.put("tokens", tokens); + + List> recordsList = new ArrayList<>(); + recordsList.add(recordEntry); + + Map bodyMap = new HashMap<>(); + bodyMap.put("records", recordsList); + + Map responseItem = new HashMap<>(); + responseItem.put("Body", bodyMap); + + List> responses = new ArrayList<>(); + responses.add(responseItem); + + V1BatchOperationResponse batchBody = V1BatchOperationResponse.builder() + .responses(responses) + .build(); + Response rawResp = buildOkHttpResponse(); + ApiClientHttpResponse httpResp = new ApiClientHttpResponse<>(batchBody, rawResp); + when(mockRawRecords.recordServiceBatchOperation(anyString(), any(), any())).thenReturn(httpResp); + + VaultController controller = createControllerWithMock(mockApi); + + ArrayList> values = new ArrayList<>(); + HashMap row = new HashMap<>(); + row.put("card_number", "4111111111111111"); + values.add(row); + InsertRequest request = InsertRequest.builder() + .table("test_table") + .values(values) + .continueOnError(true) + .build(); + + InsertResponse response = controller.insert(request); + Assert.assertNotNull(INVALID_EXCEPTION_THROWN, response); + Assert.assertNotNull("insertedFields should not be null", response.getInsertedFields()); + Assert.assertEquals(1, response.getInsertedFields().size()); + Assert.assertEquals("id-batch-001", response.getInsertedFields().get(0).get("skyflowId")); + } + + // --- insert (bulk) with tokens in metadata — covers getFormattedBulkInsertRecord tokens branch --- + + @Test + public void testInsert_bulkSuccessWithTokens() throws Exception { + ApiClient mockApi = Mockito.mock(ApiClient.class); + RecordsClient mockRecords = Mockito.mock(RecordsClient.class); + when(mockApi.records()).thenReturn(mockRecords); + + Map tokens = new HashMap<>(); + tokens.put("card_number", "tok-card-456"); + V1RecordMetaProperties meta = V1RecordMetaProperties.builder() + .skyflowId("id-with-tokens") + .tokens(tokens) + .build(); + V1InsertRecordResponse insertResp = V1InsertRecordResponse.builder() + .records(Collections.singletonList(meta)) + .build(); + when(mockRecords.recordServiceInsertRecord(anyString(), anyString(), any())).thenReturn(insertResp); + + VaultController controller = createControllerWithMock(mockApi); + + ArrayList> values = new ArrayList<>(); + HashMap row = new HashMap<>(); + row.put("card_number", "4111111111111111"); + values.add(row); + InsertRequest request = InsertRequest.builder().table("test_table").values(values).build(); + + InsertResponse response = controller.insert(request); + Assert.assertNotNull(INVALID_EXCEPTION_THROWN, response); + Assert.assertNotNull("insertedFields should not be null", response.getInsertedFields()); + Assert.assertEquals(1, response.getInsertedFields().size()); + Assert.assertEquals("id-with-tokens", response.getInsertedFields().get(0).get("skyflowId")); + Assert.assertEquals("tok-card-456", response.getInsertedFields().get(0).get("card_number")); + } + + // --- update with tokens in response — covers lambda$1 (getFormattedUpdateRecord tokens branch) --- + + @Test + public void testUpdate_withTokensInResponse() throws Exception { + ApiClient mockApi = Mockito.mock(ApiClient.class); + RecordsClient mockRecords = Mockito.mock(RecordsClient.class); + when(mockApi.records()).thenReturn(mockRecords); + + Map tokens = new HashMap<>(); + tokens.put("card_number", "tok-upd-card"); + V1UpdateRecordResponse updateResp = V1UpdateRecordResponse.builder() + .skyflowId("id-upd-tok") + .tokens(tokens) + .build(); + when(mockRecords.recordServiceUpdateRecord(anyString(), anyString(), anyString(), any(), any())) + .thenReturn(updateResp); + + VaultController controller = createControllerWithMock(mockApi); + + HashMap data = new HashMap<>(); + data.put("skyflowId", "id-upd-tok"); + data.put("card_number", "4111111111111111"); + UpdateRequest request = UpdateRequest.builder().table("test_table").data(data).build(); + + UpdateResponse response = controller.update(request); + Assert.assertNotNull(INVALID_EXCEPTION_THROWN, response); + Assert.assertNotNull("tokens map should not be null", response.getTokens()); + // skyflowId is put into the tokens map by getFormattedUpdateRecord + Assert.assertEquals("id-upd-tok", response.getTokens().get("skyflowId")); + Assert.assertEquals("tok-upd-card", response.getTokens().get("card_number")); + } + + // --- detokenize with an error record — covers error-record path in detokenize --- + + @Test + public void testDetokenize_errorRecordPath() throws Exception { + ApiClient mockApi = Mockito.mock(ApiClient.class); + TokensClient mockTokens = Mockito.mock(TokensClient.class); + RawTokensClient mockRawTokens = Mockito.mock(RawTokensClient.class); + when(mockApi.tokens()).thenReturn(mockTokens); + when(mockTokens.withRawResponse()).thenReturn(mockRawTokens); + + V1DetokenizeRecordResponse errRecord = V1DetokenizeRecordResponse.builder() + .token("tok-bad") + .error("token not found") + .build(); + V1DetokenizeResponse detokBody = V1DetokenizeResponse.builder() + .records(Collections.singletonList(errRecord)) + .build(); + Response rawResp = buildOkHttpResponse(); + ApiClientHttpResponse httpResp = new ApiClientHttpResponse<>(detokBody, rawResp); + when(mockRawTokens.recordServiceDetokenize(anyString(), any(), any())).thenReturn(httpResp); + + VaultController controller = createControllerWithMock(mockApi); + + ArrayList detokenizeDataList = new ArrayList<>(); + detokenizeDataList.add(new DetokenizeData("tok-bad")); + DetokenizeRequest request = DetokenizeRequest.builder() + .detokenizeData(detokenizeDataList) + .build(); + + DetokenizeResponse response = controller.detokenize(request); + Assert.assertNotNull(INVALID_EXCEPTION_THROWN, response); + Assert.assertNotNull("errors should not be null", response.getErrors()); + Assert.assertEquals(1, response.getErrors().size()); + Assert.assertEquals("tok-bad", response.getErrors().get(0).getToken()); + Assert.assertEquals("token not found", response.getErrors().get(0).getError()); + } + + // --- insert (batch) with deprecated skyflow_id key — covers getFormattedBatchInsertRecord L108-110 --- + + @Test + public void testInsert_batchItemWithDeprecatedSkyflowId() 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); + + Map recordEntry = new HashMap<>(); + recordEntry.put("skyflow_id", "id-deprecated-001"); + + Map bodyMap = new HashMap<>(); + bodyMap.put("records", Collections.singletonList(recordEntry)); + + Map responseItem = new HashMap<>(); + responseItem.put("Body", bodyMap); + + V1BatchOperationResponse batchBody = V1BatchOperationResponse.builder() + .responses(Collections.singletonList(responseItem)) + .build(); + Response rawResp = buildOkHttpResponse(); + ApiClientHttpResponse httpResp = new ApiClientHttpResponse<>(batchBody, rawResp); + when(mockRawRecords.recordServiceBatchOperation(anyString(), any(), any())).thenReturn(httpResp); + + VaultController controller = createControllerWithMock(mockApi); + + ArrayList> values = new ArrayList<>(); + HashMap row = new HashMap<>(); + row.put("card_number", "4111111111111111"); + values.add(row); + InsertRequest request = InsertRequest.builder() + .table("test_table").values(values).continueOnError(true).build(); + + InsertResponse response = controller.insert(request); + Assert.assertNotNull(INVALID_EXCEPTION_THROWN, response); + Assert.assertNotNull("insertedFields should not be null", response.getInsertedFields()); + Assert.assertEquals("id-deprecated-001", response.getInsertedFields().get(0).get("skyflowId")); + } + + // --- insert (batch) mixed result — covers error branch L119-121, L212-214, and L243 --- + + @Test + public void testInsert_batchMixedResult() 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); + + Map successRecord = new HashMap<>(); + successRecord.put("skyflowId", "id-success-001"); + Map successBody = new HashMap<>(); + successBody.put("records", Collections.singletonList(successRecord)); + Map successItem = new HashMap<>(); + successItem.put("Body", successBody); + + Map errorBody = new HashMap<>(); + errorBody.put("error", "validation failed for record 2"); + Map errorItem = new HashMap<>(); + errorItem.put("Body", errorBody); + + V1BatchOperationResponse batchBody = V1BatchOperationResponse.builder() + .responses(Arrays.asList(successItem, errorItem)) + .build(); + Response rawResp = buildOkHttpResponse(); + ApiClientHttpResponse httpResp = new ApiClientHttpResponse<>(batchBody, rawResp); + when(mockRawRecords.recordServiceBatchOperation(anyString(), any(), any())).thenReturn(httpResp); + + VaultController controller = createControllerWithMock(mockApi); + + ArrayList> values = new ArrayList<>(); + HashMap row1 = new HashMap<>(); + row1.put("card_number", "4111111111111111"); + values.add(row1); + HashMap row2 = new HashMap<>(); + row2.put("card_number", "invalid-card"); + values.add(row2); + InsertRequest request = InsertRequest.builder() + .table("test_table").values(values).continueOnError(true).build(); + + InsertResponse response = controller.insert(request); + Assert.assertNotNull(INVALID_EXCEPTION_THROWN, response); + Assert.assertNotNull("insertedFields should not be null", response.getInsertedFields()); + Assert.assertNotNull("errorFields should not be null", response.getErrors()); + Assert.assertEquals(1, response.getInsertedFields().size()); + Assert.assertEquals(1, response.getErrors().size()); + } + + // --- insert (batch) all errors — covers L238 errorFields branch --- + + @Test + public void testInsert_batchAllErrors() 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); + + Map errorBody = new HashMap<>(); + errorBody.put("error", "invalid card data"); + Map errorItem = new HashMap<>(); + errorItem.put("Body", errorBody); + + V1BatchOperationResponse batchBody = V1BatchOperationResponse.builder() + .responses(Collections.singletonList(errorItem)) + .build(); + Response rawResp = buildOkHttpResponse(); + ApiClientHttpResponse httpResp = new ApiClientHttpResponse<>(batchBody, rawResp); + when(mockRawRecords.recordServiceBatchOperation(anyString(), any(), any())).thenReturn(httpResp); + + VaultController controller = createControllerWithMock(mockApi); + + ArrayList> values = new ArrayList<>(); + HashMap row = new HashMap<>(); + row.put("card_number", "bad-card"); + values.add(row); + InsertRequest request = InsertRequest.builder() + .table("test_table").values(values).continueOnError(true).build(); + + InsertResponse response = controller.insert(request); + Assert.assertNotNull(INVALID_EXCEPTION_THROWN, response); + Assert.assertNull("insertedFields should be null when all items fail", response.getInsertedFields()); + Assert.assertNotNull("errors should not be null", response.getErrors()); + Assert.assertEquals(1, response.getErrors().size()); + } + + // --- detokenize with empty records list — covers L288 null branch --- + + @Test + public void testDetokenize_emptyRecordsList() throws Exception { + ApiClient mockApi = Mockito.mock(ApiClient.class); + TokensClient mockTokens = Mockito.mock(TokensClient.class); + RawTokensClient mockRawTokens = Mockito.mock(RawTokensClient.class); + when(mockApi.tokens()).thenReturn(mockTokens); + when(mockTokens.withRawResponse()).thenReturn(mockRawTokens); + + V1DetokenizeResponse detokBody = V1DetokenizeResponse.builder() + .records(Collections.emptyList()) + .build(); + Response rawResp = buildOkHttpResponse(); + ApiClientHttpResponse httpResp = new ApiClientHttpResponse<>(detokBody, rawResp); + when(mockRawTokens.recordServiceDetokenize(anyString(), any(), any())).thenReturn(httpResp); + + VaultController controller = createControllerWithMock(mockApi); + + ArrayList detokenizeDataList = new ArrayList<>(); + detokenizeDataList.add(new DetokenizeData("tok-empty")); + DetokenizeRequest request = DetokenizeRequest.builder() + .detokenizeData(detokenizeDataList) + .build(); + + DetokenizeResponse response = controller.detokenize(request); + Assert.assertNotNull(INVALID_EXCEPTION_THROWN, response); + Assert.assertNull("detokenizedFields should be null when records list is empty", response.getDetokenizedFields()); + Assert.assertNull("errors should be null when records list is empty", response.getErrors()); + } + + // --- get with redactionType set — covers L308 non-null branch --- + + @Test + public void testGet_successWithRedactionType() throws Exception { + ApiClient mockApi = Mockito.mock(ApiClient.class); + RecordsClient mockRecords = Mockito.mock(RecordsClient.class); + when(mockApi.records()).thenReturn(mockRecords); + + Map fields = new HashMap<>(); + fields.put("skyflow_id", "id-redact-001"); + V1FieldRecords fieldRecords = V1FieldRecords.builder().fields(fields).build(); + V1BulkGetRecordResponse getResp = V1BulkGetRecordResponse.builder() + .records(Collections.singletonList(fieldRecords)) + .build(); + when(mockRecords.recordServiceBulkGetRecord(anyString(), anyString(), any(), any())).thenReturn(getResp); + + VaultController controller = createControllerWithMock(mockApi); + + ArrayList ids = new ArrayList<>(); + ids.add("id-redact-001"); + GetRequest request = GetRequest.builder() + .table("test_table") + .ids(ids) + .redactionType(RedactionType.PLAIN_TEXT) + .build(); + + GetResponse response = controller.get(request); + Assert.assertNotNull(INVALID_EXCEPTION_THROWN, response); + Assert.assertNotNull("data should not be null", response.getData()); + Assert.assertEquals(1, response.getData().size()); + } + + // --- detokenize with mixed success + error — covers L293 --- + + @Test + public void testDetokenize_mixedSuccessAndError() throws Exception { + ApiClient mockApi = Mockito.mock(ApiClient.class); + TokensClient mockTokens = Mockito.mock(TokensClient.class); + RawTokensClient mockRawTokens = Mockito.mock(RawTokensClient.class); + when(mockApi.tokens()).thenReturn(mockTokens); + when(mockTokens.withRawResponse()).thenReturn(mockRawTokens); + + V1DetokenizeRecordResponse goodRecord = V1DetokenizeRecordResponse.builder() + .token("tok-good") + .build(); + V1DetokenizeRecordResponse badRecord = V1DetokenizeRecordResponse.builder() + .token("tok-bad") + .error("token not found") + .build(); + V1DetokenizeResponse detokBody = V1DetokenizeResponse.builder() + .records(Arrays.asList(goodRecord, badRecord)) + .build(); + Response rawResp = buildOkHttpResponse(); + ApiClientHttpResponse httpResp = new ApiClientHttpResponse<>(detokBody, rawResp); + when(mockRawTokens.recordServiceDetokenize(anyString(), any(), any())).thenReturn(httpResp); + + VaultController controller = createControllerWithMock(mockApi); + + ArrayList detokenizeDataList = new ArrayList<>(); + detokenizeDataList.add(new DetokenizeData("tok-good")); + detokenizeDataList.add(new DetokenizeData("tok-bad")); + DetokenizeRequest request = DetokenizeRequest.builder() + .detokenizeData(detokenizeDataList) + .build(); + + DetokenizeResponse response = controller.detokenize(request); + Assert.assertNotNull(INVALID_EXCEPTION_THROWN, response); + Assert.assertNotNull("detokenizedFields should not be null", response.getDetokenizedFields()); + Assert.assertNotNull("errors should not be null", response.getErrors()); + Assert.assertEquals(1, response.getDetokenizedFields().size()); + Assert.assertEquals(1, response.getErrors().size()); + } + + // --- tokenize --- + + @Test + public void testTokenize_success() throws Exception { + ApiClient mockApi = Mockito.mock(ApiClient.class); + TokensClient mockTokens = Mockito.mock(TokensClient.class); + when(mockApi.tokens()).thenReturn(mockTokens); + + V1TokenizeRecordResponse tokenRecord = V1TokenizeRecordResponse.builder().token("tok-abc").build(); + V1TokenizeResponse tokenResp = V1TokenizeResponse.builder() + .records(Collections.singletonList(tokenRecord)) + .build(); + when(mockTokens.recordServiceTokenize(anyString(), any(), any())).thenReturn(tokenResp); + + VaultController controller = createControllerWithMock(mockApi); + + ColumnValue cv = ColumnValue.builder().value("test-val").columnGroup("test-group").build(); + TokenizeRequest request = TokenizeRequest.builder() + .values(Collections.singletonList(cv)) + .build(); + + TokenizeResponse response = controller.tokenize(request); + Assert.assertNotNull(INVALID_EXCEPTION_THROWN, response); + Assert.assertNotNull("tokens should not be null", response.getTokens()); + Assert.assertEquals(1, response.getTokens().size()); + Assert.assertEquals("tok-abc", response.getTokens().get(0)); + } + + @Test + public void testTokenize_apiErrorThrowsSkyflowException() throws Exception { + ApiClient mockApi = Mockito.mock(ApiClient.class); + TokensClient mockTokens = Mockito.mock(TokensClient.class); + when(mockApi.tokens()).thenReturn(mockTokens); + when(mockTokens.recordServiceTokenize(anyString(), any(), any())) + .thenThrow(new ApiClientApiException("tokenize failed", 422, "unprocessable")); + + VaultController controller = createControllerWithMock(mockApi); + + ColumnValue cv = ColumnValue.builder().value("test-val").columnGroup("test-group").build(); + TokenizeRequest request = TokenizeRequest.builder() + .values(Collections.singletonList(cv)) + .build(); + + try { + controller.tokenize(request); + Assert.fail(EXCEPTION_NOT_THROWN); + } catch (SkyflowException e) { + Assert.assertEquals(422, e.getHttpCode()); + } + } + + // ─── uploadFile — getFileForFileUpload all three input paths ────────────── + + @Test + public void testUploadFile_withFilePath() throws Exception { + File tmpFile = File.createTempFile("upload-test-path", ".txt"); + tmpFile.deleteOnExit(); + java.nio.file.Files.write(tmpFile.toPath(), "data".getBytes()); + + ApiClient mockApi = Mockito.mock(ApiClient.class); + RecordsClient mockRecords = Mockito.mock(RecordsClient.class); + when(mockApi.records()).thenReturn(mockRecords); + UploadFileV2Response uploadResp = UploadFileV2Response.builder() + .skyflowId(java.util.Optional.of("sky-id-path")).build(); + when(mockRecords.uploadFileV2(anyString(), any(File.class), any(), any())).thenReturn(uploadResp); + + VaultController controller = createControllerWithMock(mockApi); + FileUploadRequest request = FileUploadRequest.builder() + .table("files_table") + .columnName("file_col") + .filePath(tmpFile.getAbsolutePath()) + .build(); + + FileUploadResponse response = controller.uploadFile(request); + Assert.assertNotNull(INVALID_EXCEPTION_THROWN, response); + Assert.assertEquals("sky-id-path", response.getSkyflowId()); + } + + @Test + public void testUploadFile_withBase64() throws Exception { + String b64 = java.util.Base64.getEncoder().encodeToString("file content".getBytes()); + + ApiClient mockApi = Mockito.mock(ApiClient.class); + RecordsClient mockRecords = Mockito.mock(RecordsClient.class); + when(mockApi.records()).thenReturn(mockRecords); + UploadFileV2Response uploadResp = UploadFileV2Response.builder() + .skyflowId(java.util.Optional.of("sky-id-b64")).build(); + when(mockRecords.uploadFileV2(anyString(), any(File.class), any(), any())).thenReturn(uploadResp); + + VaultController controller = createControllerWithMock(mockApi); + FileUploadRequest request = FileUploadRequest.builder() + .table("files_table") + .columnName("file_col") + .base64(b64) + .fileName("test.txt") + .build(); + + FileUploadResponse response = controller.uploadFile(request); + Assert.assertNotNull(INVALID_EXCEPTION_THROWN, response); + Assert.assertEquals("sky-id-b64", response.getSkyflowId()); + } + + @Test + public void testUploadFile_withFileObject() throws Exception { + File tmpFile = File.createTempFile("upload-test-obj", ".txt"); + tmpFile.deleteOnExit(); + java.nio.file.Files.write(tmpFile.toPath(), "data".getBytes()); + + ApiClient mockApi = Mockito.mock(ApiClient.class); + RecordsClient mockRecords = Mockito.mock(RecordsClient.class); + when(mockApi.records()).thenReturn(mockRecords); + UploadFileV2Response uploadResp = UploadFileV2Response.builder() + .skyflowId(java.util.Optional.of("sky-id-obj")).build(); + when(mockRecords.uploadFileV2(anyString(), any(File.class), any(), any())).thenReturn(uploadResp); + + VaultController controller = createControllerWithMock(mockApi); + FileUploadRequest request = FileUploadRequest.builder() + .table("files_table") + .columnName("file_col") + .fileObject(tmpFile) + .build(); + + FileUploadResponse response = controller.uploadFile(request); + Assert.assertNotNull(INVALID_EXCEPTION_THROWN, response); + Assert.assertEquals("sky-id-obj", response.getSkyflowId()); + } + + @Test + public void testUploadFile_apiError() throws Exception { + File tmpFile = File.createTempFile("upload-test-err", ".txt"); + tmpFile.deleteOnExit(); + java.nio.file.Files.write(tmpFile.toPath(), "data".getBytes()); + + ApiClient mockApi = Mockito.mock(ApiClient.class); + RecordsClient mockRecords = Mockito.mock(RecordsClient.class); + when(mockApi.records()).thenReturn(mockRecords); + when(mockRecords.uploadFileV2(anyString(), any(File.class), any(), any())) + .thenThrow(new ApiClientApiException("upload failed", 403, "forbidden")); + + VaultController controller = createControllerWithMock(mockApi); + FileUploadRequest request = FileUploadRequest.builder() + .table("files_table") + .columnName("file_col") + .filePath(tmpFile.getAbsolutePath()) + .build(); + + try { + controller.uploadFile(request); + Assert.fail(EXCEPTION_NOT_THROWN); + } catch (SkyflowException e) { + Assert.assertEquals(403, e.getHttpCode()); + } + } }