diff --git a/.cspell.json b/.cspell.json
index a0ec0be6..757f0c48 100644
--- a/.cspell.json
+++ b/.cspell.json
@@ -73,9 +73,32 @@
"pkcs",
"prioritise",
"Prioritise",
+ "prioritised",
"Timeto",
"Wdex",
- "jacoco"
+ "jacoco",
+ "serialise",
+ "serialised",
+ "serialises",
+ "serialising",
+ "normalise",
+ "Normalise",
+ "normalised",
+ "normalises",
+ "Normalises",
+ "normalising",
+ "behaviour",
+ "Behaviour",
+ "behaviours",
+ "sanitisation",
+ "recognised",
+ "unrecognised",
+ "nocreds",
+ "nodir",
+ "detok",
+ "qhdmceurtnlz",
+ "ngrok",
+ "obac"
],
"languageSettings": [
{
@@ -98,7 +121,9 @@
"src/main/java/com/skyflow/generated/**",
"**/*.ts",
"**/processed-*",
- "samples/src/main/java/com/example/credentials.json"
+ "samples/src/main/java/com/example/credentials.json",
+ "RUNNING_SAMPLES.md",
+ "docs/superpowers/**"
],
"ignoreRegExpList": [
"/\\b[A-Z][A-Z0-9_]{2,}\\b/g",
@@ -106,6 +131,7 @@
"/(eyJ[A-Za-z0-9+/=_-]+\\.)+[A-Za-z0-9+/=_-]+/g",
"/[A-Za-z0-9_.~-]*%[0-9A-Fa-f]{2}[A-Za-z0-9_.~%-]*/g",
"/\\b[A-Za-z0-9_]{7,}\\b(?=])/g",
- "/\"[A-Za-z0-9+/=]{15,}\"/g"
+ "/\"[A-Za-z0-9+/=]{15,}\"/g",
+ "/-D[A-Za-z][A-Za-z0-9.]*/g"
]
}
diff --git a/.gitignore b/.gitignore
index d5a178d4..0efc6bd4 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,3 +1,7 @@
.idea
-target
\ No newline at end of file
+target
+
+RUNNING_SAMPLES.md
+
+docs/superpowers/
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 10606856..9cf57026 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,103 +1,5 @@
# Changelog
-All notable changes to this project will be documented in this file.
-## [1.15.0] - 2024-08-01
-### Added
-- insert data using bulk operation `insertBulk`
+All notable changes to this project will be documented as part of the release notes.
-## [1.14.0] - 2024-02-01
-### Fixed
-- handling of detokenize response to avoid breaking changes.
-
-## [1.13.0] - 2024-01-10
-### Added
-- Continue on error support for batch Insert.
-
-## [1.12.1] - 2023-11-09
-### Fixed
-- Static Bearer token being used for multiple Skyflow Client instances.
-
-## [1.12.0] - 2023-10-25
-### Added
-- `tokens` support in Get Method
-
-## [1.11.0] - 2023-09-01
-### Added
-- `query` vault API
-
-## [1.10.0] - 2023-08-09
-- Added `delete` vault API support.
-## [1.9.0] - 2023-06-08
-### Added
-- `redaction` key for detokenize method for column group support.
-
-## [1.8.2] - 2023-03-20
-### Fixed
-- removed grace period logic for bearer token generation.
-
-## [1.8.1] - 2023-03-01
-### Fixed
-- java cached token bug
-
-## [1.8.0] - 2023-01-10
-### Added
-- `update` vault API
-- `get` vault API
-
-## [1.7.1] - 2022-11-29
-### Changed
-- `setContext` to `setCtx` method.
-- `setTimetoLive` accepts seconds in `Integer` instead of `Double`.
-
-## [1.7.0] - 2022-11-22
-### Added
-- `upsert` support for insert method.
-
-## [1.6.0] - 2022-10-11
-
-### Added
-- Added Support for Context Aware Authorization.
-- Added Support to generate scoped skyflow bearer tokens.
-## [1.5.0] - 2022-04-12
-
-### Added
-- support for application/x-www-form-urlencoded and multipart/form-data content-type's in connections.
-
-## [1.4.1] - 2022-03-29
-
-### Fixed
-- Request headers not getting overridden due to case sensitivity
-
-## [1.4.0] - 2022-03-15
-
-### Changed
-
-- deprecated `isValid` in favour of `isExpired`
-
-## [1.3.0] - 2022-02-24
-
-### Added
-
-- `requestId` in error logs and error responses for API Errors
-- `isValid` method for validating Service Account bearer token
-
-## [1.2.0] - 2022-01-11
-
-### Added
-- Logging functionality
-- `Configuration.setLogLevel` function for setting the package-level LogLevel
-- `generateBearerTokenFromCreds` function which takes credentials as string
-
-### Changed
-- Renamed and deprecated `GenerateToken` in favor of `generateBearerToken`
-- `vaultID` and `vaultURL` are optional in `SkyflowConfiguration` constructor
-
-## [1.1.0] - 2021-11-10
-### Added
-- `insert` vault API
-- `detokenize` vault API
-- `getById` vault API
-- `invokeConnection`
-## [1.0.1] - 2021-10-20
-### Added
-- Service Account Token generation
\ No newline at end of file
+See [GitHub](https://github.com/skyflowapi/skyflow-java/releases) or [Maven](https://mvnrepository.com/artifact/com.skyflow/skyflow-java) for more details on each released version.
diff --git a/README.md b/README.md
index 3f8a9adb..e041ca2e 100644
--- a/README.md
+++ b/README.md
@@ -1,5 +1,9 @@
# Skyflow Java
+> **This is the current, recommended version of the Skyflow SDK.** V2.1.0 brings flexible auth, multi-vault support, builder patterns, native data types, and rich error diagnostics.
+>
+> Migrating from v1? See the **[Migration Guide](docs/migrate_to_v2.md)** for step-by-step instructions. V1 is in maintenance mode and will reach End of Life on October 31, 2026.
+
The Skyflow Java SDK is designed to help with integrating Skyflow into a Java backend.
[](https://github.com/skyflowapi/skyflow-java/actions)
@@ -15,12 +19,7 @@ The Skyflow Java SDK is designed to help with integrating Skyflow into a Java ba
- [Configuration](#configuration)
- [Gradle users](#gradle-users)
- [Maven users](#maven-users)
-- [Migration from v1 to v2](#migration-from-v1-to-v2)
- - [Authentication options](#authentication-options)
- - [Initializing the client](#initializing-the-client)
- - [Request & response structure](#request--response-structure)
- - [Request options](#request-options)
- - [Error structure](#error-structure)
+- [Migration from v1 to v2](docs/migrate_to_v2.md)
- [Quickstart](#quickstart)
- [Authenticate](#authenticate)
- [Initialize the client](#initialize-the-client)
@@ -51,6 +50,9 @@ The Skyflow Java SDK is designed to help with integrating Skyflow into a Java ba
- [Generate scoped bearer tokens](#generate-scoped-bearer-tokens)
- [Generate signed data tokens](#generate-signed-data-tokens)
- [Bearer token expiry edge case](#bearer-token-expiry-edge-case)
+- [Error Handling](#error-handling)
+ - [Catching SkyflowException](#catching-skyflowexception)
+ - [SkyflowException properties](#skyflowexception-properties)
- [Logging](#logging)
- [Reporting a Vulnerability](#reporting-a-vulnerability)
@@ -94,265 +96,7 @@ Add this dependency to your project's `pom.xml` file:
# Migrate from v1 to v2
-Below are the steps to migrate the java sdk from v1 to v2.
-
-### Authentication options
-
-In V2, we have introduced multiple authentication options. You can now provide credentials in the following ways:
-
-- Passing credentials in ENV. (`SKYFLOW_CREDENTIALS`) _(Recommended)_
-- API Key
-- Path to your credentials JSON file
-- Stringified JSON of your credentials
-- Bearer token
-
-These options allow you to choose the authentication method that best suits your use case.
-
-**V1 (Old)**
-
-```java
-static class DemoTokenProvider implements TokenProvider {
- @Override
- public String getBearerToken() throws Exception {
- ResponseToken res = null;
- try {
- String filePath = "";
- res = Token.generateBearerToken(filePath);
- } catch (SkyflowException e) {
- e.printStackTrace();
- }
- return res.getAccessToken();
- }
-}
-```
-
-**V2 (New): Passing one of the following:**
-
-```java
-// Option 1: API Key (Recommended)
-Credentials skyflowCredentials = new Credentials();
-skyflowCredentials.setApiKey(""); // Replace with your actual API key
-
-// Option 2: Environment Variables (Recommended)
-// Set SKYFLOW_CREDENTIALS in your environment
-
-// Option 3: Credentials File
-skyflowCredentials.setPath(""); // Replace with the path to credentials file
-
-// Option 4: Stringified JSON
-skyflowCredentials.setCredentialsString(""); // Replace with the credentials string
-
-// Option 5: Bearer Token
-skyflowCredentials.setToken(""); // Replace with your actual authentication token.
-```
-
-Notes:
-
-- Use only ONE authentication method.
-- API Key or environment variables are recommended for production use.
-- Secure storage of credentials is essential.
-- For overriding behavior and priority order of credentials, please refer to [Initialize the client](#initialize-the-client) section in [Quickstart](#quickstart).
-
----
-
-### Initializing the client
-
-In V2, we have introduced a builder design pattern for client initialization and added support for multi-vault. This allows you to configure multiple vaults during client initialization. In V2, the log level is tied to each individual client instance. During client initialization, you can pass the following parameters:
-
-- `vaultId` and `clusterId`: These values are derived from the vault ID & vault URL.
-- `env`: Specify the environment (e.g., SANDBOX or PROD).
-- `credentials`: The necessary authentication credentials.
-
-**V1 (Old)**
-
-```java
-// DemoTokenProvider class is an implementation of the TokenProvider interface
-DemoTokenProvider demoTokenProvider = new DemoTokenProvider();
-SkyflowConfiguration skyflowConfig = new SkyflowConfiguration("","", demoTokenProvider);
-Skyflow skyflowClient = Skyflow.init(skyflowConfig);
-```
-
-**V2 (New)**
-
-```java
-Credentials credentials = new Credentials();
-credentials.setPath(""); // Replace with the path to the credentials file
-
-// Configure the first vault (Blitz)
-VaultConfig config = new VaultConfig();
-config.setVaultId(""); // Replace with the ID of the first vault
-config.setClusterId(""); // Replace with the cluster ID of the first vault
-config.setEnv(Env.DEV); // Set the environment (e.g., DEV, STAGE, PROD)
-config.setCredentials(credentials); // Associate the credentials with the vault
-
-// Set up credentials for the Skyflow client
-Credentials skyflowCredentials = new Credentials();
-skyflowCredentials.setPath(""); // Replace with the path to another credentials file
-
-// Create a Skyflow client and add vault configurations
-Skyflow skyflowClient = Skyflow.builder()
- .setLogLevel(LogLevel.DEBUG) // Enable debugging for detailed logs
- .addVaultConfig(config) // Add the first vault configuration
- .addSkyflowCredentials(skyflowCredentials) // Add general Skyflow credentials
- .build();
-```
-
-**Key Changes:**
-
-- `vaultUrl` replaced with `clusterId`.
-- Added environment specification (`env`).
-- Instance-specific log levels.
-
----
-
-### Request & response structure
-
-In V2, we have removed the use of JSON objects from a third-party package. Instead, we have transitioned to accepting native ArrayList and HashMap data structures and adopted the builder pattern for request creation. This request needs:
-
-- `table`: The name of the table.
-- `values`: An array list of objects containing the data to be inserted.
-
-The response will be of type `InsertResponse` class, which contains `insertedFields` and `errors`.
-
-**V1 (Old):** Request building
-
-```java
-JSONObject recordsJson = new JSONObject();
-JSONArray recordsArrayJson = new JSONArray();
-
-JSONObject recordJson = new JSONObject();
-recordJson.put("table", "cards");
-
-JSONObject fieldsJson = new JSONObject();
-fields.put("cardNumber", "41111111111");
-fields.put("cvv", "123");
-
-recordJson.put("fields", fieldsJson);
-recordsArrayJson.add(record);
-recordsJson.put("records", recordsArrayJson);
-try {
- JSONObject insertResponse = skyflowClient.insert(records);
- System.out.println(insertResponse);
-} catch (SkyflowException exception) {
- System.out.println(exception);
-}
-```
-
-**V2 (New):** Request building
-
-```java
-ArrayList> values = new ArrayList<>();
-HashMap value = new HashMap<>();
-value.put("", ""); // Replace with column name and value
-value.put("", ""); // Replace with another column name and value
-values.add(values);
-
-ArrayList> tokens = new ArrayList<>();
-HashMap token = new HashMap<>();
-token.put("", ""); // Replace with the token for COLUMN_NAME_2
-tokens.add(token);
-
-InsertRequest insertRequest = InsertRequest.builder()
- .table("") // Replace with the table name
- .continueOnError(true) // Continue inserting even if some records fail
- .tokenMode(TokenMode.ENABLE) // Enable BYOT for token validation
- .values(values) // Data to insert
- .tokens(tokens) // Provide tokens for BYOT columns
- .returnTokens(true) // Return tokens along with the response
- .build();
-```
-
-**V1 (Old):** Response structure
-
-```json
-{
- "records": [
- {
- "table": "cards",
- "fields": {
- "skyflow_id": "16419435-aa63-4823-aae7-19c6a2d6a19f",
- "cardNumber": "f3907186-e7e2-466f-91e5-48e12c2bcbc1",
- "cvv": "1989cb56-63da-4482-a2df-1f74cd0dd1a5"
- }
- }
- ]
-}
-```
-
-**V2 (New):** Response structure
-
-```json
-{
- "insertedFields": [
- {
- "card_number": "5484-7829-1702-9110",
- "request_index": "0",
- "skyflow_id": "9fac9201-7b8a-4446-93f8-5244e1213bd1",
- "cardholder_name": "b2308e2a-c1f5-469b-97b7-1f193159399b"
- }
- ],
- "errors": []
-}
-```
-
----
-
-### Request options
-
-In V2, with the introduction of the builder design pattern has made handling optional fields in Java more efficient and straightforward.
-
-**V1 (Old)**
-
-```java
-InsertOptions insertOptions = new InsertOptions(true);
-```
-
-**V2 (New)**
-
-```java
-InsertRequest upsertRequest = InsertRequest.builder()
- .table("") // Replace with the table name
- .continueOnError(false) // Stop inserting if any record fails
- .tokenMode(TokenMode.DISABLE) // Disable BYOT
- .values(values) // Data to insert
- .returnTokens(false) // Do not return tokens
- .upsert("") // Replace with the column name used for upsert logic
- .build();
-```
-
----
-
-### Error structure
-
-In V2, we have enriched the error details to provide better debugging capabilities.
-The error response now includes:
-
-- `httpStatus`: The HTTP status code.
-- `grpcCode`: The gRPC code associated with the error.
-- `details` & `message`: A detailed description of the error.
-- `requestId`: A unique request identifier for easier debugging.
-
-**V1 (Old):** Error structure
-
-```json
-{
- "code": "",
- "description": ""
-}
-```
-
-**V2 (New):** Error structure
-
-```js
-{
- "httpStatus": "",
- "grpcCode": ,
- "httpCode": ,
- "message": "",
- "requestId": "",
- "details": [""]
-}
-```
+Upgrading from v1? See the dedicated migration guide: **[docs/migrate_to_v2.md](docs/migrate_to_v2.md)**
# Quickstart
@@ -423,10 +167,10 @@ public class InitSkyflowClient {
// Step 3: Create credentials as a JSON object (if a Bearer Token is not provided).
// Demonstrates an alternate approach to authenticate with Skyflow using a credentials object.
JsonObject credentialsObject = new JsonObject();
- credentialsObject.addProperty("clientID", ""); // Replace with your Client ID.
+ credentialsObject.addProperty("clientId", ""); // Replace with your Client ID.
credentialsObject.addProperty("clientName", ""); // Replace with your Client Name.
- credentialsObject.addProperty("TokenURI", ""); // Replace with the Token URI.
- credentialsObject.addProperty("keyID", ""); // Replace with your Key ID.
+ credentialsObject.addProperty("tokenUri", ""); // Replace with the Token URI.
+ credentialsObject.addProperty("keyId", ""); // Replace with your Key ID.
credentialsObject.addProperty("privateKey", ""); // Replace with your Private Key.
// Step 4: Convert the JSON object to a string and use it as credentials.
@@ -549,8 +293,8 @@ Skyflow returns tokens for the record that was just inserted.
"insertedFields": [
{
"card_number": "5484-7829-1702-9110",
- "request_index": "0",
- "skyflow_id": "9fac9201-7b8a-4446-93f8-5244e1213bd1",
+ "requestIndex": "0",
+ "skyflowId": "9fac9201-7b8a-4446-93f8-5244e1213bd1",
"cardholder_name": "b2308e2a-c1f5-469b-97b7-1f193159399b"
}
],
@@ -558,6 +302,8 @@ Skyflow returns tokens for the record that was just inserted.
}
```
+> **Note:** The response key is `skyflowId`. The legacy `skyflow_id` key is deprecated and will be removed in an upcoming release.
+
# Vault
The [Vault](https://github.com/skyflowapi/skyflow-java/tree/main/samples/src/main/java/com/example/vault) module performs operations on the vault, including inserting records, detokenizing tokens, and retrieving tokens associated with a `skyflow_id`.
@@ -694,20 +440,22 @@ Sample response:
"insertedFields": [
{
"card_number": "5484-7829-1702-9110",
- "request_index": "0",
- "skyflow_id": "9fac9201-7b8a-4446-93f8-5244e1213bd1",
+ "requestIndex": "0",
+ "skyflowId": "9fac9201-7b8a-4446-93f8-5244e1213bd1",
"cardholder_name": "b2308e2a-c1f5-469b-97b7-1f193159399b"
}
],
"errors": [
{
- "request_index": "1",
+ "requestIndex": "1",
"error": "Insert failed. Column card_number is invalid. Specify a valid column."
}
]
}
```
+> **Note:** The response key is `skyflowId`. The legacy `skyflow_id` key is deprecated and will be removed in an upcoming release.
+
### Insert call example with `upsert` option
An upsert operation checks for a record based on a unique column's value. If a match exists, the record is updated; otherwise, a new record is inserted.
@@ -1233,19 +981,21 @@ Sample response:
"card_number": "4555555555555553",
"email": "john.doe@gmail.com",
"name": "john doe",
- "skyflow_id": "a581d205-1969-4350-acbe-a2a13eb871a6"
+ "skyflowId": "a581d205-1969-4350-acbe-a2a13eb871a6"
},
{
"card_number": "4555555555555559",
"email": "jane.doe@gmail.com",
"name": "jane doe",
- "skyflow_id": "5ff887c3-b334-4294-9acc-70e78ae5164a"
+ "skyflowId": "5ff887c3-b334-4294-9acc-70e78ae5164a"
}
],
"errors": []
}
```
+> **Note:** The response key is `skyflowId`. The legacy `skyflow_id` key is deprecated and will be removed in an upcoming release.
+
### Get tokens
Return tokens for records. Ideal for securely processing sensitive data while maintaining data privacy.
@@ -1309,19 +1059,21 @@ Sample response:
"card_number": "3998-2139-0328-0697",
"email": "c9a6c9555060@82c092e7.bd52",
"name": "82c092e7-74c0-4e60-bd52-c9a6c9555060",
- "skyflow_id": "a581d205-1969-4350-acbe-a2a13eb871a6"
+ "skyflowId": "a581d205-1969-4350-acbe-a2a13eb871a6"
},
{
"card_number": "3562-0140-8820-7499",
"email": "6174366e2bc6@59f82e89.93fc",
"name": "59f82e89-138e-4f9b-93fc-6174366e2bc6",
- "skyflow_id": "5ff887c3-b334-4294-9acc-70e78ae5164a"
+ "skyflowId": "5ff887c3-b334-4294-9acc-70e78ae5164a"
}
],
"errors": []
}
```
+> **Note:** The response key is `skyflowId`. The legacy `skyflow_id` key is deprecated and will be removed in an upcoming release.
+
### Get By column name and column values
Retrieve records by unique column values. Ideal for querying data without knowing Skyflow IDs, using alternate unique identifiers.
@@ -1387,19 +1139,21 @@ Sample response:
"card_number": "4555555555555553",
"email": "john.doe@gmail.com",
"name": "john doe",
- "skyflow_id": "a581d205-1969-4350-acbe-a2a13eb871a6"
+ "skyflowId": "a581d205-1969-4350-acbe-a2a13eb871a6"
},
{
"card_number": "4555555555555559",
"email": "jane.doe@gmail.com",
"name": "jane doe",
- "skyflow_id": "5ff887c3-b334-4294-9acc-70e78ae5164a"
+ "skyflowId": "5ff887c3-b334-4294-9acc-70e78ae5164a"
}
],
"errors": []
}
```
+> **Note:** The response key is `skyflowId`. The legacy `skyflow_id` key is deprecated and will be removed in an upcoming release.
+
### Redaction types
Redaction types determine how sensitive data is displayed when retrieved from the vault.
@@ -1754,13 +1508,15 @@ Sample response:
{
"card_number": "XXXXXXXXXXXX1112",
"name": "S***ar",
- "skyflow_id": "3ea3861-x107-40w8-la98-106sp08ea83f",
+ "skyflowId": "3ea3861-x107-40w8-la98-106sp08ea83f",
"tokenizedData": null
}
]
}
```
+> **Note:** The response key is `skyflowId`. The legacy `skyflow_id` key is deprecated and will be removed in an upcoming release.
+
## Upload File
To upload files to a Skyflow vault, use the `uploadFile` method. The `UploadFileRequest` class accepts parameters such as the file path, table name, and file name.
@@ -3033,6 +2789,48 @@ public class DetokenizeExample {
}
```
+# Error Handling
+
+The SDK uses `SkyflowException` for all errors — both client-side validation errors and server-side API errors.
+
+## Catching SkyflowException
+
+Wrap SDK calls in a `try/catch` block and catch `SkyflowException` to handle Skyflow-specific errors separately from unexpected exceptions:
+
+```java
+import com.skyflow.errors.SkyflowException;
+
+try {
+ InsertResponse response = skyflowClient.vault().insert(insertRequest);
+} catch (SkyflowException e) {
+ System.err.println("Skyflow error:");
+ System.err.println(" HTTP code : " + e.getHttpCode());
+ System.err.println(" Message : " + e.getMessage());
+ System.err.println(" Request ID: " + e.getRequestId());
+ System.err.println(" Details : " + e.getDetails());
+} catch (Exception e) {
+ System.err.println("Unexpected error: " + e.getMessage());
+}
+```
+
+## SkyflowException properties
+
+| Property | Method | Description |
+|---|---|---|
+| HTTP status code | `getHttpCode()` | Integer status code (e.g. `400`, `404`, `500`). |
+| Message | `getMessage()` | Human-readable description of the error. |
+| HTTP status string | `getHttpStatus()` | Status string from the server (e.g. `"BAD_REQUEST"`). |
+| gRPC code | `getGrpcCode()` | gRPC status code from the server. |
+| Request ID | `getRequestId()` | The `x-request-id` header — useful for support escalations. |
+| Details | `getDetails()` | `JsonArray` of additional error context from the server. Empty array for validation errors, `null` if the server response omitted the field. |
+
+**Validation errors** (missing table name, empty token list, etc.) are thrown before any network call:
+- `httpCode` is always `400`
+- `requestId` and `grpcCode` are `null`
+- `details` is an empty array
+
+**API errors** are returned by the Skyflow server and have all fields populated from the response body and headers.
+
# Logging
The SDK provides logging with Java's built-in logging library. By default, the SDK's logging level is set to `LogLevel.ERROR`. This can be changed using the `setLogLevel(logLevel)` method, as shown below:
@@ -3088,10 +2886,10 @@ public class ChangeLogLevel {
// Step 3: Define additional Skyflow credentials (optional, if needed for credentials string)
// Create a JSON object to hold your Skyflow credentials
JsonObject credentialsObject = new JsonObject();
- credentialsObject.addProperty("clientID", ""); // Replace with your client ID
+ credentialsObject.addProperty("clientId", ""); // Replace with your client ID
credentialsObject.addProperty("clientName", ""); // Replace with your client name
- credentialsObject.addProperty("TokenURI", ""); // Replace with your token URI
- credentialsObject.addProperty("keyID", ""); // Replace with your key ID
+ credentialsObject.addProperty("tokenUri", ""); // Replace with your token URI
+ credentialsObject.addProperty("keyId", ""); // Replace with your key ID
credentialsObject.addProperty("privateKey", ""); // Replace with your private key
// Convert the credentials object to a string format to be used for generating a Bearer Token
diff --git a/Rule/gitleaks.toml b/Rule/gitleaks.toml
index e127f827..6546a5ab 100644
--- a/Rule/gitleaks.toml
+++ b/Rule/gitleaks.toml
@@ -15,6 +15,12 @@ title = "gitleaks config"
[allowlist]
description = "global allow lists"
+commits = [
+ "536cbb5857e0b7a21cb25f9cf1349af9071fb84c",
+ "5acbb849d7f6831197a8a057cbbf5753115631df",
+ "96e7e39d6b44d5e47895d444a0d0d3ca82dba4ba",
+ "5e484d975d3dd715c38314d83db02bd4a2316c1d",
+]
regexes = [
'''(?i)^true|false|null$''',
'''^(?i:a+|b+|c+|d+|e+|f+|g+|h+|i+|j+|k+|l+|m+|n+|o+|p+|q+|r+|s+|t+|u+|v+|w+|x+|y+|z+|\*+|\.+)$''',
@@ -593,6 +599,7 @@ regexTarget = "match"
regexes = [
'''(?i)(?:access(?:ibility|or)|access[_.-]?id|random[_.-]?access|api[_.-]?(?:id|name|version)|rapid|capital|[a-z0-9-]*?api[a-z0-9-]*?:jar:|author|X-MS-Exchange-Organization-Auth|Authentication-Results|(?:credentials?[_.-]?id|withCredentials)|(?:bucket|foreign|hot|idx|natural|primary|pub(?:lic)?|schema|sequence)[_.-]?key|(?:turkey)|key[_.-]?(?:alias|board|code|frame|id|length|mesh|name|pair|press(?:ed)?|ring|selector|signature|size|stone|storetype|word|up|down|left|right)|key[_.-]?vault[_.-]?(?:id|name)|keyVaultToStoreSecrets|key(?:store|tab)[_.-]?(?:file|path)|issuerkeyhash|(?-i:[DdMm]onkey|[DM]ONKEY)|keying|(?:secret)[_.-]?(?:length|name|size)|UserSecretsId|(?:csrf)[_.-]?token|(?:io\.jsonwebtoken[ \t]?:[ \t]?[\w-]+)|(?:api|credentials|token)[_.-]?(?:endpoint|ur[il])|public[_.-]?token|(?:key|token)[_.-]?file|(?-i:(?:[A-Z_]+=\n[A-Z_]+=|[a-z_]+=\n[a-z_]+=)(?:\n|\z))|(?-i:(?:[A-Z.]+=\n[A-Z.]+=|[a-z.]+=\n[a-z.]+=)(?:\n|\z)))''',
]
+[[rules.allowlists]]
stopwords = [
"000000",
"aaaaaa",
@@ -2071,6 +2078,7 @@ stopwords = [
"zsh.",
"zsh_",
"6fe4476ee5a1832882e326b506d14126",
+ "sky-ab123",
]
[[rules.allowlists]]
regexTarget = "line"
@@ -2716,6 +2724,16 @@ id = "private-key"
description = "Identified a Private Key, which may compromise cryptographic security and sensitive data encryption."
regex = '''(?i)-----BEGIN[ A-Z0-9_-]{0,100}PRIVATE KEY(?: BLOCK)?-----[\s\S-]{64,}?KEY(?: BLOCK)?-----'''
keywords = ["-----begin"]
+[[rules.allowlists]]
+regexTarget = "match"
+regexes = [
+ '''cHJpdmF0ZV9rZXlfdmFsdWU=''',
+]
+
+[[rules.allowlists]]
+paths = [
+ '''src/test/resources/invalidTokenURICredentials\.json''',
+]
[[rules]]
id = "privateai-api-token"
diff --git a/codecov.yml b/codecov.yml
index 69cb7601..05c58c3e 100644
--- a/codecov.yml
+++ b/codecov.yml
@@ -1 +1,52 @@
comment: false
+
+component_management:
+ default_rules:
+ statuses:
+ - type: project
+ target: auto
+ individual_components:
+ - component_id: service_account
+ name: Service Account
+ paths:
+ - src/main/java/com/skyflow/serviceaccount/**
+ - component_id: vault_data
+ name: Vault Data
+ paths:
+ - src/main/java/com/skyflow/vault/data/**
+ - component_id: vault_tokens
+ name: Vault Tokens
+ paths:
+ - src/main/java/com/skyflow/vault/tokens/**
+ - component_id: vault_connection
+ name: Vault Connection
+ paths:
+ - src/main/java/com/skyflow/vault/connection/**
+ - component_id: vault_controller
+ name: Vault Controller
+ paths:
+ - src/main/java/com/skyflow/vault/controller/**
+ - component_id: vault_detect
+ name: Detect
+ paths:
+ - src/main/java/com/skyflow/vault/detect/**
+ - component_id: vault_audit
+ name: Audit
+ paths:
+ - src/main/java/com/skyflow/vault/audit/**
+ - component_id: vault_bin
+ name: BIN Lookup
+ paths:
+ - src/main/java/com/skyflow/vault/bin/**
+ - component_id: config
+ name: Config
+ paths:
+ - src/main/java/com/skyflow/config/**
+ - component_id: utils
+ name: Utils
+ paths:
+ - src/main/java/com/skyflow/utils/**
+ - component_id: errors
+ name: Errors
+ paths:
+ - src/main/java/com/skyflow/errors/**
diff --git a/docs/migrate_to_v2.md b/docs/migrate_to_v2.md
new file mode 100644
index 00000000..671210fd
--- /dev/null
+++ b/docs/migrate_to_v2.md
@@ -0,0 +1,284 @@
+# Skyflow Java SDK — V1 to V2 Migration Guide
+
+This guide covers the steps to migrate the Skyflow Java SDK from v1 to v2.
+
+---
+
+## Authentication options
+
+In V2, multiple authentication options are available. You can now provide credentials in the following ways:
+
+- Environment variable (`SKYFLOW_CREDENTIALS`) _(Recommended)_
+- API Key
+- Path to credentials JSON file
+- Stringified JSON of credentials
+- Bearer token
+
+**V1 (Old)**
+
+```java
+static class DemoTokenProvider implements TokenProvider {
+ @Override
+ public String getBearerToken() throws Exception {
+ ResponseToken res = null;
+ try {
+ String filePath = "";
+ res = Token.generateBearerToken(filePath);
+ } catch (SkyflowException e) {
+ e.printStackTrace();
+ }
+ return res.getAccessToken();
+ }
+}
+```
+
+**V2 (New): Choose one of the following:**
+
+```java
+// Option 1: API Key (Recommended)
+Credentials skyflowCredentials = new Credentials();
+skyflowCredentials.setApiKey("");
+
+// Option 2: Environment Variable (Recommended)
+// Set SKYFLOW_CREDENTIALS in your environment
+
+// Option 3: Credentials File
+skyflowCredentials.setPath("");
+
+// Option 4: Stringified JSON
+skyflowCredentials.setCredentialsString("");
+
+// Option 5: Bearer Token
+skyflowCredentials.setToken("");
+```
+
+> **Notes:**
+> - Use only ONE authentication method per credentials object.
+> - API Key or environment variable are recommended for production.
+> - For priority order see [Quickstart — Initialize the client](../README.md#initialize-the-client).
+
+---
+
+## Initializing the client
+
+V2 introduces a builder pattern for client initialization with multi-vault support.
+
+**Key changes:**
+- `vaultUrl` replaced with `clusterId` (derived from vault URL)
+- Added `env` specification (e.g. `Env.PROD`, `Env.SANDBOX`)
+- Log level is now per-client-instance
+
+**V1 (Old)**
+
+```java
+DemoTokenProvider demoTokenProvider = new DemoTokenProvider();
+SkyflowConfiguration skyflowConfig = new SkyflowConfiguration(
+ "", "", demoTokenProvider
+);
+Skyflow skyflowClient = Skyflow.init(skyflowConfig);
+```
+
+**V2 (New)**
+
+```java
+Credentials credentials = new Credentials();
+credentials.setPath("");
+
+VaultConfig config = new VaultConfig();
+config.setVaultId("");
+config.setClusterId("");
+config.setEnv(Env.PROD);
+config.setCredentials(credentials);
+
+Skyflow skyflowClient = Skyflow.builder()
+ .setLogLevel(LogLevel.DEBUG)
+ .addVaultConfig(config)
+ .build();
+```
+
+---
+
+## Request and response structure
+
+V2 removes third-party JSON objects in favour of native `ArrayList` and `HashMap` with a builder pattern for requests.
+
+**V1 (Old) — Request**
+
+```java
+JSONObject recordsJson = new JSONObject();
+JSONArray recordsArrayJson = new JSONArray();
+JSONObject recordJson = new JSONObject();
+recordJson.put("table", "cards");
+JSONObject fieldsJson = new JSONObject();
+fieldsJson.put("cardNumber", "41111111111");
+fieldsJson.put("cvv", "123");
+recordJson.put("fields", fieldsJson);
+recordsArrayJson.add(recordJson);
+recordsJson.put("records", recordsArrayJson);
+try {
+ JSONObject insertResponse = skyflowClient.insert(records);
+} catch (SkyflowException e) {
+ System.out.println(e);
+}
+```
+
+**V2 (New) — Request**
+
+```java
+HashMap value = new HashMap<>();
+value.put("", "");
+value.put("", "");
+ArrayList> values = new ArrayList<>();
+values.add(value);
+
+InsertRequest insertRequest = InsertRequest.builder()
+ .table("")
+ .values(values)
+ .returnTokens(true)
+ .build();
+
+InsertResponse response = skyflowClient.vault().insert(insertRequest);
+```
+
+**V1 (Old) — Response**
+
+```json
+{
+ "records": [
+ {
+ "table": "cards",
+ "fields": {
+ "skyflow_id": "16419435-aa63-4823-aae7-19c6a2d6a19f",
+ "cardNumber": "f3907186-e7e2-466f-91e5-48e12c2bcbc1",
+ "cvv": "1989cb56-63da-4482-a2df-1f74cd0dd1a5"
+ }
+ }
+ ]
+}
+```
+
+**V2 (New) — Response**
+
+```json
+{
+ "insertedFields": [
+ {
+ "skyflowId": "9fac9201-7b8a-4446-93f8-5244e1213bd1",
+ "card_number": "5484-7829-1702-9110",
+ "cardholder_name": "b2308e2a-c1f5-469b-97b7-1f193159399b"
+ }
+ ],
+ "errors": null
+}
+```
+
+---
+
+## Request options
+
+V2 builder pattern replaces V1 options objects.
+
+**V1 (Old)**
+
+```java
+InsertOptions insertOptions = new InsertOptions(true);
+```
+
+**V2 (New)**
+
+```java
+InsertRequest request = InsertRequest.builder()
+ .table("")
+ .values(values)
+ .continueOnError(false)
+ .tokenMode(TokenMode.DISABLE)
+ .returnTokens(false)
+ .upsert("")
+ .build();
+```
+
+---
+
+## Error structure
+
+V2 provides richer error details for easier debugging.
+
+**V1 (Old)**
+
+```json
+{
+ "code": "",
+ "description": ""
+}
+```
+
+**V2 (New)**
+
+```json
+{
+ "httpStatus": "",
+ "grpcCode": "",
+ "httpCode": "",
+ "message": "",
+ "requestId": "",
+ "details": [""]
+}
+```
+
+---
+
+## Credential field names (v2.1+)
+
+The credentials JSON file field names are updated to follow Java camelCase conventions. Both old and new forms are permanently accepted.
+
+| Old form (still accepted) | New form (preferred) |
+|---|---|
+| `clientID` | `clientId` |
+| `keyID` | `keyId` |
+| `tokenURI` | `tokenUri` |
+
+---
+
+## Response field names (v2.1+)
+
+Response maps now return `skyflowId` (camelCase). The legacy `skyflow_id` key is still present for backward compatibility but is deprecated.
+
+| Deprecated (still returned) | Preferred |
+|---|---|
+| `skyflow_id` | `skyflowId` |
+
+---
+
+## Update request data key (v2.1+)
+
+When calling `update()`, use `skyflowId` (camelCase) as the key in the data map to identify the record. Using `skyflow_id` still works but emits a deprecation warning. If both keys are present, `skyflowId` takes precedence.
+
+```java
+HashMap data = new HashMap<>();
+data.put("skyflowId", ""); // preferred
+data.put("card_number", "");
+
+UpdateRequest request = UpdateRequest.builder()
+ .table("")
+ .data(data)
+ .returnTokens(true)
+ .build();
+
+skyflowClient.vault().update(request);
+```
+
+---
+
+## Method renames (v2.1+)
+
+The following instance methods have been renamed for consistency. The old names still work but emit deprecation warnings.
+
+| Deprecated | Preferred |
+|---|---|
+| `skyflowClient.updateLogLevel(logLevel)` | `skyflowClient.setLogLevel(logLevel)` |
+| `TokenMode.getBYOT()` | `TokenMode.getByot()` |
+| `DetokenizeRequest.builder().downloadURL(b)` | `DetokenizeRequest.builder().downloadUrl(b)` |
+
+---
+
+For the full list of changes see [CHANGELOG.md](../CHANGELOG.md).
diff --git a/pom.xml b/pom.xml
index 1e8f32f3..e70f0ac6 100644
--- a/pom.xml
+++ b/pom.xml
@@ -6,7 +6,7 @@
com.skyflow
skyflow-java
- 2.0.4
+ 3.0.0-beta.11-dev.556d78e
jar
${project.groupId}:${project.artifactId}
@@ -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/UpdateExample.java b/samples/src/main/java/com/example/vault/UpdateExample.java
index a5947780..9757ba44 100644
--- a/samples/src/main/java/com/example/vault/UpdateExample.java
+++ b/samples/src/main/java/com/example/vault/UpdateExample.java
@@ -46,7 +46,7 @@ public static void main(String[] args) throws SkyflowException {
// Step 5: Update records with TokenMode enabled
try {
HashMap data1 = new HashMap<>();
- data1.put("skyflow_id", ""); // Replace with the Skyflow ID of the record
+ data1.put("skyflowId", ""); // Replace with the Skyflow ID of the record
data1.put("", ""); // Replace with column name and value to update
data1.put("", ""); // Replace with another column name and value
@@ -71,7 +71,7 @@ public static void main(String[] args) throws SkyflowException {
// Step 6: Update records with TokenMode disabled
try {
HashMap data2 = new HashMap<>();
- data2.put("skyflow_id", ""); // Replace with the Skyflow ID of the record
+ data2.put("skyflowId", ""); // Replace with the Skyflow ID of the record
data2.put("", ""); // Replace with column name and value to update
data2.put("", ""); // Replace with another column name and value
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
new file mode 100644
index 00000000..f33a7606
--- /dev/null
+++ b/samples/src/main/java/com/example/vault/deprecated/UpdateExample.java
@@ -0,0 +1,99 @@
+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.TokenMode;
+import com.skyflow.errors.SkyflowException;
+import com.skyflow.vault.data.UpdateRequest;
+import com.skyflow.vault.data.UpdateResponse;
+
+import java.util.HashMap;
+
+/**
+ * @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.
+ *
+ * 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();
+ credentials.setApiKey(""); // Replace with the actual API key
+
+ // Step 2: Configure the vault
+ VaultConfig vaultConfig = new VaultConfig();
+ vaultConfig.setVaultId(""); // Replace with the ID of the vault
+ vaultConfig.setClusterId(""); // Replace with the cluster ID of the vault
+ vaultConfig.setEnv(Env.PROD); // Set the environment (e.g., DEV, STAGE, PROD)
+ vaultConfig.setCredentials(credentials); // Associate the credentials with the vault
+
+ // Step 3: Set up credentials for the Skyflow client
+ Credentials skyflowCredentials = new Credentials();
+ 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()
+ .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"
+ try {
+ HashMap data1 = new HashMap<>();
+ data1.put("skyflow_id", ""); // @deprecated — use "skyflowId"
+ data1.put("", ""); // Replace with column name and value to update
+ data1.put("", ""); // Replace with another column name and value
+
+ HashMap tokens = new HashMap<>();
+ tokens.put("", ""); // Replace with the token for COLUMN_NAME_2
+
+ UpdateRequest updateRequest1 = UpdateRequest.builder()
+ .table("") // Replace with the table name
+ .tokenMode(TokenMode.ENABLE) // Enable TokenMode for token validation
+ .data(data1) // Data to update
+ .tokens(tokens) // Provide tokens for TokenMode columns
+ .returnTokens(true) // Return tokens along with the update response
+ .build();
+
+ UpdateResponse updateResponse1 = skyflowClient.vault().update(updateRequest1); // Perform the update
+ System.out.println("Update Response (TokenMode Enabled): " + updateResponse1);
+ } catch (SkyflowException e) {
+ System.out.println("Error during update with TokenMode enabled:");
+ e.printStackTrace();
+ }
+
+ // Step 6: Update records with TokenMode disabled
+ // DEPRECATED: use "skyflowId" key instead of "skyflow_id"
+ try {
+ HashMap data2 = new HashMap<>();
+ data2.put("skyflow_id", ""); // @deprecated — use "skyflowId"
+ data2.put("", ""); // Replace with column name and value to update
+ data2.put("", ""); // Replace with another column name and value
+
+ UpdateRequest updateRequest2 = UpdateRequest.builder()
+ .table("") // Replace with the table name
+ .tokenMode(TokenMode.DISABLE) // Disable TokenMode
+ .data(data2) // Data to update
+ .returnTokens(false) // Do not return tokens
+ .build();
+
+ UpdateResponse updateResponse2 = skyflowClient.vault().update(updateRequest2); // Perform the update
+ System.out.println("Update Response (TokenMode Disabled): " + updateResponse2);
+ } catch (SkyflowException e) {
+ System.out.println("Error during update with TokenMode disabled:" + e);
+ }
+ }
+}
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/Skyflow.java b/src/main/java/com/skyflow/Skyflow.java
index 8e51da57..eba8d6fd 100644
--- a/src/main/java/com/skyflow/Skyflow.java
+++ b/src/main/java/com/skyflow/Skyflow.java
@@ -74,11 +74,18 @@ public Skyflow updateSkyflowCredentials(Credentials credentials) throws SkyflowE
return this;
}
- public Skyflow updateLogLevel(LogLevel logLevel) {
+ public Skyflow setLogLevel(LogLevel logLevel) {
this.builder.setLogLevel(logLevel);
return this;
}
+ /** @deprecated Use {@link #setLogLevel(LogLevel)} instead. */
+ @Deprecated(since = "2.1", forRemoval = true)
+ public Skyflow updateLogLevel(LogLevel logLevel) {
+ LogUtil.printWarningLog(InfoLogs.DEPRECATED_UPDATE_LOG_LEVEL.getLog());
+ return setLogLevel(logLevel);
+ }
+
public LogLevel getLogLevel() {
return this.builder.logLevel;
}
diff --git a/src/main/java/com/skyflow/VaultClient.java b/src/main/java/com/skyflow/VaultClient.java
index e3352f19..1d5e5d74 100644
--- a/src/main/java/com/skyflow/VaultClient.java
+++ b/src/main/java/com/skyflow/VaultClient.java
@@ -122,7 +122,7 @@ protected V1DetokenizePayload getDetokenizePayload(DetokenizeRequest request) {
return V1DetokenizePayload.builder()
.continueOnError(request.getContinueOnError())
- .downloadUrl(request.getDownloadURL())
+ .downloadUrl(request.getDownloadUrl())
.detokenizationParameters(recordRequests)
.build();
}
@@ -144,7 +144,7 @@ protected RecordServiceInsertRecordBody getBulkInsertRequestBody(InsertRequest r
.tokenization(request.getReturnTokens())
.homogeneous(request.getHomogeneous())
.upsert(request.getUpsert())
- .byot(request.getTokenMode().getBYOT())
+ .byot(request.getTokenMode().getByot())
.records(records)
.build();
}
@@ -171,14 +171,14 @@ protected RecordServiceBatchOperationBody getBatchInsertRequestBody(InsertReques
return RecordServiceBatchOperationBody.builder()
.continueOnError(true)
- .byot(request.getTokenMode().getBYOT())
+ .byot(request.getTokenMode().getByot())
.records(records)
.build();
}
protected RecordServiceUpdateRecordBody getUpdateRequestBody(UpdateRequest request) {
RecordServiceUpdateRecordBody.Builder updateRequestBodyBuilder = RecordServiceUpdateRecordBody.builder();
- updateRequestBodyBuilder.byot(request.getTokenMode().getBYOT());
+ updateRequestBodyBuilder.byot(request.getTokenMode().getByot());
updateRequestBodyBuilder.tokenization(request.getReturnTokens());
V1FieldRecords.Builder recordBuilder = V1FieldRecords.builder();
HashMap values = request.getData();
@@ -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/enums/TokenMode.java b/src/main/java/com/skyflow/enums/TokenMode.java
index 63e9b8a9..f855d7cc 100644
--- a/src/main/java/com/skyflow/enums/TokenMode.java
+++ b/src/main/java/com/skyflow/enums/TokenMode.java
@@ -1,6 +1,8 @@
package com.skyflow.enums;
import com.skyflow.generated.rest.types.V1Byot;
+import com.skyflow.logs.InfoLogs;
+import com.skyflow.utils.logger.LogUtil;
public enum TokenMode {
DISABLE(V1Byot.DISABLE),
@@ -13,10 +15,17 @@ public enum TokenMode {
this.byot = byot;
}
- public V1Byot getBYOT() {
+ public V1Byot getByot() {
return byot;
}
+ /** @deprecated Use {@link #getByot()} instead. */
+ @Deprecated(since = "2.1", forRemoval = true)
+ public V1Byot getBYOT() {
+ LogUtil.printWarningLog(InfoLogs.DEPRECATED_GET_BYOT.getLog());
+ return getByot();
+ }
+
@Override
public String toString() {
return byot.toString();
diff --git a/src/main/java/com/skyflow/errors/ErrorMessage.java b/src/main/java/com/skyflow/errors/ErrorMessage.java
index fc222522..a99dd1cc 100644
--- a/src/main/java/com/skyflow/errors/ErrorMessage.java
+++ b/src/main/java/com/skyflow/errors/ErrorMessage.java
@@ -58,13 +58,10 @@ public enum ErrorMessage {
TableKeyError("%s0 Validation error. 'table' key is missing from the payload. Specify a 'table' key."),
EmptyTable("%s0 Validation error. 'table' can't be empty. Specify a table."),
ValuesKeyError("%s0 Validation error. 'values' key is missing from the payload. Specify a 'values' key."),
- EmptyValues("%s0 Validation error. 'values' can't be empty. Specify values."),
EmptyKeyInValues("%s0 Validation error. Invalid key in values. Specify a valid key."),
- EmptyValueInValues("%s0 Validation error. Invalid value in values. Specify a valid value."),
TokensKeyError("%s0 Validation error. 'tokens' key is missing from the payload. Specify a 'tokens' key."),
EmptyTokens("%s0 Validation error. The 'tokens' field is empty. Specify tokens for one or more fields."),
EmptyKeyInTokens("%s0 Validation error. Invalid key tokens. Specify a valid key."),
- EmptyValueInTokens("%s0 Validation error. Invalid value in tokens. Specify a valid value."),
EmptyUpsert("%s0 Validation error. 'upsert' key can't be empty. Specify an upsert column."),
HomogenousNotSupportedWithUpsert("%s0 Validation error. 'homogenous' is not supported with 'upsert'. Specify either 'homogenous' or 'upsert'."),
TokensPassedForTokenModeDisable("%s0 Validation error. 'tokenMode' wasn't specified. Set 'tokenMode' to 'ENABLE' to insert tokens."),
diff --git a/src/main/java/com/skyflow/errors/SkyflowException.java b/src/main/java/com/skyflow/errors/SkyflowException.java
index 32c71f39..6fedf9c3 100644
--- a/src/main/java/com/skyflow/errors/SkyflowException.java
+++ b/src/main/java/com/skyflow/errors/SkyflowException.java
@@ -9,6 +9,33 @@
import java.util.List;
import java.util.Map;
+/**
+ * Exception thrown by all Skyflow SDK operations.
+ *
+ * There are two broad categories of errors:
+ *
+ *
+ * - Validation errors — caught before any network call is made (e.g. missing table,
+ * empty token list). These always have {@code httpCode = 400} and an empty
+ * {@link #getDetails()} array. {@link #getRequestId()} and {@link #getGrpcCode()} are
+ * {@code null}.
+ *
- API errors — returned by the Skyflow server. The HTTP status code, gRPC code,
+ * human-readable status string, error message, and request ID are all parsed from the
+ * response and available via the corresponding getters.
+ *
+ *
+ * Typical error-handling pattern:
+ *
{@code
+ * try {
+ * InsertResponse response = vault.insert(request);
+ * } catch (SkyflowException e) {
+ * System.err.println("HTTP " + e.getHttpCode() + " — " + e.getMessage());
+ * if (e.getRequestId() != null) {
+ * System.err.println("Request ID: " + e.getRequestId());
+ * }
+ * }
+ * }
+ */
public class SkyflowException extends Exception {
private String requestId;
private Integer grpcCode;
@@ -33,6 +60,11 @@ public SkyflowException(String message, Throwable cause) {
this.message = message;
}
+ /**
+ * Constructs a validation error with a fixed HTTP 400 status.
+ * {@link #getDetails()} returns an empty array; {@link #getRequestId()} and
+ * {@link #getGrpcCode()} return {@code null}.
+ */
public SkyflowException(int code, String message) {
super(message);
this.httpCode = code;
@@ -41,6 +73,13 @@ public SkyflowException(int code, String message) {
this.details = new JsonArray();
}
+ /**
+ * Constructs an API error from an HTTP response.
+ * Parses the JSON error body to populate {@link #getMessage()}, {@link #getGrpcCode()},
+ * {@link #getHttpStatus()}, and {@link #getDetails()}. The request ID is read from the
+ * {@code x-request-id} response header. If the body cannot be parsed, falls back to the
+ * raw body string as the message.
+ */
public SkyflowException(int httpCode, Throwable cause, Map> responseHeaders, String responseBody) {
super(cause);
this.httpCode = httpCode > 0 ? httpCode : 400;
@@ -65,6 +104,10 @@ private void setResponseBody(String responseBody, Map> resp
}
}
+ /**
+ * Returns the {@code x-request-id} from the server response, useful for support escalations.
+ * {@code null} for validation errors that never reached the server.
+ */
public String getRequestId() {
return requestId;
}
@@ -89,10 +132,19 @@ private void setHttpStatus() {
this.httpStatus = statusElement == null ? null : statusElement.getAsString();
}
+ /**
+ * Returns the HTTP status code (e.g. 400, 404, 500).
+ * Defaults to 400 when the server returned a non-positive code.
+ */
public int getHttpCode() {
return httpCode;
}
+ /**
+ * Returns additional error details from the server response, or an empty array for
+ * validation errors. Never {@code null} for validation errors; may be {@code null} for
+ * API errors whose response body contained no {@code details} field.
+ */
public JsonArray getDetails() {
return details;
}
@@ -112,10 +164,18 @@ private void setDetails(Map> responseHeaders) {
}
}
+ /**
+ * Returns the gRPC status code from the server response.
+ * {@code null} for validation errors and API responses that omit this field.
+ */
public Integer getGrpcCode() {
return grpcCode;
}
+ /**
+ * Returns the human-readable HTTP status string from the server response (e.g.
+ * {@code "Bad Request"}, {@code "Not Found"}).
+ */
public String getHttpStatus() {
return httpStatus;
}
diff --git a/src/main/java/com/skyflow/logs/ErrorLogs.java b/src/main/java/com/skyflow/logs/ErrorLogs.java
index e6e8b304..47866efc 100644
--- a/src/main/java/com/skyflow/logs/ErrorLogs.java
+++ b/src/main/java/com/skyflow/logs/ErrorLogs.java
@@ -50,15 +50,12 @@ public enum ErrorLogs {
TABLE_IS_REQUIRED("Invalid %s1 request. Table is required."),
EMPTY_TABLE_NAME("Invalid %s1 request. Table name can not be empty."),
VALUES_IS_REQUIRED("Invalid %s1 request. Values are required."),
- EMPTY_VALUES("Invalid %s1 request. Values can not be empty."),
- EMPTY_OR_NULL_VALUE_IN_VALUES("Invalid %s1 request. Value can not be null or empty in values for key \"%s2\"."),
EMPTY_OR_NULL_KEY_IN_VALUES("Invalid %s1 request. Key can not be null or empty in values"),
EMPTY_UPSERT("Invalid %s1 request. Upsert can not be empty."),
HOMOGENOUS_NOT_SUPPORTED_WITH_UPSERT("Invalid %s1 request. Homogenous is not supported when upsert is passed."),
TOKENS_NOT_ALLOWED_WITH_TOKEN_MODE_DISABLE("Invalid %s1 request. Tokens are not allowed when tokenMode is DISABLE."),
TOKENS_REQUIRED_WITH_TOKEN_MODE("Invalid %s1 request. Tokens are required when tokenMode is %s2."),
EMPTY_TOKENS("Invalid %s1 request. Tokens can not be empty."),
- EMPTY_OR_NULL_VALUE_IN_TOKENS("Invalid %s1 request. Value can not be null or empty in tokens for key \"%s2\"."),
EMPTY_OR_NULL_KEY_IN_TOKENS("Invalid %s1 request. Key can not be null or empty in tokens."),
INSUFFICIENT_TOKENS_PASSED_FOR_TOKEN_MODE_ENABLE_STRICT("Invalid %s1 request. For tokenMode as ENABLE_STRICT, tokens should be passed for all fields."),
MISMATCH_OF_FIELDS_AND_TOKENS("Invalid %s1 request. Keys for values and tokens are not matching."),
diff --git a/src/main/java/com/skyflow/logs/InfoLogs.java b/src/main/java/com/skyflow/logs/InfoLogs.java
index f71fc416..e747bfa2 100644
--- a/src/main/java/com/skyflow/logs/InfoLogs.java
+++ b/src/main/java/com/skyflow/logs/InfoLogs.java
@@ -14,7 +14,7 @@ public enum InfoLogs {
// Bearer token generation
EMPTY_BEARER_TOKEN("Bearer token is empty."),
- BEARER_TOKEN_EXPIRED("Bearer token is expired."),
+ BEARER_TOKEN_EXPIRED("Bearer token is invalid or expired."),
GET_BEARER_TOKEN_TRIGGERED("getBearerToken method triggered."),
GET_BEARER_TOKEN_SUCCESS("Bearer token generated."),
GET_SIGNED_DATA_TOKENS_TRIGGERED("getSignedDataTokens method triggered."),
@@ -95,7 +95,17 @@ public enum InfoLogs {
GET_DETECT_RUN_TRIGGERED("Get detect run method triggered."),
VALIDATE_GET_DETECT_RUN_REQUEST("Validating get detect run request."),
REIDENTIFY_TEXT_SUCCESS("Text data re-identified."),
- ;
+
+ // Deprecation warnings — v2 backward compat
+ DEPRECATED_SKYFLOW_ID_KEY("[DEPRECATED] Response key 'skyflow_id' is deprecated and will be removed in an upcoming release. Use 'skyflowId' instead."),
+ 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_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.");
+
private final String log;
diff --git a/src/main/java/com/skyflow/serviceaccount/util/BearerToken.java b/src/main/java/com/skyflow/serviceaccount/util/BearerToken.java
index 9e3a6d63..ad7cae30 100644
--- a/src/main/java/com/skyflow/serviceaccount/util/BearerToken.java
+++ b/src/main/java/com/skyflow/serviceaccount/util/BearerToken.java
@@ -20,6 +20,7 @@
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileReader;
+import java.io.IOException;
import java.net.MalformedURLException;
import java.security.PrivateKey;
import java.util.ArrayList;
@@ -58,8 +59,12 @@ private static V1GetAuthTokenResponse generateBearerTokenFromCredentials(
throw new SkyflowException(ErrorCode.INVALID_INPUT.getCode(), ErrorMessage.InvalidCredentials.getMessage());
}
FileReader reader = new FileReader(String.valueOf(credentialsFile));
- JsonObject serviceAccountCredentials = JsonParser.parseReader(reader).getAsJsonObject();
- return getBearerTokenFromCredentials(serviceAccountCredentials, context, roles);
+ try {
+ JsonObject serviceAccountCredentials = JsonParser.parseReader(reader).getAsJsonObject();
+ return getBearerTokenFromCredentials(serviceAccountCredentials, context, roles);
+ } finally {
+ try { reader.close(); } catch (IOException ignored) {}
+ }
} catch (JsonSyntaxException e) {
LogUtil.printErrorLog(ErrorLogs.INVALID_CREDENTIALS_FILE_FORMAT.getLog());
throw new SkyflowException(ErrorCode.INVALID_INPUT.getCode(), Utils.parameterizedString(
@@ -99,30 +104,49 @@ private static V1GetAuthTokenResponse getBearerTokenFromCredentials(
throw new SkyflowException(ErrorCode.INVALID_INPUT.getCode(), ErrorMessage.MissingPrivateKey.getMessage());
}
- JsonElement clientID = credentials.get("clientID");
- if (clientID == null) {
+ // Accept both new-form keys (clientId/keyId/tokenUri) and legacy all-caps form for migration
+ 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());
throw new SkyflowException(ErrorCode.INVALID_INPUT.getCode(), ErrorMessage.MissingClientId.getMessage());
}
- JsonElement keyID = credentials.get("keyID");
- if (keyID == null) {
+ 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());
throw new SkyflowException(ErrorCode.INVALID_INPUT.getCode(), ErrorMessage.MissingKeyId.getMessage());
}
- JsonElement tokenURI = credentials.get("tokenURI");
- if (tokenURI == null) {
+ 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());
throw new SkyflowException(ErrorCode.INVALID_INPUT.getCode(), ErrorMessage.MissingTokenUri.getMessage());
}
PrivateKey pvtKey = Utils.getPrivateKeyFromPem(privateKey.getAsString());
String signedUserJWT = getSignedToken(
- clientID.getAsString(), keyID.getAsString(), tokenURI.getAsString(), pvtKey, context
+ clientId.getAsString(), keyId.getAsString(), tokenUri.getAsString(), pvtKey, context
);
- String basePath = Utils.getBaseURL(tokenURI.getAsString());
+ String basePath = Utils.getBaseURL(tokenUri.getAsString());
API_CLIENT_BUILDER.url(basePath);
ApiClient apiClient = API_CLIENT_BUILDER.token("token").build();
AuthenticationClient authenticationApi = apiClient.authentication();
@@ -145,15 +169,15 @@ private static V1GetAuthTokenResponse getBearerTokenFromCredentials(
}
private static String getSignedToken(
- String clientID, String keyID, String tokenURI, PrivateKey pvtKey, Object context
+ String clientId, String keyId, String tokenUri, PrivateKey pvtKey, Object context
) {
final Date createdDate = new Date();
final Date expirationDate = new Date(createdDate.getTime() + (3600 * 1000));
io.jsonwebtoken.JwtBuilder builder = Jwts.builder()
- .claim("iss", clientID)
- .claim("key", keyID)
- .claim("aud", tokenURI)
- .claim("sub", clientID)
+ .claim("iss", clientId)
+ .claim("key", keyId)
+ .claim("aud", tokenUri)
+ .claim("sub", clientId)
.expiration(expirationDate);
if (context != null) {
diff --git a/src/main/java/com/skyflow/serviceaccount/util/SignedDataTokens.java b/src/main/java/com/skyflow/serviceaccount/util/SignedDataTokens.java
index 0ce14007..b909e45b 100644
--- a/src/main/java/com/skyflow/serviceaccount/util/SignedDataTokens.java
+++ b/src/main/java/com/skyflow/serviceaccount/util/SignedDataTokens.java
@@ -16,6 +16,7 @@
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileReader;
+import java.io.IOException;
import java.security.PrivateKey;
import java.util.ArrayList;
import java.util.Date;
@@ -55,8 +56,12 @@ private static List generateSignedTokenFromCredentialsF
throw new SkyflowException(ErrorCode.INVALID_INPUT.getCode(), ErrorMessage.InvalidCredentials.getMessage());
}
FileReader reader = new FileReader(String.valueOf(credentialsFile));
- JsonObject serviceAccountCredentials = JsonParser.parseReader(reader).getAsJsonObject();
- responseToken = generateSignedTokensFromCredentials(serviceAccountCredentials, dataTokens, timeToLive, context);
+ try {
+ JsonObject serviceAccountCredentials = JsonParser.parseReader(reader).getAsJsonObject();
+ responseToken = generateSignedTokensFromCredentials(serviceAccountCredentials, dataTokens, timeToLive, context);
+ } finally {
+ try { reader.close(); } catch (IOException ignored) {}
+ }
} catch (JsonSyntaxException e) {
LogUtil.printErrorLog(ErrorLogs.INVALID_CREDENTIALS_FILE_FORMAT.getLog());
throw new SkyflowException(ErrorCode.INVALID_INPUT.getCode(), Utils.parameterizedString(
@@ -100,20 +105,33 @@ private static List generateSignedTokensFromCredentials
throw new SkyflowException(ErrorCode.INVALID_INPUT.getCode(), ErrorMessage.MissingPrivateKey.getMessage());
}
- JsonElement clientID = credentials.get("clientID");
- if (clientID == null) {
+ // Accept both new-form keys (clientId/keyId) and legacy all-caps form for migration
+ 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());
throw new SkyflowException(ErrorCode.INVALID_INPUT.getCode(), ErrorMessage.MissingClientId.getMessage());
}
- JsonElement keyID = credentials.get("keyID");
- if (keyID == null) {
+ 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());
throw new SkyflowException(ErrorCode.INVALID_INPUT.getCode(), ErrorMessage.MissingKeyId.getMessage());
}
PrivateKey pvtKey = Utils.getPrivateKeyFromPem(privateKey.getAsString());
signedDataTokens = getSignedToken(
- clientID.getAsString(), keyID.getAsString(), pvtKey, dataTokens, timeToLive, context);
+ clientId.getAsString(), keyId.getAsString(), pvtKey, dataTokens, timeToLive, context);
} catch (RuntimeException e) {
LogUtil.printErrorLog(ErrorLogs.SIGNED_DATA_TOKENS_REJECTED.getLog());
throw new SkyflowException(e);
@@ -122,7 +140,7 @@ private static List generateSignedTokensFromCredentials
}
private static List getSignedToken(
- String clientID, String keyID, PrivateKey pvtKey,
+ String clientId, String keyId, PrivateKey pvtKey,
ArrayList dataTokens, Integer timeToLive, Object context
) {
final Date createdDate = new Date();
@@ -139,8 +157,8 @@ private static List getSignedToken(
io.jsonwebtoken.JwtBuilder builder = Jwts.builder()
.claim("iss", "sdk")
.claim("iat", (createdDate.getTime() / 1000))
- .claim("key", keyID)
- .claim("sub", clientID)
+ .claim("key", keyId)
+ .claim("sub", clientId)
.claim("tok", dataToken)
.expiration(expirationDate);
diff --git a/src/main/java/com/skyflow/utils/Constants.java b/src/main/java/com/skyflow/utils/Constants.java
index 6732ea60..1162b3f0 100644
--- a/src/main/java/com/skyflow/utils/Constants.java
+++ b/src/main/java/com/skyflow/utils/Constants.java
@@ -34,6 +34,16 @@ public final class Constants {
public static final String PROCESSED_FILE_NAME_PREFIX = "processed-";
public static final String ERROR_FROM_CLIENT_HEADER_KEY = "error-from-client";
public static final String DEIDENTIFIED_FILE_PREFIX = "deidentified";
+ public static final String HTTPS_PROTOCOL = "https";
+ public static final String CURLY_PLACEHOLDER = "{%s}";
+ public static final String EMPTY_STRING = "";
+ public static final String QUOTE = "\"";
+
+ public static final class HttpUtilityExtra {
+ public static final String RAW_BODY_KEY = "__raw_body__";
+ public static final String SDK_GENERATED_PREFIX = "SDK-Generated-";
+ private HttpUtilityExtra() {}
+ }
static {
String sdkVersion;
diff --git a/src/main/java/com/skyflow/utils/HttpUtility.java b/src/main/java/com/skyflow/utils/HttpUtility.java
index b8e9283b..35cdb2fc 100644
--- a/src/main/java/com/skyflow/utils/HttpUtility.java
+++ b/src/main/java/com/skyflow/utils/HttpUtility.java
@@ -7,6 +7,7 @@
import java.io.*;
import java.net.HttpURLConnection;
import java.net.URL;
+import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.List;
@@ -32,8 +33,11 @@ public static String sendRequest(String method, URL url, JsonObject params, Map<
try {
connection = (HttpURLConnection) url.openConnection();
connection.setRequestMethod(method);
- connection.setRequestProperty("content-type", "application/json");
connection.setRequestProperty("Accept", "*/*");
+ boolean hasContentType = headers != null && headers.containsKey("content-type");
+ if (!hasContentType && params != null && !params.isEmpty()) {
+ connection.setRequestProperty("content-type", "application/json");
+ }
if (headers != null && !headers.isEmpty()) {
for (Map.Entry entry : headers.entrySet())
@@ -52,9 +56,11 @@ public static String sendRequest(String method, URL url, JsonObject params, Map<
byte[] input = null;
String requestContentType = connection.getRequestProperty("content-type");
- if (requestContentType.contains("application/x-www-form-urlencoded")) {
+ if (params.has(Constants.HttpUtilityExtra.RAW_BODY_KEY) && params.size() == 1) {
+ input = params.get(Constants.HttpUtilityExtra.RAW_BODY_KEY).getAsString().getBytes(StandardCharsets.UTF_8);
+ } else if (requestContentType != null && requestContentType.contains("application/x-www-form-urlencoded")) {
input = formatJsonToFormEncodedString(params).getBytes(StandardCharsets.UTF_8);
- } else if (requestContentType.contains("multipart/form-data")) {
+ } else if (requestContentType != null && requestContentType.contains("multipart/form-data")) {
input = formatJsonToMultiPartFormDataString(params, boundary).getBytes(StandardCharsets.UTF_8);
} else {
input = params.toString().getBytes(StandardCharsets.UTF_8);
@@ -67,7 +73,7 @@ public static String sendRequest(String method, URL url, JsonObject params, Map<
int httpCode = connection.getResponseCode();
String requestID = connection.getHeaderField("x-request-id");
- HttpUtility.requestID = requestID.split(",")[0];
+ HttpUtility.requestID = requestID != null ? requestID.split(",")[0] : null;
Map> responseHeaders = connection.getHeaderFields();
Reader streamReader;
if (httpCode > 299) {
@@ -110,7 +116,7 @@ public static String formatJsonToFormEncodedString(JsonObject requestBody) {
for (Map.Entry currentEntry : jsonMap.entrySet())
formEncodeString.append(makeFormEncodeKeyValuePair(currentEntry.getKey(), currentEntry.getValue()));
- return formEncodeString.substring(0, formEncodeString.length() - 1);
+ return formEncodeString.length() == 0 ? "" : formEncodeString.substring(0, formEncodeString.length() - 1);
}
public static String formatJsonToMultiPartFormDataString(JsonObject requestBody, String boundary) {
@@ -159,7 +165,13 @@ public static String appendRequestId(String message, String requestId) {
}
private static String makeFormEncodeKeyValuePair(String key, String value) {
- return key + "=" + value + "&";
+ try {
+ String encodedKey = URLEncoder.encode(key, StandardCharsets.UTF_8.toString());
+ String encodedValue = URLEncoder.encode(value, StandardCharsets.UTF_8.toString());
+ return encodedKey + "=" + encodedValue + "&";
+ } catch (Exception e) {
+ return key + "=" + value + "&";
+ }
}
}
diff --git a/src/main/java/com/skyflow/utils/Utils.java b/src/main/java/com/skyflow/utils/Utils.java
index b33b08c1..0c20bd6c 100644
--- a/src/main/java/com/skyflow/utils/Utils.java
+++ b/src/main/java/com/skyflow/utils/Utils.java
@@ -17,6 +17,8 @@
import java.io.File;
import java.net.MalformedURLException;
import java.net.URL;
+import java.net.URLEncoder;
+import java.nio.charset.StandardCharsets;
import java.security.KeyFactory;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
@@ -119,7 +121,12 @@ public static String constructConnectionURL(ConnectionConfig config, InvokeConne
for (Map.Entry entry : invokeConnectionRequest.getPathParams().entrySet()) {
String key = entry.getKey();
String value = entry.getValue();
- filledURL = new StringBuilder(filledURL.toString().replace(String.format("{%s}", key), value));
+ try {
+ String encodedValue = URLEncoder.encode(value, StandardCharsets.UTF_8.name());
+ filledURL = new StringBuilder(filledURL.toString().replace(String.format(Constants.CURLY_PLACEHOLDER, key), encodedValue));
+ } catch (Exception e) {
+ filledURL = new StringBuilder(filledURL.toString().replace(String.format(Constants.CURLY_PLACEHOLDER, key), value));
+ }
}
}
@@ -128,7 +135,13 @@ public static String constructConnectionURL(ConnectionConfig config, InvokeConne
for (Map.Entry entry : invokeConnectionRequest.getQueryParams().entrySet()) {
String key = entry.getKey();
String value = entry.getValue();
- filledURL.append(key).append("=").append(value).append("&");
+ try {
+ String encodedKey = URLEncoder.encode(key, StandardCharsets.UTF_8.name());
+ String encodedValue = URLEncoder.encode(value, StandardCharsets.UTF_8.name());
+ filledURL.append(encodedKey).append("=").append(encodedValue).append("&");
+ } catch (Exception e) {
+ filledURL.append(key).append("=").append(value).append("&");
+ }
}
filledURL = new StringBuilder(filledURL.substring(0, filledURL.length() - 1));
}
diff --git a/src/main/java/com/skyflow/utils/validations/Validations.java b/src/main/java/com/skyflow/utils/validations/Validations.java
index e1f18795..0c36445c 100644
--- a/src/main/java/com/skyflow/utils/validations/Validations.java
+++ b/src/main/java/com/skyflow/utils/validations/Validations.java
@@ -10,6 +10,7 @@
import java.util.regex.Pattern;
import com.google.gson.Gson;
+import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.skyflow.config.ConnectionConfig;
import com.skyflow.config.Credentials;
@@ -146,12 +147,27 @@ public static void validateInvokeConnectionRequest(InvokeConnectionRequest invok
}
if (requestBody != null) {
- Gson gson = new Gson();
- JsonObject bodyObject = gson.toJsonTree(requestBody).getAsJsonObject();
- if (bodyObject.isEmpty()) {
- LogUtil.printErrorLog(Utils.parameterizedString(
- ErrorLogs.EMPTY_REQUEST_BODY.getLog(), InterfaceName.INVOKE_CONNECTION.getName()));
- throw new SkyflowException(ErrorCode.INVALID_INPUT.getCode(), ErrorMessage.EmptyRequestBody.getMessage());
+ if (requestBody.getClass().equals(Object.class)) {
+ return;
+ }
+ if (requestBody instanceof String) {
+ String bodyStr = (String) requestBody;
+ if (bodyStr.trim().isEmpty()) {
+ LogUtil.printErrorLog(Utils.parameterizedString(
+ ErrorLogs.EMPTY_REQUEST_BODY.getLog(), InterfaceName.INVOKE_CONNECTION.getName()));
+ throw new SkyflowException(ErrorCode.INVALID_INPUT.getCode(), ErrorMessage.EmptyRequestBody.getMessage());
+ }
+ } else {
+ Gson gson = new Gson();
+ JsonElement bodyElement = gson.toJsonTree(requestBody);
+ if (bodyElement.isJsonObject()) {
+ JsonObject bodyObject = bodyElement.getAsJsonObject();
+ if (bodyObject.isEmpty()) {
+ LogUtil.printErrorLog(Utils.parameterizedString(
+ ErrorLogs.EMPTY_REQUEST_BODY.getLog(), InterfaceName.INVOKE_CONNECTION.getName()));
+ throw new SkyflowException(ErrorCode.INVALID_INPUT.getCode(), ErrorMessage.EmptyRequestBody.getMessage());
+ }
+ }
}
}
}
@@ -296,11 +312,6 @@ public static void validateInsertRequest(InsertRequest insertRequest) throws Sky
ErrorLogs.VALUES_IS_REQUIRED.getLog(), InterfaceName.INSERT.getName()
));
throw new SkyflowException(ErrorCode.INVALID_INPUT.getCode(), ErrorMessage.ValuesKeyError.getMessage());
- } else if (values.isEmpty()) {
- LogUtil.printErrorLog(Utils.parameterizedString(
- ErrorLogs.EMPTY_VALUES.getLog(), InterfaceName.INSERT.getName()
- ));
- throw new SkyflowException(ErrorCode.INVALID_INPUT.getCode(), ErrorMessage.EmptyValues.getMessage());
} else if (upsert != null) {
if (upsert.trim().isEmpty()) {
LogUtil.printErrorLog(Utils.parameterizedString(
@@ -324,15 +335,6 @@ public static void validateInsertRequest(InsertRequest insertRequest) throws Sky
ErrorLogs.EMPTY_OR_NULL_KEY_IN_VALUES.getLog(), InterfaceName.INSERT.getName()
));
throw new SkyflowException(ErrorCode.INVALID_INPUT.getCode(), ErrorMessage.EmptyKeyInValues.getMessage());
- } else {
- Object value = valuesMap.get(key);
- if (value == null || value.toString().trim().isEmpty()) {
- LogUtil.printErrorLog(Utils.parameterizedString(
- ErrorLogs.EMPTY_OR_NULL_VALUE_IN_VALUES.getLog(),
- InterfaceName.INSERT.getName(), key
- ));
- throw new SkyflowException(ErrorCode.INVALID_INPUT.getCode(), ErrorMessage.EmptyValueInValues.getMessage());
- }
}
}
}
@@ -546,17 +548,17 @@ public static void validateUpdateRequest(UpdateRequest updateRequest) throws Sky
ErrorLogs.EMPTY_DATA.getLog(), InterfaceName.UPDATE.getName()
));
throw new SkyflowException(ErrorCode.INVALID_INPUT.getCode(), ErrorMessage.EmptyData.getMessage());
- } else if (!data.containsKey("skyflow_id")) {
+ } else if (!data.containsKey("skyflowId") && !data.containsKey("skyflow_id")) {
LogUtil.printErrorLog(Utils.parameterizedString(
ErrorLogs.SKYFLOW_ID_IS_REQUIRED.getLog(), InterfaceName.UPDATE.getName()
));
throw new SkyflowException(ErrorCode.INVALID_INPUT.getCode(), ErrorMessage.SkyflowIdKeyError.getMessage());
- } else if (!(data.get("skyflow_id") instanceof String)) {
+ } else if (!(resolveUpdateId(data) instanceof String)) {
LogUtil.printErrorLog(Utils.parameterizedString(
ErrorLogs.INVALID_SKYFLOW_ID_TYPE.getLog(), InterfaceName.UPDATE.getName()
));
throw new SkyflowException(ErrorCode.INVALID_INPUT.getCode(), ErrorMessage.InvalidSkyflowIdType.getMessage());
- } else if (data.get("skyflow_id").toString().trim().isEmpty()) {
+ } else if (resolveUpdateId(data).toString().trim().isEmpty()) {
LogUtil.printErrorLog(Utils.parameterizedString(
ErrorLogs.EMPTY_SKYFLOW_ID.getLog(), InterfaceName.UPDATE.getName()
));
@@ -574,15 +576,6 @@ public static void validateUpdateRequest(UpdateRequest updateRequest) throws Sky
ErrorLogs.EMPTY_OR_NULL_KEY_IN_VALUES.getLog(), InterfaceName.UPDATE.getName()
));
throw new SkyflowException(ErrorCode.INVALID_INPUT.getCode(), ErrorMessage.EmptyKeyInValues.getMessage());
- } else {
- Object value = data.get(key);
- if (value == null || value.toString().trim().isEmpty()) {
- LogUtil.printErrorLog(Utils.parameterizedString(
- ErrorLogs.EMPTY_OR_NULL_VALUE_IN_VALUES.getLog(), InterfaceName.UPDATE.getName(), key
- ));
- throw new SkyflowException(ErrorCode.INVALID_INPUT.getCode(),
- ErrorMessage.EmptyValueInValues.getMessage());
- }
}
}
@@ -875,15 +868,6 @@ private static void validateTokensMapWithTokenStrict(
ErrorLogs.MISMATCH_OF_FIELDS_AND_TOKENS.getLog(), interfaceName
));
throw new SkyflowException(ErrorCode.INVALID_INPUT.getCode(), ErrorMessage.MismatchOfFieldsAndTokens.getMessage());
- } else {
- Object value = tokensMap.get(key);
- if (value == null || value.toString().trim().isEmpty()) {
- LogUtil.printErrorLog(Utils.parameterizedString(
- ErrorLogs.EMPTY_OR_NULL_VALUE_IN_TOKENS.getLog(),
- interfaceName, key
- ));
- throw new SkyflowException(ErrorCode.INVALID_INPUT.getCode(), ErrorMessage.EmptyValueInTokens.getMessage());
- }
}
}
}
@@ -984,11 +968,18 @@ 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());
}
}
+ static Object resolveUpdateId(HashMap data) {
+ if (data.containsKey("skyflowId")) {
+ return data.get("skyflowId");
+ }
+ return data.get("skyflow_id");
+ }
+
public static void validateGetDetectRunRequest(GetDetectRunRequest request) throws SkyflowException {
if (request == null) {
throw new SkyflowException(ErrorCode.INVALID_INPUT.getCode(), ErrorMessage.EmptyRequestBody.getMessage());
diff --git a/src/main/java/com/skyflow/vault/controller/ConnectionController.java b/src/main/java/com/skyflow/vault/controller/ConnectionController.java
index 4a9334d4..07f12dc8 100644
--- a/src/main/java/com/skyflow/vault/controller/ConnectionController.java
+++ b/src/main/java/com/skyflow/vault/controller/ConnectionController.java
@@ -55,16 +55,37 @@ public InvokeConnectionResponse invoke(InvokeConnectionRequest invokeConnectionR
Object requestBodyObject = invokeConnectionRequest.getRequestBody();
if (requestBodyObject != null) {
- try {
- requestBody = convertObjectToJson(requestBodyObject);
- } catch (Exception e) {
- LogUtil.printErrorLog(ErrorLogs.INVALID_REQUEST_HEADERS.getLog());
- throw new SkyflowException(ErrorCode.INVALID_INPUT.getCode(), ErrorMessage.InvalidRequestBody.getMessage());
+ if (requestBodyObject instanceof String) {
+ String contentType = headers.getOrDefault("content-type", "");
+ if (!contentType.isEmpty() && !contentType.toLowerCase().contains("application/json")) {
+ requestBody = new JsonObject();
+ requestBody.addProperty(Constants.HttpUtilityExtra.RAW_BODY_KEY, (String) requestBodyObject);
+ } else {
+ try {
+ requestBody = convertObjectToJson(requestBodyObject);
+ } catch (Exception e) {
+ LogUtil.printErrorLog(ErrorLogs.INVALID_REQUEST_HEADERS.getLog());
+ throw new SkyflowException(ErrorCode.INVALID_INPUT.getCode(), ErrorMessage.InvalidRequestBody.getMessage());
+ }
+ }
+ } else {
+ try {
+ requestBody = convertObjectToJson(requestBodyObject);
+ } catch (Exception e) {
+ LogUtil.printErrorLog(ErrorLogs.INVALID_REQUEST_HEADERS.getLog());
+ throw new SkyflowException(ErrorCode.INVALID_INPUT.getCode(), ErrorMessage.InvalidRequestBody.getMessage());
+ }
}
}
String response = HttpUtility.sendRequest(requestMethod.name(), new URL(filledURL), requestBody, headers);
- JsonObject data = JsonParser.parseString(response).getAsJsonObject();
+ JsonObject data;
+ try {
+ data = JsonParser.parseString(response).getAsJsonObject();
+ } catch (Exception e) {
+ data = new JsonObject();
+ data.addProperty("response", response);
+ }
HashMap metadata = new HashMap<>();
metadata.put("requestId", HttpUtility.getRequestID());
connectionResponse = new InvokeConnectionResponse(data, metadata, null);
diff --git a/src/main/java/com/skyflow/vault/controller/DetectController.java b/src/main/java/com/skyflow/vault/controller/DetectController.java
index d2dd891d..873c83e1 100644
--- a/src/main/java/com/skyflow/vault/controller/DetectController.java
+++ b/src/main/java/com/skyflow/vault/controller/DetectController.java
@@ -135,7 +135,6 @@ public DeidentifyFileResponse deidentifyFile(DeidentifyFileRequest request) thro
if (DeidentifyFileStatus.SUCCESS.value().equalsIgnoreCase(response.getStatus())) {
String base64File = response.getFileBase64();
- response.getEntities().get(0).getFile();
if (base64File != null) {
byte[] decodedBytes = Base64.getDecoder().decode(base64File);
String outputDir = request.getOutputDirectory();
@@ -297,7 +296,9 @@ private static synchronized DeidentifyFileResponse parseDeidentifyFileResponse(D
.map(Object::toString)
.orElse(DetectRunsResponseStatus.UNKNOWN.toString());
- String fileExtension = firstOutput.getProcessedFileExtension().get().toString();
+ String fileExtension = processedFileExtension
+ .map(Object::toString)
+ .orElse(DetectRunsResponseStatus.UNKNOWN.toString());
Float sizeInKb = response.getSize().orElse(null);
Float durationInSeconds = response.getDuration().orElse(null);
DeidentifyFileResponse deidentifyFileResponse = new DeidentifyFileResponse(
@@ -320,7 +321,7 @@ private static synchronized DeidentifyFileResponse parseDeidentifyFileResponse(D
}
private static synchronized DeidentifiedFileOutput getFirstOutput(DetectRunsResponse response) {
- List outputs = response.getOutput().get();
+ List outputs = response.getOutput().orElse(null);
return outputs != null && !outputs.isEmpty() ? outputs.get(0) : null;
}
diff --git a/src/main/java/com/skyflow/vault/controller/VaultController.java b/src/main/java/com/skyflow/vault/controller/VaultController.java
index acac9608..1812b83b 100644
--- a/src/main/java/com/skyflow/vault/controller/VaultController.java
+++ b/src/main/java/com/skyflow/vault/controller/VaultController.java
@@ -3,6 +3,7 @@
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
+import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@@ -80,6 +81,18 @@ public VaultController(VaultConfig vaultConfig, Credentials credentials) {
super(vaultConfig, credentials);
}
+ private static String extractUpdateSkyflowId(HashMap data) {
+ if (data.containsKey("skyflowId")) {
+ if (data.containsKey("skyflow_id")) {
+ data.remove("skyflow_id");
+ LogUtil.printWarningLog(InfoLogs.DEPRECATED_SKYFLOW_ID_REQUEST_KEY.getLog());
+ }
+ return data.remove("skyflowId").toString();
+ }
+ LogUtil.printWarningLog(InfoLogs.DEPRECATED_SKYFLOW_ID_REQUEST_KEY.getLog());
+ return data.remove("skyflow_id").toString();
+ }
+
private static synchronized HashMap getFormattedBatchInsertRecord(Object record, Integer requestIndex) {
HashMap insertRecord = new HashMap<>();
String jsonString = GSON.toJson(record);
@@ -90,7 +103,12 @@ private static synchronized HashMap getFormattedBatchInsertRecor
if (records != null) {
for (JsonElement recordElement : records) {
JsonObject recordObject = recordElement.getAsJsonObject();
- insertRecord.put("skyflowId", recordObject.get("skyflow_id").getAsString());
+ if (recordObject.has("skyflowId")) {
+ insertRecord.put("skyflowId", recordObject.get("skyflowId").getAsString());
+ } else if (recordObject.has("skyflow_id")) {
+ insertRecord.put("skyflowId", recordObject.get("skyflow_id").getAsString());
+ LogUtil.printWarningLog(InfoLogs.DEPRECATED_SKYFLOW_ID_KEY.getLog());
+ }
JsonElement tokensElement = recordObject.get("tokens");
if (tokensElement != null) {
insertRecord.putAll(tokensElement.getAsJsonObject().asMap());
@@ -129,6 +147,12 @@ private static synchronized HashMap getFormattedGetRecord(V1Fiel
} else if (tokensOpt.isPresent()) {
getRecord.putAll(tokensOpt.get());
}
+
+ if (getRecord.containsKey("skyflow_id")) {
+ getRecord.put("skyflowId", getRecord.get("skyflow_id"));
+ LogUtil.printWarningLog(InfoLogs.DEPRECATED_SKYFLOW_ID_KEY.getLog());
+ }
+
return getRecord;
}
@@ -148,6 +172,12 @@ private static synchronized HashMap getFormattedQueryRecord(V1Fi
if (fieldsOpt.isPresent()) {
queryRecord.putAll(fieldsOpt.get());
}
+
+ if (queryRecord.containsKey("skyflow_id")) {
+ queryRecord.put("skyflowId", queryRecord.get("skyflow_id"));
+ LogUtil.printWarningLog(InfoLogs.DEPRECATED_SKYFLOW_ID_KEY.getLog());
+ }
+
return queryRecord;
}
@@ -208,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);
}
@@ -279,7 +309,7 @@ public GetResponse get(GetRequest getRequest) throws SkyflowException {
.tokenization(getRequest.getReturnTokens())
.offset(getRequest.getOffset())
.limit(getRequest.getLimit())
- .downloadUrl(getRequest.getDownloadURL())
+ .downloadUrl(getRequest.getDownloadUrl())
.columnName(getRequest.getColumnName())
.columnValues(getRequest.getColumnValues())
.fields(getRequest.getFields())
@@ -323,7 +353,7 @@ public UpdateResponse update(UpdateRequest updateRequest) throws SkyflowExceptio
result = super.getRecordsApi().recordServiceUpdateRecord(
super.getVaultConfig().getVaultId(),
updateRequest.getTable(),
- updateRequest.getData().remove("skyflow_id").toString(),
+ extractUpdateSkyflowId(updateRequest.getData()),
updateBody,
requestOptions
);
@@ -359,7 +389,7 @@ public DeleteResponse delete(DeleteRequest deleteRequest) throws SkyflowExceptio
throw new SkyflowException(e.statusCode(), e, e.headers(), bodyString);
}
LogUtil.printInfoLog(InfoLogs.DELETE_SUCCESS.getLog());
- return new DeleteResponse(result.getRecordIdResponse().get());
+ return new DeleteResponse(result.getRecordIdResponse().orElse(Collections.emptyList()));
}
public QueryResponse query(QueryRequest queryRequest) throws SkyflowException {
diff --git a/src/main/java/com/skyflow/vault/data/GetRequest.java b/src/main/java/com/skyflow/vault/data/GetRequest.java
index 04626e35..0fccf7b8 100644
--- a/src/main/java/com/skyflow/vault/data/GetRequest.java
+++ b/src/main/java/com/skyflow/vault/data/GetRequest.java
@@ -1,7 +1,9 @@
package com.skyflow.vault.data;
import com.skyflow.enums.RedactionType;
+import com.skyflow.logs.InfoLogs;
import com.skyflow.utils.Constants;
+import com.skyflow.utils.logger.LogUtil;
import java.util.ArrayList;
@@ -44,8 +46,17 @@ public String getLimit() {
return this.builder.limit;
}
+ /**
+ * @deprecated Use {@link #getDownloadUrl()} instead.
+ */
+ @Deprecated(since = "2.1", forRemoval = true)
public Boolean getDownloadURL() {
- return this.builder.downloadURL;
+ LogUtil.printWarningLog(InfoLogs.DEPRECATED_DOWNLOAD_URL.getLog());
+ return getDownloadUrl();
+ }
+
+ public Boolean getDownloadUrl() {
+ return this.builder.downloadUrl;
}
public String getColumnName() {
@@ -68,14 +79,14 @@ public static final class GetRequestBuilder {
private ArrayList fields;
private String offset;
private String limit;
- private Boolean downloadURL;
+ private Boolean downloadUrl;
private String columnName;
private ArrayList columnValues;
private String orderBy;
private GetRequestBuilder() {
- this.downloadURL = true;
this.orderBy = Constants.ORDER_ASCENDING;
+ this.downloadUrl = true;
}
public GetRequestBuilder table(String table) {
@@ -113,8 +124,17 @@ public GetRequestBuilder limit(String limit) {
return this;
}
+ /**
+ * @deprecated Use {@link #downloadUrl(Boolean)} instead.
+ */
+ @Deprecated(since = "2.1", forRemoval = true)
public GetRequestBuilder downloadURL(Boolean downloadURL) {
- this.downloadURL = downloadURL == null || downloadURL;
+ LogUtil.printWarningLog(InfoLogs.DEPRECATED_DOWNLOAD_URL.getLog());
+ return downloadUrl(downloadURL);
+ }
+
+ public GetRequestBuilder downloadUrl(Boolean downloadUrl) {
+ this.downloadUrl = downloadUrl == null || downloadUrl;
return this;
}
diff --git a/src/main/java/com/skyflow/vault/data/GetResponse.java b/src/main/java/com/skyflow/vault/data/GetResponse.java
index 34a01303..365fe38e 100644
--- a/src/main/java/com/skyflow/vault/data/GetResponse.java
+++ b/src/main/java/com/skyflow/vault/data/GetResponse.java
@@ -14,6 +14,14 @@ public GetResponse(ArrayList> data, ArrayListDeprecation notice: The {@code skyflow_id} key in each record map is
+ * deprecated and will be removed in an upcoming release. Use {@code skyflowId} instead.
+ * Both keys are present simultaneously in v2 for backward compatibility.
+ */
public ArrayList> getData() {
return data;
}
diff --git a/src/main/java/com/skyflow/vault/data/QueryResponse.java b/src/main/java/com/skyflow/vault/data/QueryResponse.java
index 7a1bca51..afb32c60 100644
--- a/src/main/java/com/skyflow/vault/data/QueryResponse.java
+++ b/src/main/java/com/skyflow/vault/data/QueryResponse.java
@@ -1,32 +1,52 @@
package com.skyflow.vault.data;
-import com.google.gson.*;
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+import com.google.gson.JsonArray;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonObject;
import java.util.ArrayList;
import java.util.HashMap;
public class QueryResponse {
private final ArrayList> fields;
- private ArrayList> tokenizedData;
+ private final ArrayList> errors;
public QueryResponse(ArrayList> fields) {
this.fields = fields;
+ this.errors = null;
}
+ /**
+ * Returns the list of record maps from the Query response. Each map contains all
+ * field name/value pairs for the record.
+ *
+ * Deprecation notice: The {@code skyflow_id} key in each record map is
+ * deprecated and will be removed in an upcoming release. Use {@code skyflowId} instead.
+ * Both keys are present simultaneously in v2 for backward compatibility.
+ */
public ArrayList> getFields() {
return fields;
}
+ /**
+ * Always returns null. The Query API does not support partial-error responses.
+ */
+ public ArrayList> getErrors() {
+ return errors;
+ }
+
@Override
public String toString() {
Gson gson = new GsonBuilder().serializeNulls().create();
JsonObject responseObject = gson.toJsonTree(this).getAsJsonObject();
JsonArray fieldsArray = responseObject.get("fields").getAsJsonArray();
+ // tokenizedData is intentionally injected per-record — Query API cannot return tokens;
+ // this ensures the field is always present in serialised output for cross-SDK consistency
for (JsonElement fieldElement : fieldsArray) {
fieldElement.getAsJsonObject().add("tokenizedData", new JsonObject());
}
- responseObject.add("errors", null);
- responseObject.remove("tokenizedData");
return responseObject.toString();
}
}
diff --git a/src/main/java/com/skyflow/vault/tokens/DetokenizeRequest.java b/src/main/java/com/skyflow/vault/tokens/DetokenizeRequest.java
index 186a18d2..481c0c16 100644
--- a/src/main/java/com/skyflow/vault/tokens/DetokenizeRequest.java
+++ b/src/main/java/com/skyflow/vault/tokens/DetokenizeRequest.java
@@ -1,5 +1,8 @@
package com.skyflow.vault.tokens;
+import com.skyflow.logs.InfoLogs;
+import com.skyflow.utils.logger.LogUtil;
+
import java.util.ArrayList;
public class DetokenizeRequest {
@@ -21,18 +24,27 @@ public Boolean getContinueOnError() {
return this.builder.continueOnError;
}
+ /**
+ * @deprecated Use {@link #getDownloadUrl()} instead.
+ */
+ @Deprecated(since = "2.1", forRemoval = true)
public Boolean getDownloadURL() {
- return this.builder.downloadURL;
+ LogUtil.printWarningLog(InfoLogs.DEPRECATED_DOWNLOAD_URL.getLog());
+ return getDownloadUrl();
+ }
+
+ public Boolean getDownloadUrl() {
+ return this.builder.downloadUrl;
}
public static final class DetokenizeRequestBuilder {
private ArrayList detokenizeData;
private Boolean continueOnError;
- private Boolean downloadURL;
+ private Boolean downloadUrl;
private DetokenizeRequestBuilder() {
this.continueOnError = false;
- this.downloadURL = false;
+ this.downloadUrl = false;
}
public DetokenizeRequestBuilder detokenizeData(ArrayList detokenizeData) {
@@ -45,8 +57,17 @@ public DetokenizeRequestBuilder continueOnError(Boolean continueOnError) {
return this;
}
+ /**
+ * @deprecated Use {@link #downloadUrl(Boolean)} instead.
+ */
+ @Deprecated(since = "2.1", forRemoval = true)
public DetokenizeRequestBuilder downloadURL(Boolean downloadURL) {
- this.downloadURL = downloadURL;
+ LogUtil.printWarningLog(InfoLogs.DEPRECATED_DOWNLOAD_URL.getLog());
+ return downloadUrl(downloadURL);
+ }
+
+ public DetokenizeRequestBuilder downloadUrl(Boolean downloadUrl) {
+ this.downloadUrl = downloadUrl;
return this;
}
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 0b44c0b4..983419a7 100644
--- a/src/test/java/com/skyflow/SkyflowTests.java
+++ b/src/test/java/com/skyflow/SkyflowTests.java
@@ -8,13 +8,36 @@
import com.skyflow.errors.ErrorCode;
import com.skyflow.errors.ErrorMessage;
import com.skyflow.errors.SkyflowException;
+import com.skyflow.logs.InfoLogs;
+import com.skyflow.utils.logger.LogUtil;
import org.junit.Assert;
import org.junit.BeforeClass;
import org.junit.Test;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.logging.Handler;
+import java.util.logging.Level;
+import java.util.logging.LogRecord;
+import java.util.logging.Logger;
+
public class SkyflowTests {
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 class CapturingHandler extends Handler {
+ final List records = new ArrayList<>();
+ @Override public void publish(LogRecord r) { records.add(r); }
+ @Override public void flush() {}
+ @Override public void close() {}
+ }
+
+ private CapturingHandler attachCapture() {
+ CapturingHandler handler = new CapturingHandler();
+ handler.setLevel(Level.ALL);
+ Logger.getLogger(LogUtil.class.getName()).addHandler(handler);
+ return handler;
+ }
private static String vaultID = null;
private static String clusterID = null;
private static String newClusterID = null;
@@ -150,6 +173,31 @@ public void testUpdatingValidVaultConfigInSkyflowClient() {
}
}
+ @Test
+ public void testUpdateVaultConfigNullCredentialsFallsBackToPrevious() {
+ try {
+ VaultConfig config = new VaultConfig();
+ config.setVaultId(vaultID);
+ config.setClusterId(clusterID);
+ config.setEnv(Env.SANDBOX);
+
+ Credentials creds = new Credentials();
+ creds.setToken(token);
+ config.setCredentials(creds);
+
+ Skyflow skyflowClient = Skyflow.builder().addVaultConfig(config).build();
+
+ // Update with null credentials — should retain previous credentials value
+ VaultConfig partialUpdate = new VaultConfig();
+ partialUpdate.setVaultId(vaultID);
+ partialUpdate.setClusterId(clusterID);
+ skyflowClient.updateVaultConfig(partialUpdate);
+ Assert.assertNotNull(skyflowClient.getVaultConfig(vaultID).getCredentials());
+ } catch (SkyflowException e) {
+ Assert.fail(INVALID_EXCEPTION_THROWN);
+ }
+ }
+
@Test
public void testRemovingNonExistentVaultConfigInSkyflowBuilder() {
try {
@@ -339,6 +387,32 @@ public void testUpdatingValidConnectionConfigInSkyflowClient() {
}
}
+ @Test
+ public void testUpdateConnectionConfigWithNullFieldsFallsBackToPrevious() {
+ try {
+ ConnectionConfig config = new ConnectionConfig();
+ config.setConnectionId(connectionID);
+ config.setConnectionUrl(connectionURL);
+
+ Credentials creds = new Credentials();
+ creds.setToken(token);
+ config.setCredentials(creds);
+
+ Skyflow skyflowClient = Skyflow.builder().addConnectionConfig(config).build();
+
+ // Update with null credentials — validation requires connectionUrl, so provide it;
+ // credentials should fall back to previous value
+ ConnectionConfig partialUpdate = new ConnectionConfig();
+ partialUpdate.setConnectionId(connectionID);
+ partialUpdate.setConnectionUrl(connectionURL);
+ // credentials is null → should retain previous value
+ skyflowClient.updateConnectionConfig(partialUpdate);
+ Assert.assertNotNull(skyflowClient.getConnectionConfig(connectionID).getCredentials());
+ } catch (SkyflowException e) {
+ Assert.fail(INVALID_EXCEPTION_THROWN);
+ }
+ }
+
@Test
public void testRemovingNonExistentConnectionConfigInSkyflowBuilder() {
try {
@@ -429,6 +503,19 @@ public void testDefaultLogLevel() {
}
@Test
+ public void testSetLogLevel() {
+ try {
+ Skyflow skyflowClient = Skyflow.builder().setLogLevel(LogLevel.INFO).build();
+ Assert.assertEquals(LogLevel.INFO, skyflowClient.getLogLevel());
+ skyflowClient.setLogLevel(LogLevel.WARN);
+ Assert.assertEquals(LogLevel.WARN, skyflowClient.getLogLevel());
+ } catch (Exception e) {
+ Assert.fail(INVALID_EXCEPTION_THROWN);
+ }
+ }
+
+ @Test
+ @SuppressWarnings("deprecation")
public void testUpdateLogLevel() {
try {
Skyflow skyflowClient = Skyflow.builder().setLogLevel(LogLevel.INFO).build();
@@ -440,6 +527,38 @@ public void testUpdateLogLevel() {
}
}
+ @Test
+ @SuppressWarnings("deprecation")
+ public void testUpdateLogLevelEmitsDeprecationWarning() {
+ try {
+ // build() calls setupLogger internally — attach capture after so it isn't wiped
+ Skyflow skyflowClient = Skyflow.builder().setLogLevel(LogLevel.INFO).build();
+ CapturingHandler handler = attachCapture();
+ skyflowClient.updateLogLevel(LogLevel.WARN);
+ boolean warnFired = handler.records.stream()
+ .anyMatch(r -> r.getLevel().equals(Level.WARNING)
+ && r.getMessage().contains(InfoLogs.DEPRECATED_UPDATE_LOG_LEVEL.getLog()));
+ Assert.assertTrue("updateLogLevel() should emit a deprecation warning log", warnFired);
+ } catch (Exception e) {
+ Assert.fail(INVALID_EXCEPTION_THROWN);
+ }
+ }
+
+ @Test
+ @SuppressWarnings("deprecation")
+ public void testUpdateLogLevelWarningIsSuppressedAtErrorLevel() {
+ try {
+ Skyflow skyflowClient = Skyflow.builder().setLogLevel(LogLevel.ERROR).build();
+ CapturingHandler handler = attachCapture();
+ skyflowClient.updateLogLevel(LogLevel.WARN);
+ boolean warnFired = handler.records.stream()
+ .anyMatch(r -> r.getLevel().equals(Level.WARNING));
+ Assert.assertFalse("updateLogLevel() warning should be suppressed at ERROR log level", warnFired);
+ } catch (Exception e) {
+ Assert.fail(INVALID_EXCEPTION_THROWN);
+ }
+ }
+
@Test
public void testVaultMethodWithNoConfig() {
try {
@@ -566,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 4c9be65f..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";
@@ -928,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/config/ManagementConfigTest.java b/src/test/java/com/skyflow/config/ManagementConfigTest.java
new file mode 100644
index 00000000..6c11f2fc
--- /dev/null
+++ b/src/test/java/com/skyflow/config/ManagementConfigTest.java
@@ -0,0 +1,14 @@
+package com.skyflow.config;
+
+import org.junit.Assert;
+import org.junit.Test;
+
+public class ManagementConfigTest {
+
+ @Test
+ public void testInstantiation() {
+ // Package-private constructor — accessible from same package
+ ManagementConfig config = new ManagementConfig();
+ Assert.assertNotNull(config);
+ }
+}
diff --git a/src/test/java/com/skyflow/enums/DeidentifyFileStatusTest.java b/src/test/java/com/skyflow/enums/DeidentifyFileStatusTest.java
new file mode 100644
index 00000000..6d70a17c
--- /dev/null
+++ b/src/test/java/com/skyflow/enums/DeidentifyFileStatusTest.java
@@ -0,0 +1,27 @@
+package com.skyflow.enums;
+
+import org.junit.Assert;
+import org.junit.Test;
+
+public class DeidentifyFileStatusTest {
+
+ @Test
+ public void testInProgress() {
+ Assert.assertEquals("IN_PROGRESS", DeidentifyFileStatus.IN_PROGRESS.value());
+ }
+
+ @Test
+ public void testFailed() {
+ Assert.assertEquals("FAILED", DeidentifyFileStatus.FAILED.value());
+ }
+
+ @Test
+ public void testSuccess() {
+ Assert.assertEquals("SUCCESS", DeidentifyFileStatus.SUCCESS.value());
+ }
+
+ @Test
+ public void testUnknown() {
+ Assert.assertEquals("UNKNOWN", DeidentifyFileStatus.UNKNOWN.value());
+ }
+}
diff --git a/src/test/java/com/skyflow/enums/DetectEntitiesTest.java b/src/test/java/com/skyflow/enums/DetectEntitiesTest.java
new file mode 100644
index 00000000..2bb29f0c
--- /dev/null
+++ b/src/test/java/com/skyflow/enums/DetectEntitiesTest.java
@@ -0,0 +1,24 @@
+package com.skyflow.enums;
+
+import org.junit.Assert;
+import org.junit.Test;
+
+public class DetectEntitiesTest {
+
+ @Test
+ public void testGetDetectEntities() {
+ Assert.assertEquals("account_number", DetectEntities.ACCOUNT_NUMBER.getDetectEntities());
+ Assert.assertEquals("account_number", DetectEntities.ACCOUNT_NUMBER.toString());
+ }
+
+ @Test
+ public void testAll() {
+ Assert.assertEquals("all", DetectEntities.ALL.getDetectEntities());
+ }
+
+ @Test
+ public void testName() {
+ Assert.assertEquals("name", DetectEntities.NAME.getDetectEntities());
+ Assert.assertEquals("name", DetectEntities.NAME.toString());
+ }
+}
diff --git a/src/test/java/com/skyflow/enums/DetectOutputTranscriptionsTest.java b/src/test/java/com/skyflow/enums/DetectOutputTranscriptionsTest.java
new file mode 100644
index 00000000..6df404ed
--- /dev/null
+++ b/src/test/java/com/skyflow/enums/DetectOutputTranscriptionsTest.java
@@ -0,0 +1,31 @@
+package com.skyflow.enums;
+
+import org.junit.Assert;
+import org.junit.Test;
+
+public class DetectOutputTranscriptionsTest {
+
+ @Test
+ public void testDiarizedTranscription() {
+ Assert.assertEquals("diarized_transcription", DetectOutputTranscriptions.DIARIZED_TRANSCRIPTION.getDetectOutputTranscriptions());
+ Assert.assertEquals("diarized_transcription", DetectOutputTranscriptions.DIARIZED_TRANSCRIPTION.toString());
+ }
+
+ @Test
+ public void testMedicalDiarizedTranscription() {
+ Assert.assertEquals("medical_diarized_transcription", DetectOutputTranscriptions.MEDICAL_DIARIZED_TRANSCRIPTION.getDetectOutputTranscriptions());
+ Assert.assertEquals("medical_diarized_transcription", DetectOutputTranscriptions.MEDICAL_DIARIZED_TRANSCRIPTION.toString());
+ }
+
+ @Test
+ public void testMedicalTranscription() {
+ Assert.assertEquals("medical_transcription", DetectOutputTranscriptions.MEDICAL_TRANSCRIPTION.getDetectOutputTranscriptions());
+ Assert.assertEquals("medical_transcription", DetectOutputTranscriptions.MEDICAL_TRANSCRIPTION.toString());
+ }
+
+ @Test
+ public void testTranscription() {
+ Assert.assertEquals("transcription", DetectOutputTranscriptions.TRANSCRIPTION.getDetectOutputTranscriptions());
+ Assert.assertEquals("transcription", DetectOutputTranscriptions.TRANSCRIPTION.toString());
+ }
+}
diff --git a/src/test/java/com/skyflow/enums/MaskingMethodTest.java b/src/test/java/com/skyflow/enums/MaskingMethodTest.java
new file mode 100644
index 00000000..f675189e
--- /dev/null
+++ b/src/test/java/com/skyflow/enums/MaskingMethodTest.java
@@ -0,0 +1,19 @@
+package com.skyflow.enums;
+
+import org.junit.Assert;
+import org.junit.Test;
+
+public class MaskingMethodTest {
+
+ @Test
+ public void testBlackbox() {
+ Assert.assertEquals("blackbox", MaskingMethod.BLACKBOX.getMaskingMethod());
+ Assert.assertEquals("blackbox", MaskingMethod.BLACKBOX.toString());
+ }
+
+ @Test
+ public void testBlur() {
+ Assert.assertEquals("blur", MaskingMethod.BLUR.getMaskingMethod());
+ Assert.assertEquals("blur", MaskingMethod.BLUR.toString());
+ }
+}
diff --git a/src/test/java/com/skyflow/enums/TokenModeTest.java b/src/test/java/com/skyflow/enums/TokenModeTest.java
new file mode 100644
index 00000000..72236a16
--- /dev/null
+++ b/src/test/java/com/skyflow/enums/TokenModeTest.java
@@ -0,0 +1,113 @@
+package com.skyflow.enums;
+
+import com.skyflow.enums.LogLevel;
+import com.skyflow.generated.rest.types.V1Byot;
+import com.skyflow.logs.InfoLogs;
+import com.skyflow.utils.logger.LogUtil;
+import org.junit.Assert;
+import org.junit.Test;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.logging.Handler;
+import java.util.logging.Level;
+import java.util.logging.LogRecord;
+import java.util.logging.Logger;
+
+public class TokenModeTest {
+
+ private static class CapturingHandler extends Handler {
+ final List records = new ArrayList<>();
+ @Override public void publish(LogRecord r) { records.add(r); }
+ @Override public void flush() {}
+ @Override public void close() {}
+ }
+
+ private CapturingHandler attachCapture() {
+ CapturingHandler handler = new CapturingHandler();
+ handler.setLevel(Level.ALL);
+ Logger.getLogger(LogUtil.class.getName()).addHandler(handler);
+ return handler;
+ }
+
+ // --- getByot() ---
+
+ @Test
+ public void testGetByotDisable() {
+ Assert.assertEquals(V1Byot.DISABLE, TokenMode.DISABLE.getByot());
+ }
+
+ @Test
+ public void testGetByotEnable() {
+ Assert.assertEquals(V1Byot.ENABLE, TokenMode.ENABLE.getByot());
+ }
+
+ @Test
+ public void testGetByotEnableStrict() {
+ Assert.assertEquals(V1Byot.ENABLE_STRICT, TokenMode.ENABLE_STRICT.getByot());
+ }
+
+ // --- getBYOT() delegates and emits a runtime warning ---
+
+ @Test
+ @SuppressWarnings("deprecation")
+ public void testGetBYOTDelegatesToGetByotDisable() {
+ Assert.assertEquals(V1Byot.DISABLE, TokenMode.DISABLE.getBYOT());
+ }
+
+ @Test
+ @SuppressWarnings("deprecation")
+ public void testGetBYOTDelegatesToGetByotEnable() {
+ Assert.assertEquals(V1Byot.ENABLE, TokenMode.ENABLE.getBYOT());
+ }
+
+ @Test
+ @SuppressWarnings("deprecation")
+ public void testGetBYOTDelegatesToGetByotEnableStrict() {
+ Assert.assertEquals(V1Byot.ENABLE_STRICT, TokenMode.ENABLE_STRICT.getBYOT());
+ }
+
+ @Test
+ @SuppressWarnings("deprecation")
+ public void testGetBYOTEmitsDeprecationWarning() {
+ LogUtil.setupLogger(LogLevel.INFO);
+ CapturingHandler handler = attachCapture();
+
+ TokenMode.ENABLE.getBYOT();
+
+ boolean warnFired = handler.records.stream()
+ .anyMatch(r -> r.getLevel().equals(Level.WARNING)
+ && r.getMessage().contains(InfoLogs.DEPRECATED_GET_BYOT.getLog()));
+ Assert.assertTrue("getBYOT() should emit a deprecation warning log", warnFired);
+ }
+
+ @Test
+ @SuppressWarnings("deprecation")
+ public void testGetBYOTWarningIsSuppressedAtErrorLevel() {
+ LogUtil.setupLogger(LogLevel.ERROR);
+ CapturingHandler handler = attachCapture();
+
+ TokenMode.ENABLE.getBYOT();
+
+ boolean warnFired = handler.records.stream()
+ .anyMatch(r -> r.getLevel().equals(Level.WARNING));
+ Assert.assertFalse("getBYOT() warning should be suppressed at ERROR log level", warnFired);
+ }
+
+ // --- toString() ---
+
+ @Test
+ public void testToStringDisable() {
+ Assert.assertEquals("DISABLE", TokenMode.DISABLE.toString());
+ }
+
+ @Test
+ public void testToStringEnable() {
+ Assert.assertEquals("ENABLE", TokenMode.ENABLE.toString());
+ }
+
+ @Test
+ public void testToStringEnableStrict() {
+ Assert.assertEquals("ENABLE_STRICT", TokenMode.ENABLE_STRICT.toString());
+ }
+}
diff --git a/src/test/java/com/skyflow/enums/TokenTypeTest.java b/src/test/java/com/skyflow/enums/TokenTypeTest.java
new file mode 100644
index 00000000..4f5c0901
--- /dev/null
+++ b/src/test/java/com/skyflow/enums/TokenTypeTest.java
@@ -0,0 +1,30 @@
+package com.skyflow.enums;
+
+import org.junit.Assert;
+import org.junit.Test;
+
+public class TokenTypeTest {
+
+ @Test
+ public void testVaultToken() {
+ Assert.assertEquals("vault_token", TokenType.VAULT_TOKEN.getTokenType());
+ Assert.assertEquals("vault_token", TokenType.VAULT_TOKEN.toString());
+ }
+
+ @Test
+ public void testEntityUniqueCounter() {
+ Assert.assertEquals("entity_unq_counter", TokenType.ENTITY_UNIQUE_COUNTER.getTokenType());
+ Assert.assertEquals("entity_unq_counter", TokenType.ENTITY_UNIQUE_COUNTER.toString());
+ }
+
+ @Test
+ public void testEntityOnly() {
+ Assert.assertEquals("entity_only", TokenType.ENTITY_ONLY.getTokenType());
+ Assert.assertEquals("entity_only", TokenType.ENTITY_ONLY.toString());
+ }
+
+ @Test
+ public void testGetDefault() {
+ Assert.assertEquals(TokenType.ENTITY_UNIQUE_COUNTER.getTokenType(), TokenType.VAULT_TOKEN.getDefault());
+ }
+}
diff --git a/src/test/java/com/skyflow/errors/SkyflowExceptionTest.java b/src/test/java/com/skyflow/errors/SkyflowExceptionTest.java
index e2a1d921..83df09ee 100644
--- a/src/test/java/com/skyflow/errors/SkyflowExceptionTest.java
+++ b/src/test/java/com/skyflow/errors/SkyflowExceptionTest.java
@@ -137,4 +137,57 @@ public void testToStringWithNullFields() {
Assert.assertTrue(str.contains("httpStatus: null"));
Assert.assertTrue(str.contains("details: null"));
}
+
+ @Test
+ public void testZeroHttpCodeDefaultsTo400() {
+ Map> headers = new HashMap<>();
+ String json = "{\"error\":{\"message\":\"zero code\",\"grpc_code\":1,\"http_status\":\"BAD_REQUEST\"}}";
+ SkyflowException ex = new SkyflowException(0, new RuntimeException("fail"), headers, json);
+ Assert.assertEquals(400, ex.getHttpCode());
+ }
+
+ @Test
+ public void testJsonBodyWithoutErrorKey() {
+ Map> headers = new HashMap<>();
+ headers.put("x-request-id", Collections.singletonList("req-no-error"));
+ String json = "{\"message\":\"no error key here\"}";
+ SkyflowException ex = new SkyflowException(400, new RuntimeException("fail"), headers, json);
+ Assert.assertEquals("req-no-error", ex.getRequestId());
+ Assert.assertNull(ex.getGrpcCode());
+ Assert.assertNull(ex.getMessage());
+ }
+
+ @Test
+ public void testJsonErrorBodyWithNoMessageField() {
+ Map> headers = new HashMap<>();
+ String json = "{\"error\":{\"grpc_code\":3,\"http_status\":\"INVALID_ARGUMENT\"}}";
+ SkyflowException ex = new SkyflowException(400, new RuntimeException("fail"), headers, json);
+ Assert.assertNull(ex.getMessage());
+ Assert.assertEquals(Integer.valueOf(3), ex.getGrpcCode());
+ }
+
+ @Test
+ public void testJsonErrorBodyWithNoGrpcCodeField() {
+ Map> headers = new HashMap<>();
+ String json = "{\"error\":{\"message\":\"some error\",\"http_status\":\"BAD_REQUEST\"}}";
+ SkyflowException ex = new SkyflowException(400, new RuntimeException("fail"), headers, json);
+ Assert.assertEquals("some error", ex.getMessage());
+ Assert.assertNull(ex.getGrpcCode());
+ }
+
+ @Test
+ public void testNonJsonBodyFallsBackToRawBodyAsMessage() {
+ Map> headers = new HashMap<>();
+ String body = "plain text error response";
+ SkyflowException ex = new SkyflowException(500, new RuntimeException("fail"), headers, body);
+ Assert.assertEquals("plain text error response", ex.getMessage());
+ }
+
+ @Test
+ public void testNullBodyNullCauseMessageFallsBackToErrorOccurred() {
+ Map> headers = new HashMap<>();
+ SkyflowException ex = new SkyflowException(500, new RuntimeException((String) null), headers, null);
+ Assert.assertNotNull(ex.getMessage());
+ Assert.assertTrue(ex.getMessage().contains("API error"));
+ }
}
\ No newline at end of file
diff --git a/src/test/java/com/skyflow/serviceaccount/util/BearerTokenTests.java b/src/test/java/com/skyflow/serviceaccount/util/BearerTokenTests.java
index ecd38e84..3aeca811 100644
--- a/src/test/java/com/skyflow/serviceaccount/util/BearerTokenTests.java
+++ b/src/test/java/com/skyflow/serviceaccount/util/BearerTokenTests.java
@@ -3,6 +3,7 @@
import com.skyflow.errors.ErrorCode;
import com.skyflow.errors.ErrorMessage;
import com.skyflow.errors.SkyflowException;
+import com.skyflow.utils.Constants;
import com.skyflow.utils.Utils;
import org.junit.Assert;
import org.junit.BeforeClass;
@@ -225,7 +226,7 @@ public void testInvalidPrivateKeyInCredentialsForCredentials() {
@Test
public void testInvalidKeySpecInCredentialsForCredentials() {
- String credentialsString = "{\"privateKey\": \"-----BEGIN PRIVATE KEY-----\\ncHJpdmF0ZV9rZXlfdmFsdWU=\\n-----END PRIVATE KEY-----\", \"clientID\": \"client_id_value\", \"keyID\": \"key_id_value\", \"tokenURI\": \"invalid_token_uri\"}";
+ String credentialsString = "{\"privateKey\": \"-----BEGIN PRIVATE KEY-----\\ncHJpdmF0ZV9rZXlfdmFsdWU=\\n-----END PRIVATE KEY-----\", \"clientID\": \"client_id_value\", \"keyID\": \"key_id_value\", \"tokenURI\": \"invalid_token_uri\"}"; // gitleaks:allow
try {
BearerToken bearerToken = BearerToken.builder().setCredentials(credentialsString).build();
bearerToken.getBearerToken();
@@ -249,4 +250,22 @@ public void testInvalidTokenURIInCredentialsForCredentials() throws SkyflowExcep
Assert.assertEquals(ErrorMessage.InvalidTokenUri.getMessage(), e.getMessage());
}
}
+
+ @Test
+ public void testBearerTokenWithNewFormCredentialKeys() {
+ try {
+ // Fake key — fails at RSA parsing, not at field lookup, confirming new-form keys were accepted
+ String credentialsString = "{\"privateKey\": \"-----BEGIN PRIVATE KEY-----\\ncHJpdmF0ZV9rZXlfdmFsdWU=\\n-----END PRIVATE KEY-----\", " // gitleaks:allow
+ + "\"clientId\": \"client_id_value\", \"keyId\": \"key_id_value\", \"tokenUri\": \"invalid_token_uri\"}";
+ BearerToken bearerToken = BearerToken.builder().setCredentials(credentialsString).build();
+ bearerToken.getBearerToken();
+ Assert.fail(EXCEPTION_NOT_THROWN);
+ } catch (SkyflowException e) {
+ Assert.assertEquals(ErrorCode.INVALID_INPUT.getCode(), e.getHttpCode());
+ // InvalidKeySpec confirms all credential fields were resolved — failure is at RSA parsing, not field lookup
+ Assert.assertEquals(
+ Utils.parameterizedString(ErrorMessage.InvalidKeySpec.getMessage(), Constants.SDK_PREFIX),
+ e.getMessage());
+ }
+ }
}
diff --git a/src/test/java/com/skyflow/serviceaccount/util/SignedDataTokensTests.java b/src/test/java/com/skyflow/serviceaccount/util/SignedDataTokensTests.java
index 5e2cbe60..93d69b0e 100644
--- a/src/test/java/com/skyflow/serviceaccount/util/SignedDataTokensTests.java
+++ b/src/test/java/com/skyflow/serviceaccount/util/SignedDataTokensTests.java
@@ -221,6 +221,26 @@ public void testInvalidKeySpecInCredentials() {
}
}
+ @Test
+ public void testSignedDataTokensWithNewFormCredentialKeys() {
+ try {
+ String credentialsString = "{\"privateKey\": \"-----BEGIN PRIVATE KEY-----\\ncHJpdmF0ZV9rZXlfdmFsdWU=\\n-----END PRIVATE KEY-----\", "
+ + "\"clientId\": \"client_id_value\", \"keyId\": \"key_id_value\"}";
+ ArrayList dataTokens = new ArrayList<>();
+ dataTokens.add("test-token");
+ SignedDataTokens signedDataTokens = SignedDataTokens.builder()
+ .setCredentials(credentialsString)
+ .setDataTokens(dataTokens)
+ .build();
+ signedDataTokens.getSignedDataTokens();
+ Assert.fail(EXCEPTION_NOT_THROWN);
+ } catch (SkyflowException e) {
+ // Failure at RSA key parsing (not field lookup) confirms new-form keys clientId/keyId were found
+ Assert.assertEquals(ErrorCode.INVALID_INPUT.getCode(), e.getHttpCode());
+ Assert.assertEquals(ErrorMessage.InvalidKeySpec.getMessage(), e.getMessage());
+ }
+ }
+
@Test
public void testSignedDataTokenResponse() {
try {
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 f7214690..0dbdf00b 100644
--- a/src/test/java/com/skyflow/utils/HttpUtilityTests.java
+++ b/src/test/java/com/skyflow/utils/HttpUtilityTests.java
@@ -124,4 +124,105 @@ public void testSendRequestError() {
fail(INVALID_EXCEPTION_THROWN);
}
}
+
+ @Test
+ @PrepareForTest({URL.class, HttpURLConnection.class})
+ public void testSendRequestWithRawBody() {
+ try {
+ given(mockConnection.getRequestProperty("content-type")).willReturn("application/xml");
+ Map headers = new HashMap<>();
+ headers.put("content-type", "application/xml");
+ JsonObject params = new JsonObject();
+ params.addProperty("__raw_body__", "test");
+ String response = httpUtility.sendRequest("POST", url, params, headers);
+ Assert.assertEquals(expected, response);
+ } catch (Exception e) {
+ fail(INVALID_EXCEPTION_THROWN);
+ }
+ }
+
+ @Test
+ @PrepareForTest({URL.class, HttpURLConnection.class})
+ public void testSendRequestWithoutContentTypeHeader() {
+ try {
+ given(mockConnection.getRequestProperty("content-type")).willReturn("application/json");
+ JsonObject params = new JsonObject();
+ params.addProperty("key", "value");
+ String response = httpUtility.sendRequest("POST", url, params, null);
+ Assert.assertEquals(expected, response);
+ } catch (Exception e) {
+ fail(INVALID_EXCEPTION_THROWN);
+ }
+ }
+
+ @Test
+ @PrepareForTest({URL.class, HttpURLConnection.class})
+ public void testSendRequestWithNullRequestId() {
+ try {
+ given(mockConnection.getHeaderField(anyString())).willReturn(null);
+ given(mockConnection.getRequestProperty("content-type")).willReturn("application/json");
+ Map headers = new HashMap<>();
+ headers.put("content-type", "application/json");
+ JsonObject params = new JsonObject();
+ params.addProperty("key", "value");
+ String response = httpUtility.sendRequest("GET", url, params, headers);
+ Assert.assertEquals(expected, response);
+ Assert.assertNull(HttpUtility.getRequestID());
+ } catch (Exception e) {
+ fail(INVALID_EXCEPTION_THROWN);
+ }
+ }
+
+ @Test
+ @PrepareForTest({URL.class, HttpURLConnection.class})
+ public void testSendRequestFormURLEncodedWithSpecialCharacters() {
+ try {
+ given(mockConnection.getRequestProperty("content-type")).willReturn("application/x-www-form-urlencoded");
+ Map headers = new HashMap<>();
+ headers.put("content-type", "application/x-www-form-urlencoded");
+ JsonObject params = new JsonObject();
+ params.addProperty("key", "value with spaces");
+ params.addProperty("special", "test@email.com");
+ String response = httpUtility.sendRequest("POST", url, params, headers);
+ Assert.assertEquals(expected, response);
+ } catch (Exception e) {
+ 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/utils/logger/LogUtilLevelTests.java b/src/test/java/com/skyflow/utils/logger/LogUtilLevelTests.java
new file mode 100644
index 00000000..9754687a
--- /dev/null
+++ b/src/test/java/com/skyflow/utils/logger/LogUtilLevelTests.java
@@ -0,0 +1,99 @@
+package com.skyflow.utils.logger;
+
+import com.skyflow.enums.LogLevel;
+import org.junit.Assert;
+import org.junit.Test;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.logging.Handler;
+import java.util.logging.Level;
+import java.util.logging.LogRecord;
+import java.util.logging.Logger;
+
+public class LogUtilLevelTests {
+
+ private static class CapturingHandler extends Handler {
+ final List records = new ArrayList<>();
+
+ @Override
+ public void publish(LogRecord record) {
+ records.add(record);
+ }
+
+ @Override public void flush() {}
+ @Override public void close() {}
+ }
+
+ // setupLogger calls LogManager.reset() which clears all handlers,
+ // so the capturing handler must be attached after setupLogger runs.
+ private CapturingHandler attachCapture() {
+ CapturingHandler handler = new CapturingHandler();
+ handler.setLevel(Level.ALL);
+ Logger.getLogger(LogUtil.class.getName()).addHandler(handler);
+ return handler;
+ }
+
+ @Test
+ public void testWarnLogAppearsWhenLogLevelIsInfo() {
+ LogUtil.setupLogger(LogLevel.INFO);
+ CapturingHandler handler = attachCapture();
+
+ LogUtil.printWarningLog("deprecation warning");
+
+ boolean warnCaptured = handler.records.stream()
+ .anyMatch(r -> r.getLevel().equals(Level.WARNING)
+ && r.getMessage().contains("deprecation warning"));
+ Assert.assertTrue("WARN log should appear when LogLevel is INFO", warnCaptured);
+ }
+
+ @Test
+ public void testWarnLogAppearsWhenLogLevelIsWarn() {
+ LogUtil.setupLogger(LogLevel.WARN);
+ CapturingHandler handler = attachCapture();
+
+ LogUtil.printWarningLog("warn level warning");
+
+ boolean warnCaptured = handler.records.stream()
+ .anyMatch(r -> r.getLevel().equals(Level.WARNING)
+ && r.getMessage().contains("warn level warning"));
+ Assert.assertTrue("WARN log should appear when LogLevel is WARN", warnCaptured);
+ }
+
+ @Test
+ public void testWarnLogAppearsWhenLogLevelIsDebug() {
+ LogUtil.setupLogger(LogLevel.DEBUG);
+ CapturingHandler handler = attachCapture();
+
+ LogUtil.printWarningLog("debug level warning");
+
+ boolean warnCaptured = handler.records.stream()
+ .anyMatch(r -> r.getLevel().equals(Level.WARNING)
+ && r.getMessage().contains("debug level warning"));
+ Assert.assertTrue("WARN log should appear when LogLevel is DEBUG", warnCaptured);
+ }
+
+ @Test
+ public void testWarnLogSuppressedWhenLogLevelIsError() {
+ LogUtil.setupLogger(LogLevel.ERROR);
+ CapturingHandler handler = attachCapture();
+
+ LogUtil.printWarningLog("suppressed warning");
+
+ boolean warnCaptured = handler.records.stream()
+ .anyMatch(r -> r.getLevel().equals(Level.WARNING));
+ Assert.assertFalse("WARN log should NOT appear when LogLevel is ERROR", warnCaptured);
+ }
+
+ @Test
+ public void testInfoLogSuppressedWhenLogLevelIsWarn() {
+ LogUtil.setupLogger(LogLevel.WARN);
+ CapturingHandler handler = attachCapture();
+
+ LogUtil.printInfoLog("info message");
+
+ boolean infoCaptured = handler.records.stream()
+ .anyMatch(r -> r.getLevel().equals(Level.INFO));
+ Assert.assertFalse("INFO log should NOT appear when LogLevel is WARN", infoCaptured);
+ }
+}
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/connection/InvokeConnectionTests.java b/src/test/java/com/skyflow/vault/connection/InvokeConnectionTests.java
index 63717b5c..6fee259d 100644
--- a/src/test/java/com/skyflow/vault/connection/InvokeConnectionTests.java
+++ b/src/test/java/com/skyflow/vault/connection/InvokeConnectionTests.java
@@ -11,6 +11,7 @@
import org.junit.BeforeClass;
import org.junit.Test;
+import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;
@@ -433,6 +434,22 @@ public void testInvokeConnectionResponse() {
Assert.assertNotNull(connectionResponse.getData());
Assert.assertEquals(responseString, connectionResponse.toString());
Assert.assertEquals(1, connectionResponse.getMetadata().size());
+ Assert.assertNull(connectionResponse.getErrors());
+ } catch (Exception e) {
+ Assert.fail(INVALID_EXCEPTION_THROWN);
+ }
+ }
+
+ @Test
+ public void testInvokeConnectionResponseWithErrors() {
+ try {
+ ArrayList> errors = new ArrayList<>();
+ HashMap err = new HashMap<>();
+ err.put("error", "connection failed");
+ errors.add(err);
+ InvokeConnectionResponse response = new InvokeConnectionResponse(null, null, errors);
+ Assert.assertEquals(1, response.getErrors().size());
+ Assert.assertEquals("connection failed", response.getErrors().get(0).get("error"));
} catch (Exception e) {
Assert.fail(INVALID_EXCEPTION_THROWN);
}
diff --git a/src/test/java/com/skyflow/vault/controller/ConnectionControllerTests.java b/src/test/java/com/skyflow/vault/controller/ConnectionControllerTests.java
index b121280a..6ce0cd4d 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"; // gitleaks:allow
+ 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/DetectControllerFileTests.java b/src/test/java/com/skyflow/vault/controller/DetectControllerFileTests.java
index da8494e1..2b8379bc 100644
--- a/src/test/java/com/skyflow/vault/controller/DetectControllerFileTests.java
+++ b/src/test/java/com/skyflow/vault/controller/DetectControllerFileTests.java
@@ -5,8 +5,12 @@
import com.skyflow.errors.ErrorCode;
import com.skyflow.errors.ErrorMessage;
import com.skyflow.errors.SkyflowException;
+import com.skyflow.generated.rest.types.DeidentifiedFileOutput;
+import com.skyflow.generated.rest.types.DetectRunsResponse;
+import com.skyflow.generated.rest.types.DetectRunsResponseStatus;
import com.skyflow.vault.detect.AudioBleep;
import com.skyflow.vault.detect.DeidentifyFileRequest;
+import com.skyflow.vault.detect.DeidentifyFileResponse;
import com.skyflow.vault.detect.FileInput;
import com.skyflow.vault.detect.GetDetectRunRequest;
import org.junit.Assert;
@@ -14,8 +18,10 @@
import org.junit.Test;
import java.io.File;
+import java.lang.reflect.Method;
import java.nio.file.Files;
import java.util.ArrayList;
+import java.util.Collections;
public class DetectControllerFileTests {
private static final String EXCEPTION_NOT_THROWN = "Should have thrown an exception";
@@ -406,4 +412,54 @@ public void testOutputDirectoryNotWritable() throws Exception {
dir.delete();
}
}
+
+ // C1 regression: the dead statement response.getEntities().get(0) has been removed.
+ // The short-form constructor produces null entities; the guarded block at the call site
+ // handles that safely.
+ @Test
+ public void testDeidentifyFileResponseNullEntitiesDoesNotThrow() {
+ DeidentifyFileResponse response = new DeidentifyFileResponse("run-id", "IN_PROGRESS");
+ Assert.assertNull("Entities must be null from short-form constructor", response.getEntities());
+ }
+
+ // C2: getFirstOutput must return null (not throw NoSuchElementException) when output is absent.
+ @Test
+ public void testGetFirstOutputReturnsNullWhenOutputAbsent() throws Exception {
+ DetectRunsResponse response = DetectRunsResponse.builder().build();
+ Method method = DetectController.class.getDeclaredMethod("getFirstOutput", DetectRunsResponse.class);
+ method.setAccessible(true);
+ DeidentifiedFileOutput result = (DeidentifiedFileOutput) method.invoke(null, response);
+ Assert.assertNull("getFirstOutput must return null when output Optional is absent", result);
+ }
+
+ // C2: getFirstOutput must return null when output list is present but empty.
+ @Test
+ public void testGetFirstOutputReturnsNullWhenOutputListEmpty() throws Exception {
+ DetectRunsResponse response = DetectRunsResponse.builder()
+ .output(Collections.emptyList())
+ .build();
+ Method method = DetectController.class.getDeclaredMethod("getFirstOutput", DetectRunsResponse.class);
+ method.setAccessible(true);
+ DeidentifiedFileOutput result = (DeidentifiedFileOutput) method.invoke(null, response);
+ Assert.assertNull("getFirstOutput must return null when output list is empty", result);
+ }
+
+ // C3: parseDeidentifyFileResponse must not throw when processedFileExtension is absent;
+ // it should fall back to the UNKNOWN sentinel value.
+ @Test
+ public void testParseDeidentifyFileResponseFallsBackWhenExtensionAbsent() throws Exception {
+ DetectRunsResponse response = DetectRunsResponse.builder()
+ .status(DetectRunsResponseStatus.SUCCESS)
+ .output(Collections.singletonList(DeidentifiedFileOutput.builder().build()))
+ .build();
+
+ Method method = DetectController.class.getDeclaredMethod(
+ "parseDeidentifyFileResponse", DetectRunsResponse.class, String.class, String.class);
+ method.setAccessible(true);
+ DeidentifyFileResponse result = (DeidentifyFileResponse) method.invoke(null, response, "run-id", "SUCCESS");
+
+ Assert.assertNotNull("Response must not be null", result);
+ Assert.assertEquals("Extension must fall back to UNKNOWN when absent",
+ DetectRunsResponseStatus.UNKNOWN.toString(), result.getExtension());
+ }
}
\ No newline at end of file
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 4115bc2c..4c1ecb2b 100644
--- a/src/test/java/com/skyflow/vault/controller/VaultControllerTests.java
+++ b/src/test/java/com/skyflow/vault/controller/VaultControllerTests.java
@@ -1,23 +1,78 @@
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";
@@ -26,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 {
@@ -42,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 {
@@ -185,4 +267,1085 @@ public void testInvalidRequestInFileUploadMethod() {
}
}
+ // --- getFormattedGetRecord / getFormattedQueryRecord tests ---
+
+ @Test
+ public void testGetFormattedGetRecordNormalisesSkyflowId() throws Exception {
+ Map fields = new HashMap<>();
+ fields.put("skyflow_id", "abc-123");
+ fields.put("name", "John");
+ V1FieldRecords record = V1FieldRecords.builder().fields(fields).build();
+
+ Method method = VaultController.class.getDeclaredMethod("getFormattedGetRecord", V1FieldRecords.class);
+ method.setAccessible(true);
+ @SuppressWarnings("unchecked")
+ HashMap result = (HashMap) method.invoke(null, record);
+
+ Assert.assertEquals("skyflowId should be present (new form)", "abc-123", result.get("skyflowId"));
+ Assert.assertEquals("skyflow_id should still be present (v2 deprecated form)", "abc-123", result.get("skyflow_id"));
+ Assert.assertEquals("other fields should be preserved", "John", result.get("name"));
+ }
+
+ @Test
+ public void testGetFormattedQueryRecordNormalisesSkyflowId() throws Exception {
+ Map fields = new HashMap<>();
+ fields.put("skyflow_id", "xyz-456");
+ fields.put("email", "test@example.com");
+ V1FieldRecords record = V1FieldRecords.builder().fields(fields).build();
+
+ Method method = VaultController.class.getDeclaredMethod("getFormattedQueryRecord", V1FieldRecords.class);
+ method.setAccessible(true);
+ @SuppressWarnings("unchecked")
+ HashMap result = (HashMap) method.invoke(null, record);
+
+ Assert.assertEquals("skyflowId should be present (new form)", "xyz-456", result.get("skyflowId"));
+ Assert.assertEquals("skyflow_id should still be present (v2 deprecated form)", "xyz-456", result.get("skyflow_id"));
+ Assert.assertEquals("other fields should be preserved", "test@example.com", result.get("email"));
+ }
+
+ @Test
+ public void testGetFormattedGetRecordNormalisesSkyflowIdInTokensBranch() throws Exception {
+ Map tokens = new HashMap<>();
+ tokens.put("skyflow_id", "tok-789");
+ tokens.put("card_number", "tok-card-abc");
+ V1FieldRecords record = V1FieldRecords.builder().tokens(tokens).build();
+
+ Method method = VaultController.class.getDeclaredMethod("getFormattedGetRecord", V1FieldRecords.class);
+ method.setAccessible(true);
+ @SuppressWarnings("unchecked")
+ HashMap result = (HashMap) method.invoke(null, record);
+
+ Assert.assertEquals("skyflowId should be present (new form)", "tok-789", result.get("skyflowId"));
+ Assert.assertEquals("skyflow_id should still be present (v2 deprecated form)", "tok-789", result.get("skyflow_id"));
+ 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()
+ .table("test_table")
+ .downloadUrl(true)
+ .build();
+ Assert.assertTrue("new downloadUrl(true) should be set", request.getDownloadUrl());
+ }
+
+ @Test
+ public void testGetRequestDownloadURLDeprecatedFormStillWorks() {
+ GetRequest request = GetRequest.builder()
+ .table("test_table")
+ .downloadURL(true)
+ .build();
+ Assert.assertTrue("deprecated downloadURL() should still work", request.getDownloadURL());
+ Assert.assertTrue("new getDownloadUrl() returns same value", request.getDownloadUrl());
+ }
+
+ @Test
+ public void testGetRequestDownloadUrlDefaultIsTrue() {
+ GetRequest request = GetRequest.builder()
+ .table("test_table")
+ .build();
+ Assert.assertTrue("downloadUrl should be true by default (preserved from original)", request.getDownloadUrl());
+ }
+
+ @Test
+ public void testDetokenizeRequestDownloadUrlNewForm() {
+ DetokenizeRequest request = DetokenizeRequest.builder()
+ .downloadUrl(true)
+ .build();
+ Assert.assertTrue("new downloadUrl(true) should be set", request.getDownloadUrl());
+ }
+
+ @Test
+ public void testDetokenizeRequestDownloadURLDeprecatedFormStillWorks() {
+ DetokenizeRequest request = DetokenizeRequest.builder()
+ .downloadURL(true)
+ .build();
+ Assert.assertTrue("deprecated downloadURL() should still work", request.getDownloadURL());
+ Assert.assertTrue("new getDownloadUrl() returns same value", request.getDownloadUrl());
+ }
+
+ @Test
+ public void testDetokenizeRequestDownloadUrlDefaultIsFalse() {
+ DetokenizeRequest request = DetokenizeRequest.builder().build();
+ Assert.assertFalse("downloadUrl should be false by default", request.getDownloadUrl());
+ }
+
+ // --- extractUpdateSkyflowId tests ---
+
+ @Test
+ public void testExtractUpdateSkyflowId_onlyCamelCase() throws Exception {
+ HashMap data = new HashMap<>();
+ data.put("skyflowId", "id-camel-only");
+ data.put("card_number", "4111111111111111");
+
+ Method method = VaultController.class.getDeclaredMethod("extractUpdateSkyflowId", HashMap.class);
+ method.setAccessible(true);
+ String result = (String) method.invoke(null, data);
+
+ Assert.assertEquals("should return the skyflowId value", "id-camel-only", result);
+ Assert.assertFalse("skyflowId should be removed from data map", data.containsKey("skyflowId"));
+ Assert.assertTrue("other fields should be preserved", data.containsKey("card_number"));
+ }
+
+ @Test
+ public void testExtractUpdateSkyflowId_onlySnakeCase() throws Exception {
+ HashMap data = new HashMap<>();
+ data.put("skyflow_id", "id-snake-only");
+ data.put("card_number", "4111111111111111");
+
+ Method method = VaultController.class.getDeclaredMethod("extractUpdateSkyflowId", HashMap.class);
+ method.setAccessible(true);
+ String result = (String) method.invoke(null, data);
+
+ Assert.assertEquals("should return the skyflow_id value", "id-snake-only", result);
+ Assert.assertFalse("skyflow_id should be removed from data map", data.containsKey("skyflow_id"));
+ Assert.assertTrue("other fields should be preserved", data.containsKey("card_number"));
+ }
+
+ @Test
+ public void testExtractUpdateSkyflowId_bothKeys_prefersSkyflowId() throws Exception {
+ HashMap data = new HashMap<>();
+ data.put("skyflowId", "id-camel");
+ data.put("skyflow_id", "id-snake");
+ data.put("card_number", "4111111111111111");
+
+ Method method = VaultController.class.getDeclaredMethod("extractUpdateSkyflowId", HashMap.class);
+ method.setAccessible(true);
+ String result = (String) method.invoke(null, data);
+
+ Assert.assertEquals("skyflowId should be preferred when both keys are present", "id-camel", result);
+ }
+
+ @Test
+ public void testExtractUpdateSkyflowId_bothKeys_removesBothFromMap() throws Exception {
+ HashMap data = new HashMap<>();
+ data.put("skyflowId", "id-camel");
+ data.put("skyflow_id", "id-snake");
+ data.put("card_number", "4111111111111111");
+
+ Method method = VaultController.class.getDeclaredMethod("extractUpdateSkyflowId", HashMap.class);
+ method.setAccessible(true);
+ method.invoke(null, data);
+
+ Assert.assertFalse("skyflowId should be removed from data map", data.containsKey("skyflowId"));
+ Assert.assertFalse("skyflow_id should be removed from data map", data.containsKey("skyflow_id"));
+ 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