diff --git a/common/src/main/java/com/skyflow/errors/ErrorMessage.java b/common/src/main/java/com/skyflow/errors/ErrorMessage.java index 1320ca6e..79a1b32e 100644 --- a/common/src/main/java/com/skyflow/errors/ErrorMessage.java +++ b/common/src/main/java/com/skyflow/errors/ErrorMessage.java @@ -186,6 +186,8 @@ public enum ErrorMessage { NullTokenGroupNameInTokenGroup("%s0 Validation error. TokenGroupName in TokenGroupRedactions is null or empty. Specify a valid tokenGroupName."), InvalidRecord("%s0 Validation error. InsertRecord object in the list is invalid. Specify a valid InsertRecord object."), + NullCustomHeaderKey("%s0 Validation error. Custom header key must not be null. Specify a valid custom header key."), + EmptyValueInCustomHeaders("%s0 Validation error. Custom header value must not be null or empty. Specify a valid value."), ; private final String message; diff --git a/common/src/main/java/com/skyflow/logs/ErrorLogs.java b/common/src/main/java/com/skyflow/logs/ErrorLogs.java index 404c6e68..360401a1 100644 --- a/common/src/main/java/com/skyflow/logs/ErrorLogs.java +++ b/common/src/main/java/com/skyflow/logs/ErrorLogs.java @@ -162,7 +162,9 @@ public enum ErrorLogs { TABLE_SPECIFIED_AT_BOTH_PLACE("Invalid %s1 request. Table name cannot be specified at both the request and record levels. Please specify the table name at only one place."), TABLE_NOT_SPECIFIED_AT_BOTH_PLACE("Invalid %s1 request. Table name is missing. Table name should be specified at one place either at the request level or record level. Please specify the table name at one place."), UPSERT_TABLE_REQUEST_AT_RECORD_LEVEL("Invalid %s1 request. Table name should be present at each record level when upsert is present at record level."), - UPSERT_TABLE_REQUEST_AT_REQUEST_LEVEL("Invalid %s1 request. Upsert should be present at each record level when table name is present at record level."); + UPSERT_TABLE_REQUEST_AT_REQUEST_LEVEL("Invalid %s1 request. Upsert should be present at each record level when table name is present at record level."), + NULL_CUSTOM_HEADER_KEY("Invalid %s1 request. Custom header key can not be null."), + EMPTY_OR_NULL_VALUE_IN_CUSTOM_HEADERS("Invalid %s1 request. Custom header value can not be null or empty for key \"%s2\"."); private final String log; ErrorLogs(String log) { diff --git a/v3/src/main/java/com/skyflow/enums/CustomHeaderKey.java b/v3/src/main/java/com/skyflow/enums/CustomHeaderKey.java new file mode 100644 index 00000000..8e6190c6 --- /dev/null +++ b/v3/src/main/java/com/skyflow/enums/CustomHeaderKey.java @@ -0,0 +1,18 @@ +package com.skyflow.enums; + +public enum CustomHeaderKey { + SkyflowAccountID("x-skyflow-account-id"), + SkyflowAccountName("x-skyflow-account-name"), + RequestIDHeader("x-request-id"); + + private final String value; + + CustomHeaderKey(String value) { + this.value = value; + } + + @Override + public String toString() { + return this.value; + } +} \ No newline at end of file diff --git a/v3/src/main/java/com/skyflow/utils/Utils.java b/v3/src/main/java/com/skyflow/utils/Utils.java index 131772c7..cdd0b03b 100644 --- a/v3/src/main/java/com/skyflow/utils/Utils.java +++ b/v3/src/main/java/com/skyflow/utils/Utils.java @@ -77,6 +77,10 @@ public static List createDetokenizeBatches(V1FlowDetoke } public static ErrorRecord createErrorRecord(Map recordMap, int indexNumber) { + return createErrorRecord(recordMap, indexNumber, null); + } + + public static ErrorRecord createErrorRecord(Map recordMap, int indexNumber, String requestId) { ErrorRecord err = null; if (recordMap != null) { int code = 500; @@ -84,7 +88,6 @@ public static ErrorRecord createErrorRecord(Map recordMap, int i code = (Integer) recordMap.get("http_code"); } else if (recordMap.containsKey("httpCode")) { code = (Integer) recordMap.get("httpCode"); - } else { if (recordMap.containsKey("statusCode")) { code = (Integer) recordMap.get("statusCode"); @@ -92,11 +95,17 @@ public static ErrorRecord createErrorRecord(Map recordMap, int i } String message = recordMap.containsKey("error") ? (String) recordMap.get("error") : recordMap.containsKey("message") ? (String) recordMap.get("message") : "Unknown error"; - err = new ErrorRecord(indexNumber, message, code); + err = new ErrorRecord(indexNumber, message, code, requestId); } return err; } + private static String extractRequestId(Map> headers) { + if (headers == null) return null; + List ids = headers.get(BaseConstants.REQUEST_ID_HEADER_KEY); + return (ids == null || ids.isEmpty()) ? null : ids.get(0); + } + public static List handleBatchException( Throwable ex, List batch, int batchNumber, int batchSize ) { @@ -104,7 +113,9 @@ public static List handleBatchException( Throwable cause = ex.getCause(); if (cause instanceof ApiClientApiException) { ApiClientApiException apiException = (ApiClientApiException) cause; - Map responseBody = (Map) apiException.body(); + String requestId = extractRequestId(apiException.headers()); + Object rawBody = apiException.body(); + Map responseBody = (rawBody instanceof Map) ? (Map) rawBody : null; int indexNumber = batchNumber > 0 ? batchNumber * batchSize : 0; if (responseBody != null) { if (responseBody.containsKey("records")) { @@ -114,21 +125,31 @@ public static List handleBatchException( for (Object record : recordsList) { if (record instanceof Map) { Map recordMap = (Map) record; - ErrorRecord err = Utils.createErrorRecord(recordMap, indexNumber); + ErrorRecord err = Utils.createErrorRecord(recordMap, indexNumber, requestId); errorRecords.add(err); indexNumber++; } } } } else if (responseBody.containsKey("error")) { - Map recordMap = (Map) responseBody.get("error"); + Object errField = responseBody.get("error"); + Map recordMap = (errField instanceof Map) ? (Map) errField : null; + String fallbackMsg = (errField instanceof String) ? (String) errField : null; for (int j = 0; j < batch.size(); j++) { - ErrorRecord err = Utils.createErrorRecord(recordMap, indexNumber); + ErrorRecord err = (recordMap != null) + ? Utils.createErrorRecord(recordMap, indexNumber, requestId) + : new ErrorRecord(indexNumber, fallbackMsg != null ? fallbackMsg : apiException.getMessage(), apiException.statusCode(), requestId); errorRecords.add(err); indexNumber++; } } } + if (errorRecords.isEmpty()) { + for (int j = 0; j < batch.size(); j++) { + errorRecords.add(new ErrorRecord(indexNumber, apiException.getMessage(), apiException.statusCode(), requestId)); + indexNumber++; + } + } } else { int indexNumber = batchNumber > 0 ? batchNumber * batchSize : 0; for (int j = 0; j < batch.size(); j++) { @@ -147,7 +168,9 @@ public static List handleDetokenizeBatchException( Throwable cause = ex.getCause(); if (cause instanceof ApiClientApiException) { ApiClientApiException apiException = (ApiClientApiException) cause; - Map responseBody = (Map) apiException.body(); + String requestId = extractRequestId(apiException.headers()); + Object rawBody = apiException.body(); + Map responseBody = (rawBody instanceof Map) ? (Map) rawBody : null; int indexNumber = batchNumber * batchSize; if (responseBody != null) { if (responseBody.containsKey("response")) { @@ -157,21 +180,33 @@ public static List handleDetokenizeBatchException( for (Object record : recordsList) { if (record instanceof Map) { Map recordMap = (Map) record; - ErrorRecord err = Utils.createErrorRecord(recordMap, indexNumber); + ErrorRecord err = Utils.createErrorRecord(recordMap, indexNumber, requestId); errorRecords.add(err); indexNumber++; } } } } else if (responseBody.containsKey("error")) { - Map recordMap = (Map) responseBody.get("error"); - for (int j = 0; j < batch.getTokens().get().size(); j++) { - ErrorRecord err = Utils.createErrorRecord(recordMap, indexNumber); + Object errField = responseBody.get("error"); + Map recordMap = (errField instanceof Map) ? (Map) errField : null; + String fallbackMsg = (errField instanceof String) ? (String) errField : null; + int tokenCount = batch.getTokens().isPresent() ? batch.getTokens().get().size() : 0; + for (int j = 0; j < tokenCount; j++) { + ErrorRecord err = (recordMap != null) + ? Utils.createErrorRecord(recordMap, indexNumber, requestId) + : new ErrorRecord(indexNumber, fallbackMsg != null ? fallbackMsg : apiException.getMessage(), apiException.statusCode(), requestId); errorRecords.add(err); indexNumber++; } } } + if (errorRecords.isEmpty()) { + int tokenCount = batch.getTokens().isPresent() ? batch.getTokens().get().size() : 0; + for (int j = 0; j < tokenCount; j++) { + errorRecords.add(new ErrorRecord(indexNumber, apiException.getMessage(), apiException.statusCode(), requestId)); + indexNumber++; + } + } } else { int indexNumber = batchNumber * batchSize; for (int j = 0; j < batch.getTokens().get().size(); j++) { @@ -184,15 +219,20 @@ public static List handleDetokenizeBatchException( } public static DetokenizeResponse formatDetokenizeResponse(com.skyflow.generated.rest.types.V1FlowDetokenizeResponse response, int batch, int batchSize) { - if (response != null) { + return formatDetokenizeResponse(response, batch, batchSize, null); + } + + public static DetokenizeResponse formatDetokenizeResponse(com.skyflow.generated.rest.types.V1FlowDetokenizeResponse response, int batch, int batchSize, Map> headers) { + if (response != null && response.getResponse().isPresent()) { + String requestId = extractRequestId(headers); List record = response.getResponse().get(); List errorRecords = new ArrayList<>(); List successRecords = new ArrayList<>(); int indexNumber = batch * batchSize; - int recordsSize = response.getResponse().get().size(); + int recordsSize = record.size(); for (int index = 0; index < recordsSize; index++) { if (record.get(index).getError().isPresent()) { - ErrorRecord errorRecord = new ErrorRecord(indexNumber, record.get(index).getError().get(), record.get(index).getHttpCode().get()); + ErrorRecord errorRecord = new ErrorRecord(indexNumber, record.get(index).getError().get(), record.get(index).getHttpCode().get(), requestId); errorRecords.add(errorRecord); } else { com.skyflow.vault.data.DetokenizeResponseObject success = new com.skyflow.vault.data.DetokenizeResponseObject(indexNumber, record.get(index).getToken().orElse(null), record.get(index).getValue().orElse(null), record.get(index).getTokenGroupName().orElse(null), record.get(index).getError().orElse(null), record.get(index).getMetadata().orElse(null)); @@ -200,23 +240,27 @@ public static DetokenizeResponse formatDetokenizeResponse(com.skyflow.generated. } indexNumber++; } - DetokenizeResponse formattedResponse = new DetokenizeResponse(successRecords, errorRecords); - return formattedResponse; + return new DetokenizeResponse(successRecords, errorRecords); } return null; } public static com.skyflow.vault.data.InsertResponse formatResponse(V1InsertResponse response, int batch, int batchSize) { + return formatResponse(response, batch, batchSize, null); + } + + public static com.skyflow.vault.data.InsertResponse formatResponse(V1InsertResponse response, int batch, int batchSize, Map> headers) { com.skyflow.vault.data.InsertResponse formattedResponse = null; List successRecords = new ArrayList<>(); List errorRecords = new ArrayList<>(); if (response != null) { + String requestId = extractRequestId(headers); List record = response.getRecords().get(); int indexNumber = batch * batchSize; int recordsSize = response.getRecords().get().size(); for (int index = 0; index < recordsSize; index++) { if (record.get(index).getError().isPresent()) { - ErrorRecord errorRecord = new ErrorRecord(indexNumber, record.get(index).getError().get(), record.get(index).getHttpCode().get()); + ErrorRecord errorRecord = new ErrorRecord(indexNumber, record.get(index).getError().get(), record.get(index).getHttpCode().get(), requestId); errorRecords.add(errorRecord); } else { Map> tokensMap = null; @@ -312,7 +356,9 @@ public static List handleDeleteTokensBatchException( Throwable cause = ex.getCause(); if (cause instanceof ApiClientApiException) { ApiClientApiException apiException = (ApiClientApiException) cause; - Map responseBody = (Map) apiException.body(); + String requestId = extractRequestId(apiException.headers()); + Object rawBody = apiException.body(); + Map responseBody = (rawBody instanceof Map) ? (Map) rawBody : null; int indexNumber = batchNumber * batchSize; if (responseBody != null) { if (responseBody.containsKey("tokens")) { @@ -322,21 +368,33 @@ public static List handleDeleteTokensBatchException( for (Object record : recordsList) { if (record instanceof Map) { Map recordMap = (Map) record; - ErrorRecord err = Utils.createErrorRecord(recordMap, indexNumber); + ErrorRecord err = Utils.createErrorRecord(recordMap, indexNumber, requestId); errorRecords.add(err); indexNumber++; } } } } else if (responseBody.containsKey("error")) { - Map recordMap = (Map) responseBody.get("error"); - for (int j = 0; j < batch.getTokens().get().size(); j++) { - ErrorRecord err = Utils.createErrorRecord(recordMap, indexNumber); + Object errField = responseBody.get("error"); + Map recordMap = (errField instanceof Map) ? (Map) errField : null; + String fallbackMsg = (errField instanceof String) ? (String) errField : null; + int tokenCount = batch.getTokens().isPresent() ? batch.getTokens().get().size() : 0; + for (int j = 0; j < tokenCount; j++) { + ErrorRecord err = (recordMap != null) + ? Utils.createErrorRecord(recordMap, indexNumber, requestId) + : new ErrorRecord(indexNumber, fallbackMsg != null ? fallbackMsg : apiException.getMessage(), apiException.statusCode(), requestId); errorRecords.add(err); indexNumber++; } } } + if (errorRecords.isEmpty()) { + int tokenCount = batch.getTokens().isPresent() ? batch.getTokens().get().size() : 0; + for (int j = 0; j < tokenCount; j++) { + errorRecords.add(new ErrorRecord(indexNumber, apiException.getMessage(), apiException.statusCode(), requestId)); + indexNumber++; + } + } } else { int indexNumber = batchNumber * batchSize; for (int j = 0; j < batch.getTokens().get().size(); j++) { @@ -350,20 +408,25 @@ public static List handleDeleteTokensBatchException( public static DeleteTokensResponse formatDeleteTokensResponse( com.skyflow.generated.rest.types.V1FlowDeleteTokenResponse response, int batch, int batchSize) { + return formatDeleteTokensResponse(response, batch, batchSize, null); + } + + public static DeleteTokensResponse formatDeleteTokensResponse( + com.skyflow.generated.rest.types.V1FlowDeleteTokenResponse response, int batch, int batchSize, Map> headers) { if (response != null && response.getTokens().isPresent()) { + String requestId = extractRequestId(headers); List records = response.getTokens().get(); List errorRecords = new ArrayList<>(); List successRecords = new ArrayList<>(); int indexNumber = batch * batchSize; for (com.skyflow.generated.rest.types.V1DeleteTokenResponseObject record : records) { - // The API returns the token string in "value" field regardless of success or error String tokenValue = record.getValue().orElse(null); if (record.getError().isPresent() && record.getError().get() != null && !record.getError().get().isEmpty() && record.getHttpCode().orElse(200) != 200) { ErrorRecord errorRecord = new ErrorRecord(indexNumber, record.getError().get(), - record.getHttpCode().orElse(500)); + record.getHttpCode().orElse(500), requestId); errorRecords.add(errorRecord); } else { DeleteTokensSuccess success = new DeleteTokensSuccess(indexNumber, tokenValue); @@ -402,7 +465,9 @@ public static List handleTokenizeBatchException( Throwable cause = ex.getCause(); if (cause instanceof ApiClientApiException) { ApiClientApiException apiException = (ApiClientApiException) cause; - Map responseBody = (Map) apiException.body(); + String requestId = extractRequestId(apiException.headers()); + Object rawBody = apiException.body(); + Map responseBody = (rawBody instanceof Map) ? (Map) rawBody : null; int indexNumber = batchNumber * batchSize; if (responseBody != null) { if (responseBody.containsKey("response")) { @@ -412,22 +477,33 @@ public static List handleTokenizeBatchException( for (Object record : recordsList) { if (record instanceof Map) { Map recordMap = (Map) record; - ErrorRecord err = Utils.createErrorRecord(recordMap, indexNumber); + ErrorRecord err = Utils.createErrorRecord(recordMap, indexNumber, requestId); errorRecords.add(err); indexNumber++; } } } } else if (responseBody.containsKey("error")) { - Map recordMap = (Map) responseBody.get("error"); + Object errField = responseBody.get("error"); + Map recordMap = (errField instanceof Map) ? (Map) errField : null; + String fallbackMsg = (errField instanceof String) ? (String) errField : null; int batchDataSize = batch.getData().isPresent() ? batch.getData().get().size() : 0; for (int j = 0; j < batchDataSize; j++) { - ErrorRecord err = Utils.createErrorRecord(recordMap, indexNumber); + ErrorRecord err = (recordMap != null) + ? Utils.createErrorRecord(recordMap, indexNumber, requestId) + : new ErrorRecord(indexNumber, fallbackMsg != null ? fallbackMsg : apiException.getMessage(), apiException.statusCode(), requestId); errorRecords.add(err); indexNumber++; } } } + if (errorRecords.isEmpty()) { + int batchDataSize = batch.getData().isPresent() ? batch.getData().get().size() : 0; + for (int j = 0; j < batchDataSize; j++) { + errorRecords.add(new ErrorRecord(indexNumber, apiException.getMessage(), apiException.statusCode(), requestId)); + indexNumber++; + } + } } else { int indexNumber = batchNumber * batchSize; int batchDataSize = batch.getData().isPresent() ? batch.getData().get().size() : 0; @@ -444,7 +520,15 @@ public static TokenizeResponse formatTokenizeResponse( com.skyflow.generated.rest.types.V1FlowTokenizeResponse response, com.skyflow.generated.rest.resources.flowservice.requests.V1FlowTokenizeRequest batchRequest, int batchNumber, int batchSize) { + return formatTokenizeResponse(response, batchRequest, batchNumber, batchSize, null); + } + + public static TokenizeResponse formatTokenizeResponse( + com.skyflow.generated.rest.types.V1FlowTokenizeResponse response, + com.skyflow.generated.rest.resources.flowservice.requests.V1FlowTokenizeRequest batchRequest, + int batchNumber, int batchSize, Map> headers) { if (response != null && response.getResponse().isPresent()) { + String requestId = extractRequestId(headers); List flatList = response.getResponse().get(); List requestData = @@ -475,7 +559,7 @@ public static TokenizeResponse formatTokenizeResponse( ? ((Number) props.get("httpCode")).intValue() : 200; if (errorMsg != null) { - errorRecords.add(new ErrorRecord(inputRecordIndex, errorMsg, httpCode)); + errorRecords.add(new ErrorRecord(inputRecordIndex, errorMsg, httpCode, requestId)); } else { if (successEntry == null) { successEntry = new TokenizeSuccess(inputRecordIndex, value); diff --git a/v3/src/main/java/com/skyflow/utils/validations/Validations.java b/v3/src/main/java/com/skyflow/utils/validations/Validations.java index 485b60d5..0cb8ef5b 100644 --- a/v3/src/main/java/com/skyflow/utils/validations/Validations.java +++ b/v3/src/main/java/com/skyflow/utils/validations/Validations.java @@ -2,9 +2,11 @@ import java.util.ArrayList; import java.util.List; +import java.util.Map; import com.skyflow.config.Credentials; import com.skyflow.config.VaultConfig; +import com.skyflow.enums.CustomHeaderKey; import com.skyflow.enums.InterfaceName; import com.skyflow.errors.ErrorCode; import com.skyflow.errors.ErrorMessage; @@ -140,15 +142,15 @@ public static void validateDetokenizeRequest(DetokenizeRequest request) throws S throw new SkyflowException(ErrorCode.INVALID_INPUT.getCode(), ErrorMessage.DetokenizeRequestNull.getMessage()); } List tokens = request.getTokens(); - if (tokens.size() > 10000) { - LogUtil.printErrorLog(ErrorLogs.TOKENS_SIZE_EXCEED.getLog()); - throw new SkyflowException(ErrorCode.INVALID_INPUT.getCode(), ErrorMessage.TokensSizeExceedError.getMessage()); - } if (tokens == null || tokens.isEmpty()) { LogUtil.printErrorLog(Utils.parameterizedString( ErrorLogs.EMPTY_DETOKENIZE_DATA.getLog(), InterfaceName.DETOKENIZE.getName() - )); - throw new SkyflowException(ErrorCode.INVALID_INPUT.getCode(), ErrorMessage.EmptyDetokenizeData.getMessage()); + )); + throw new SkyflowException(ErrorCode.INVALID_INPUT.getCode(), ErrorMessage.EmptyDetokenizeData.getMessage()); + } + if (tokens.size() > 10000) { + LogUtil.printErrorLog(ErrorLogs.TOKENS_SIZE_EXCEED.getLog()); + throw new SkyflowException(ErrorCode.INVALID_INPUT.getCode(), ErrorMessage.TokensSizeExceedError.getMessage()); } for (int index = 0; index < tokens.size(); index++) { String token = tokens.get(index); @@ -264,6 +266,25 @@ public static void validateTokenizeRequest(TokenizeRequest request) throws Skyfl } } + public static void validateCustomHeaders(Map customHeaders, InterfaceName interfaceName) throws SkyflowException { + if (customHeaders == null || customHeaders.isEmpty()) return; + for (Map.Entry entry : customHeaders.entrySet()) { + if (entry.getKey() == null) { + LogUtil.printErrorLog(Utils.parameterizedString( + ErrorLogs.NULL_CUSTOM_HEADER_KEY.getLog(), interfaceName.getName() + )); + throw new SkyflowException(ErrorCode.INVALID_INPUT.getCode(), ErrorMessage.NullCustomHeaderKey.getMessage()); + } + String value = entry.getValue(); + if (value == null || value.trim().isEmpty()) { + LogUtil.printErrorLog(Utils.parameterizedString( + ErrorLogs.EMPTY_OR_NULL_VALUE_IN_CUSTOM_HEADERS.getLog(), interfaceName.getName(), entry.getKey().toString() + )); + throw new SkyflowException(ErrorCode.INVALID_INPUT.getCode(), ErrorMessage.EmptyValueInCustomHeaders.getMessage()); + } + } + } + public static void validateVaultConfiguration(VaultConfig vaultConfig) throws SkyflowException { String vaultId = vaultConfig.getVaultId(); String clusterId = vaultConfig.getClusterId(); diff --git a/v3/src/main/java/com/skyflow/vault/controller/VaultController.java b/v3/src/main/java/com/skyflow/vault/controller/VaultController.java index 74e6642b..097b8fcd 100644 --- a/v3/src/main/java/com/skyflow/vault/controller/VaultController.java +++ b/v3/src/main/java/com/skyflow/vault/controller/VaultController.java @@ -1,13 +1,25 @@ package com.skyflow.vault.controller; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + import com.google.gson.Gson; import com.google.gson.GsonBuilder; import com.google.gson.JsonObject; import com.skyflow.VaultClient; import com.skyflow.config.Credentials; import com.skyflow.config.VaultConfig; +import com.skyflow.enums.InterfaceName; import com.skyflow.errors.SkyflowException; import com.skyflow.generated.rest.core.ApiClientApiException; +import com.skyflow.generated.rest.core.ApiClientHttpResponse; import com.skyflow.generated.rest.core.RequestOptions; import com.skyflow.generated.rest.types.V1InsertRecordData; import com.skyflow.generated.rest.types.V1InsertResponse; @@ -19,21 +31,27 @@ import com.skyflow.utils.Utils; import com.skyflow.utils.logger.LogUtil; import com.skyflow.utils.validations.Validations; -import com.skyflow.vault.data.*; +import com.skyflow.vault.data.DeleteTokensOptions; +import com.skyflow.vault.data.DeleteTokensRequest; +import com.skyflow.vault.data.DeleteTokensResponse; +import com.skyflow.vault.data.DeleteTokensSuccess; +import com.skyflow.vault.data.DetokenizeOptions; +import com.skyflow.vault.data.DetokenizeRequest; +import com.skyflow.vault.data.DetokenizeResponse; +import com.skyflow.vault.data.DetokenizeResponseObject; +import com.skyflow.vault.data.ErrorRecord; +import com.skyflow.vault.data.InsertOptions; +import com.skyflow.vault.data.InsertRecord; +import com.skyflow.vault.data.InsertRequest; +import com.skyflow.vault.data.Success; +import com.skyflow.vault.data.TokenizeOptions; import com.skyflow.vault.data.TokenizeRequest; import com.skyflow.vault.data.TokenizeResponse; import com.skyflow.vault.data.TokenizeSuccess; + import io.github.cdimascio.dotenv.Dotenv; import io.github.cdimascio.dotenv.DotenvException; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; - public final class VaultController extends VaultClient { private static final Gson gson = new GsonBuilder().serializeNulls().create(); private JsonObject metrics = Utils.getMetrics(); @@ -58,45 +76,60 @@ public VaultController(VaultConfig vaultConfig, Credentials credentials) throws this.tokenizeConcurrencyLimit = Constants.TOKENIZE_CONCURRENCY_LIMIT; } + // ── Insert ──────────────────────────────────────────────────────────────── + public com.skyflow.vault.data.InsertResponse bulkInsert(InsertRequest insertRequest) throws SkyflowException { - com.skyflow.vault.data.InsertResponse response; + return bulkInsert(insertRequest, null); + } + + public com.skyflow.vault.data.InsertResponse bulkInsert(InsertRequest insertRequest, InsertOptions options) throws SkyflowException { LogUtil.printInfoLog(InfoLogs.INSERT_TRIGGERED.getLog()); try { LogUtil.printInfoLog(InfoLogs.VALIDATE_INSERT_REQUEST.getLog()); Validations.validateInsertRequest(insertRequest); + if (options != null) Validations.validateCustomHeaders(options.getCustomHeaders(), InterfaceName.INSERT); configureInsertConcurrencyAndBatchSize(insertRequest.getRecords().size()); setBearerToken(); com.skyflow.generated.rest.resources.flowservice.requests.V1InsertRequest request = super.getBulkInsertRequestBody(insertRequest, super.getVaultConfig()); - - response = this.processSync(request, insertRequest.getRecords()); - return response; + Map customHeaders = extractCustomHeaders(options); + return this.processSync(request, insertRequest.getRecords(), customHeaders); } catch (ApiClientApiException e) { String bodyString = gson.toJson(e.body()); LogUtil.printErrorLog(ErrorLogs.INSERT_RECORDS_REJECTED.getLog()); throw new SkyflowException(e.statusCode(), e, e.headers(), bodyString); - } catch (ExecutionException | InterruptedException e) { + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); LogUtil.printErrorLog(ErrorLogs.INSERT_RECORDS_REJECTED.getLog()); throw new SkyflowException(e.getMessage()); + } catch (ExecutionException e) { + LogUtil.printErrorLog(ErrorLogs.INSERT_RECORDS_REJECTED.getLog()); + Throwable cause = e.getCause(); + throw new SkyflowException(cause != null && cause.getMessage() != null ? cause.getMessage() : e.getMessage()); } } public CompletableFuture bulkInsertAsync(InsertRequest insertRequest) throws SkyflowException { + return bulkInsertAsync(insertRequest, null); + } + + public CompletableFuture bulkInsertAsync(InsertRequest insertRequest, InsertOptions options) throws SkyflowException { LogUtil.printInfoLog(InfoLogs.INSERT_TRIGGERED.getLog()); try { LogUtil.printInfoLog(InfoLogs.VALIDATE_INSERT_REQUEST.getLog()); Validations.validateInsertRequest(insertRequest); + if (options != null) Validations.validateCustomHeaders(options.getCustomHeaders(), InterfaceName.INSERT); configureInsertConcurrencyAndBatchSize(insertRequest.getRecords().size()); setBearerToken(); com.skyflow.generated.rest.resources.flowservice.requests.V1InsertRequest request = super.getBulkInsertRequestBody(insertRequest, super.getVaultConfig()); + Map customHeaders = extractCustomHeaders(options); List errorRecords = Collections.synchronizedList(new ArrayList<>()); - List> futures = this.insertBatchFutures(request, errorRecords); + List> futures = this.insertBatchFutures(request, errorRecords, customHeaders); return CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])) .thenApply(v -> { List successRecords = new ArrayList<>(); -// List errorRecords = new ArrayList<>(); for (CompletableFuture future : futures) { com.skyflow.vault.data.InsertResponse futureResponse = future.join(); @@ -119,18 +152,23 @@ public CompletableFuture bulkInsertAsync( } } + // ── Detokenize ──────────────────────────────────────────────────────────── + public DetokenizeResponse bulkDetokenize(DetokenizeRequest detokenizeRequest) throws SkyflowException { + return bulkDetokenize(detokenizeRequest, null); + } + + public DetokenizeResponse bulkDetokenize(DetokenizeRequest detokenizeRequest, DetokenizeOptions options) throws SkyflowException { LogUtil.printInfoLog(InfoLogs.DETOKENIZE_TRIGGERED.getLog()); try { - DetokenizeResponse response; configureDetokenizeConcurrencyAndBatchSize(detokenizeRequest.getTokens().size()); LogUtil.printInfoLog(InfoLogs.VALIDATE_DETOKENIZE_REQUEST.getLog()); Validations.validateDetokenizeRequest(detokenizeRequest); + if (options != null) Validations.validateCustomHeaders(options.getCustomHeaders(), InterfaceName.DETOKENIZE); setBearerToken(); com.skyflow.generated.rest.resources.flowservice.requests.V1FlowDetokenizeRequest request = super.getDetokenizeRequestBody(detokenizeRequest); - - response = this.processDetokenizeSync(request, detokenizeRequest.getTokens()); - return response; + Map customHeaders = extractCustomHeaders(options); + return this.processDetokenizeSync(request, detokenizeRequest.getTokens(), customHeaders); } catch (ApiClientApiException e) { String bodyString = gson.toJson(e.body()); LogUtil.printErrorLog(ErrorLogs.DETOKENIZE_REQUEST_REJECTED.getLog()); @@ -142,24 +180,29 @@ public DetokenizeResponse bulkDetokenize(DetokenizeRequest detokenizeRequest) th } public CompletableFuture bulkDetokenizeAsync(DetokenizeRequest detokenizeRequest) throws SkyflowException { + return bulkDetokenizeAsync(detokenizeRequest, null); + } + + public CompletableFuture bulkDetokenizeAsync(DetokenizeRequest detokenizeRequest, DetokenizeOptions options) throws SkyflowException { LogUtil.printInfoLog(InfoLogs.DETOKENIZE_TRIGGERED.getLog()); ExecutorService executor = Executors.newFixedThreadPool(detokenizeConcurrencyLimit); try { configureDetokenizeConcurrencyAndBatchSize(detokenizeRequest.getTokens().size()); LogUtil.printInfoLog(InfoLogs.VALIDATE_DETOKENIZE_REQUEST.getLog()); Validations.validateDetokenizeRequest(detokenizeRequest); + if (options != null) Validations.validateCustomHeaders(options.getCustomHeaders(), InterfaceName.DETOKENIZE); setBearerToken(); com.skyflow.generated.rest.resources.flowservice.requests.V1FlowDetokenizeRequest request = super.getDetokenizeRequestBody(detokenizeRequest); + Map customHeaders = extractCustomHeaders(options); LogUtil.printInfoLog(InfoLogs.PROCESSING_BATCHES.getLog()); List errorTokens = Collections.synchronizedList(new ArrayList<>()); List successRecords = new ArrayList<>(); - // Create batches List batches = Utils.createDetokenizeBatches(request, detokenizeBatchSize); - List> futures = this.detokenizeBatchFutures(executor, batches, errorTokens); + List> futures = this.detokenizeBatchFutures(executor, batches, errorTokens, customHeaders); return CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])) .thenApply(v -> { for (CompletableFuture future : futures) { @@ -177,6 +220,13 @@ public CompletableFuture bulkDetokenizeAsync(DetokenizeReque executor.shutdown(); return new DetokenizeResponse(successRecords, errorTokens, detokenizeRequest.getTokens()); }); + } catch (ApiClientApiException e) { + String bodyString = gson.toJson(e.body()); + LogUtil.printErrorLog(ErrorLogs.DETOKENIZE_REQUEST_REJECTED.getLog()); + throw new SkyflowException(e.statusCode(), e, e.headers(), bodyString); + } catch (SkyflowException e) { + LogUtil.printErrorLog(ErrorLogs.DETOKENIZE_REQUEST_REJECTED.getLog()); + throw e; } catch (Exception e) { LogUtil.printErrorLog(ErrorLogs.DETOKENIZE_REQUEST_REJECTED.getLog()); throw new SkyflowException(e.getMessage()); @@ -185,16 +235,24 @@ public CompletableFuture bulkDetokenizeAsync(DetokenizeReque } } + // ── Delete Tokens ───────────────────────────────────────────────────────── + public DeleteTokensResponse bulkDeleteTokens(DeleteTokensRequest deleteTokensRequest) throws SkyflowException { + return bulkDeleteTokens(deleteTokensRequest, null); + } + + public DeleteTokensResponse bulkDeleteTokens(DeleteTokensRequest deleteTokensRequest, DeleteTokensOptions options) throws SkyflowException { LogUtil.printInfoLog(InfoLogs.DELETE_TOKENS_TRIGGERED.getLog()); try { LogUtil.printInfoLog(InfoLogs.VALIDATE_DELETE_TOKENS_REQUEST.getLog()); Validations.validateDeleteTokensRequest(deleteTokensRequest); + if (options != null) Validations.validateCustomHeaders(options.getCustomHeaders(), InterfaceName.DELETE); configureDeleteTokensConcurrencyAndBatchSize(deleteTokensRequest.getTokens().size()); setBearerToken(); com.skyflow.generated.rest.resources.flowservice.requests.V1FlowDeleteTokenRequest request = super.getDeleteTokensRequestBody(deleteTokensRequest); - return this.processDeleteTokensSync(request, deleteTokensRequest.getTokens()); + Map customHeaders = extractCustomHeaders(options); + return this.processDeleteTokensSync(request, deleteTokensRequest.getTokens(), customHeaders); } catch (ApiClientApiException e) { String bodyString = gson.toJson(e.body()); LogUtil.printErrorLog(ErrorLogs.DELETE_REQUEST_REJECTED.getLog()); @@ -206,15 +264,21 @@ public DeleteTokensResponse bulkDeleteTokens(DeleteTokensRequest deleteTokensReq } public CompletableFuture bulkDeleteTokensAsync(DeleteTokensRequest deleteTokensRequest) throws SkyflowException { + return bulkDeleteTokensAsync(deleteTokensRequest, null); + } + + public CompletableFuture bulkDeleteTokensAsync(DeleteTokensRequest deleteTokensRequest, DeleteTokensOptions options) throws SkyflowException { LogUtil.printInfoLog(InfoLogs.DELETE_TOKENS_TRIGGERED.getLog()); ExecutorService executor = Executors.newFixedThreadPool(deleteTokensConcurrencyLimit); try { LogUtil.printInfoLog(InfoLogs.VALIDATE_DELETE_TOKENS_REQUEST.getLog()); Validations.validateDeleteTokensRequest(deleteTokensRequest); + if (options != null) Validations.validateCustomHeaders(options.getCustomHeaders(), InterfaceName.DELETE); configureDeleteTokensConcurrencyAndBatchSize(deleteTokensRequest.getTokens().size()); setBearerToken(); com.skyflow.generated.rest.resources.flowservice.requests.V1FlowDeleteTokenRequest request = super.getDeleteTokensRequestBody(deleteTokensRequest); + Map customHeaders = extractCustomHeaders(options); LogUtil.printInfoLog(InfoLogs.PROCESSING_BATCHES.getLog()); @@ -225,7 +289,7 @@ public CompletableFuture bulkDeleteTokensAsync(DeleteToken Utils.createDeleteTokensBatches(request, deleteTokensBatchSize); List> futures = - this.deleteTokensBatchFutures(executor, batches); + this.deleteTokensBatchFutures(executor, batches, customHeaders); return CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])) .thenApply(v -> { @@ -259,9 +323,140 @@ public CompletableFuture bulkDeleteTokensAsync(DeleteToken } } + // ── Tokenize ────────────────────────────────────────────────────────────── + + public TokenizeResponse bulkTokenize(TokenizeRequest tokenizeRequest) throws SkyflowException { + return bulkTokenize(tokenizeRequest, null); + } + + public TokenizeResponse bulkTokenize(TokenizeRequest tokenizeRequest, TokenizeOptions options) throws SkyflowException { + LogUtil.printInfoLog(InfoLogs.TOKENIZE_TRIGGERED.getLog()); + try { + LogUtil.printInfoLog(InfoLogs.VALIDATING_TOKENIZE_REQUEST.getLog()); + Validations.validateTokenizeRequest(tokenizeRequest); + if (options != null) Validations.validateCustomHeaders(options.getCustomHeaders(), InterfaceName.TOKENIZE); + configureTokenizeConcurrencyAndBatchSize(tokenizeRequest.getData().size()); + setBearerToken(); + com.skyflow.generated.rest.resources.flowservice.requests.V1FlowTokenizeRequest request = + super.getTokenizeRequestBody(tokenizeRequest); + Map customHeaders = extractCustomHeaders(options); + return this.processTokenizeSync(request, tokenizeRequest.getData(), customHeaders); + } catch (ApiClientApiException e) { + String bodyString = gson.toJson(e.body()); + LogUtil.printErrorLog(ErrorLogs.TOKENIZE_REQUEST_REJECTED.getLog()); + throw new SkyflowException(e.statusCode(), e, e.headers(), bodyString); + } catch (SkyflowException e) { + LogUtil.printErrorLog(ErrorLogs.TOKENIZE_REQUEST_REJECTED.getLog()); + throw e; + } catch (ExecutionException | InterruptedException e) { + LogUtil.printErrorLog(ErrorLogs.TOKENIZE_REQUEST_REJECTED.getLog()); + throw new SkyflowException(e.getMessage()); + } + } + + public CompletableFuture bulkTokenizeAsync(TokenizeRequest tokenizeRequest) throws SkyflowException { + return bulkTokenizeAsync(tokenizeRequest, null); + } + + public CompletableFuture bulkTokenizeAsync(TokenizeRequest tokenizeRequest, TokenizeOptions options) throws SkyflowException { + LogUtil.printInfoLog(InfoLogs.TOKENIZE_TRIGGERED.getLog()); + ExecutorService executor = Executors.newFixedThreadPool(tokenizeConcurrencyLimit); + try { + LogUtil.printInfoLog(InfoLogs.VALIDATING_TOKENIZE_REQUEST.getLog()); + Validations.validateTokenizeRequest(tokenizeRequest); + if (options != null) Validations.validateCustomHeaders(options.getCustomHeaders(), InterfaceName.TOKENIZE); + configureTokenizeConcurrencyAndBatchSize(tokenizeRequest.getData().size()); + setBearerToken(); + com.skyflow.generated.rest.resources.flowservice.requests.V1FlowTokenizeRequest request = + super.getTokenizeRequestBody(tokenizeRequest); + Map customHeaders = extractCustomHeaders(options); + + LogUtil.printInfoLog(InfoLogs.PROCESSING_BATCHES.getLog()); + + List errorRecords = Collections.synchronizedList(new ArrayList<>()); + List successRecords = Collections.synchronizedList(new ArrayList<>()); + + List batches = + Utils.createTokenizeBatches(request, tokenizeBatchSize); + + List> futures = + this.tokenizeBatchFutures(executor, batches, customHeaders); + + return CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])) + .thenApply(v -> { + for (CompletableFuture future : futures) { + TokenizeResponse futureResponse = future.join(); + if (futureResponse != null) { + if (futureResponse.getSuccess() != null) { + successRecords.addAll(futureResponse.getSuccess()); + } + if (futureResponse.getErrors() != null) { + errorRecords.addAll(futureResponse.getErrors()); + } + } + } + LogUtil.printInfoLog(InfoLogs.TOKENIZE_REQUEST_RESOLVED.getLog()); + executor.shutdown(); + return new TokenizeResponse(successRecords, errorRecords, tokenizeRequest.getData()); + }); + } catch (ApiClientApiException e) { + String bodyString = gson.toJson(e.body()); + LogUtil.printErrorLog(ErrorLogs.TOKENIZE_REQUEST_REJECTED.getLog()); + throw new SkyflowException(e.statusCode(), e, e.headers(), bodyString); + } catch (SkyflowException e) { + LogUtil.printErrorLog(ErrorLogs.TOKENIZE_REQUEST_REJECTED.getLog()); + throw e; + } catch (Exception e) { + LogUtil.printErrorLog(ErrorLogs.TOKENIZE_REQUEST_REJECTED.getLog()); + throw new SkyflowException(e.getMessage()); + } finally { + executor.shutdown(); + } + } + + // ── Private helpers ─────────────────────────────────────────────────────── + + private Map extractCustomHeaders(InsertOptions options) { + if (options == null) return Collections.emptyMap(); + Map result = new HashMap<>(); + options.getCustomHeaders().forEach((k, v) -> result.put(k.toString(), v)); + return result; + } + + private Map extractCustomHeaders(DetokenizeOptions options) { + if (options == null) return Collections.emptyMap(); + Map result = new HashMap<>(); + options.getCustomHeaders().forEach((k, v) -> result.put(k.toString(), v)); + return result; + } + + private Map extractCustomHeaders(TokenizeOptions options) { + if (options == null) return Collections.emptyMap(); + Map result = new HashMap<>(); + options.getCustomHeaders().forEach((k, v) -> result.put(k.toString(), v)); + return result; + } + + private Map extractCustomHeaders(DeleteTokensOptions options) { + if (options == null) return Collections.emptyMap(); + Map result = new HashMap<>(); + options.getCustomHeaders().forEach((k, v) -> result.put(k.toString(), v)); + return result; + } + + private RequestOptions buildRequestOptions(Map customHeaders) { + RequestOptions.Builder builder = RequestOptions.builder() + .addHeader(Constants.SDK_METRICS_HEADER_KEY, metrics.toString()); + if (customHeaders != null) { + customHeaders.forEach(builder::addHeader); + } + return builder.build(); + } + private DeleteTokensResponse processDeleteTokensSync( com.skyflow.generated.rest.resources.flowservice.requests.V1FlowDeleteTokenRequest deleteTokensRequest, - List originalTokens + List originalTokens, + Map customHeaders ) throws ExecutionException, InterruptedException, SkyflowException { LogUtil.printInfoLog(InfoLogs.PROCESSING_BATCHES.getLog()); List errorRecords = Collections.synchronizedList(new ArrayList<>()); @@ -271,7 +466,7 @@ private DeleteTokensResponse processDeleteTokensSync( Utils.createDeleteTokensBatches(deleteTokensRequest, deleteTokensBatchSize); try { List> futures = - this.deleteTokensBatchFutures(executor, batches); + this.deleteTokensBatchFutures(executor, batches, customHeaders); try { CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).get(); } catch (Exception e) { @@ -297,33 +492,32 @@ private DeleteTokensResponse processDeleteTokensSync( private List> deleteTokensBatchFutures( ExecutorService executor, - List batches) { + List batches, + Map customHeaders) { List> futures = new ArrayList<>(); if (batches == null) return futures; for (int batchIndex = 0; batchIndex < batches.size(); batchIndex++) { final int index = batchIndex; com.skyflow.generated.rest.resources.flowservice.requests.V1FlowDeleteTokenRequest batch = batches.get(index); CompletableFuture future = CompletableFuture - .supplyAsync(() -> processDeleteTokensBatch(batch), executor) + .supplyAsync(() -> processDeleteTokensBatch(batch, customHeaders), executor) .handle((result, ex) -> { if (ex != null) { List batchErrors = Utils.handleDeleteTokensBatchException(ex, batch, index, deleteTokensBatchSize); return new DeleteTokensResponse(new ArrayList<>(), batchErrors); } - return Utils.formatDeleteTokensResponse(result, index, deleteTokensBatchSize); + return Utils.formatDeleteTokensResponse(result.body(), index, deleteTokensBatchSize, result.headers()); }); futures.add(future); } return futures; } - private com.skyflow.generated.rest.types.V1FlowDeleteTokenResponse processDeleteTokensBatch( - com.skyflow.generated.rest.resources.flowservice.requests.V1FlowDeleteTokenRequest batch) { - RequestOptions requestOptions = RequestOptions.builder() - .addHeader(Constants.SDK_METRICS_HEADER_KEY, metrics.toString()) - .build(); - return this.getRecordsApi().deletetoken(batch, requestOptions); + private ApiClientHttpResponse processDeleteTokensBatch( + com.skyflow.generated.rest.resources.flowservice.requests.V1FlowDeleteTokenRequest batch, + Map customHeaders) { + return this.getRecordsApi().withRawResponse().deletetoken(batch, buildRequestOptions(customHeaders)); } private void configureDeleteTokensConcurrencyAndBatchSize(int totalRequests) { @@ -362,7 +556,6 @@ private void configureDeleteTokensConcurrencyAndBatchSize(int totalRequests) { } } - // Max no of threads required to run all batches concurrently at once int maxConcurrencyNeeded = (totalRequests + this.deleteTokensBatchSize - 1) / this.deleteTokensBatchSize; if (userProvidedConcurrencyLimit != null) { @@ -392,86 +585,10 @@ private void configureDeleteTokensConcurrencyAndBatchSize(int totalRequests) { } } - public TokenizeResponse bulkTokenize(TokenizeRequest tokenizeRequest) throws SkyflowException { - LogUtil.printInfoLog(InfoLogs.TOKENIZE_TRIGGERED.getLog()); - try { - LogUtil.printInfoLog(InfoLogs.VALIDATING_TOKENIZE_REQUEST.getLog()); - Validations.validateTokenizeRequest(tokenizeRequest); - configureTokenizeConcurrencyAndBatchSize(tokenizeRequest.getData().size()); - setBearerToken(); - com.skyflow.generated.rest.resources.flowservice.requests.V1FlowTokenizeRequest request = - super.getTokenizeRequestBody(tokenizeRequest); - return this.processTokenizeSync(request, tokenizeRequest.getData()); - } catch (ApiClientApiException e) { - String bodyString = gson.toJson(e.body()); - LogUtil.printErrorLog(ErrorLogs.TOKENIZE_REQUEST_REJECTED.getLog()); - throw new SkyflowException(e.statusCode(), e, e.headers(), bodyString); - } catch (SkyflowException e) { - LogUtil.printErrorLog(ErrorLogs.TOKENIZE_REQUEST_REJECTED.getLog()); - throw e; - } catch (ExecutionException | InterruptedException e) { - LogUtil.printErrorLog(ErrorLogs.TOKENIZE_REQUEST_REJECTED.getLog()); - throw new SkyflowException(e.getMessage()); - } - } - - public CompletableFuture bulkTokenizeAsync(TokenizeRequest tokenizeRequest) throws SkyflowException { - LogUtil.printInfoLog(InfoLogs.TOKENIZE_TRIGGERED.getLog()); - ExecutorService executor = Executors.newFixedThreadPool(tokenizeConcurrencyLimit); - try { - LogUtil.printInfoLog(InfoLogs.VALIDATING_TOKENIZE_REQUEST.getLog()); - Validations.validateTokenizeRequest(tokenizeRequest); - configureTokenizeConcurrencyAndBatchSize(tokenizeRequest.getData().size()); - setBearerToken(); - com.skyflow.generated.rest.resources.flowservice.requests.V1FlowTokenizeRequest request = - super.getTokenizeRequestBody(tokenizeRequest); - - LogUtil.printInfoLog(InfoLogs.PROCESSING_BATCHES.getLog()); - - List errorRecords = Collections.synchronizedList(new ArrayList<>()); - List successRecords = Collections.synchronizedList(new ArrayList<>()); - - List batches = - Utils.createTokenizeBatches(request, tokenizeBatchSize); - - List> futures = - this.tokenizeBatchFutures(executor, batches); - - return CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])) - .thenApply(v -> { - for (CompletableFuture future : futures) { - TokenizeResponse futureResponse = future.join(); - if (futureResponse != null) { - if (futureResponse.getSuccess() != null) { - successRecords.addAll(futureResponse.getSuccess()); - } - if (futureResponse.getErrors() != null) { - errorRecords.addAll(futureResponse.getErrors()); - } - } - } - LogUtil.printInfoLog(InfoLogs.TOKENIZE_REQUEST_RESOLVED.getLog()); - executor.shutdown(); - return new TokenizeResponse(successRecords, errorRecords, tokenizeRequest.getData()); - }); - } catch (ApiClientApiException e) { - String bodyString = gson.toJson(e.body()); - LogUtil.printErrorLog(ErrorLogs.TOKENIZE_REQUEST_REJECTED.getLog()); - throw new SkyflowException(e.statusCode(), e, e.headers(), bodyString); - } catch (SkyflowException e) { - LogUtil.printErrorLog(ErrorLogs.TOKENIZE_REQUEST_REJECTED.getLog()); - throw e; - } catch (Exception e) { - LogUtil.printErrorLog(ErrorLogs.TOKENIZE_REQUEST_REJECTED.getLog()); - throw new SkyflowException(e.getMessage()); - } finally { - executor.shutdown(); - } - } - private TokenizeResponse processTokenizeSync( com.skyflow.generated.rest.resources.flowservice.requests.V1FlowTokenizeRequest tokenizeRequest, - java.util.ArrayList originalData + java.util.ArrayList originalData, + Map customHeaders ) throws ExecutionException, InterruptedException, SkyflowException { LogUtil.printInfoLog(InfoLogs.PROCESSING_BATCHES.getLog()); List errorRecords = Collections.synchronizedList(new ArrayList<>()); @@ -481,7 +598,7 @@ private TokenizeResponse processTokenizeSync( Utils.createTokenizeBatches(tokenizeRequest, tokenizeBatchSize); try { List> futures = - this.tokenizeBatchFutures(executor, batches); + this.tokenizeBatchFutures(executor, batches, customHeaders); try { CompletableFuture allFutures = CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])); allFutures.join(); @@ -512,33 +629,32 @@ private TokenizeResponse processTokenizeSync( private List> tokenizeBatchFutures( ExecutorService executor, - List batches) { + List batches, + Map customHeaders) { List> futures = new ArrayList<>(); if (batches == null) return futures; for (int batchIndex = 0; batchIndex < batches.size(); batchIndex++) { final int index = batchIndex; com.skyflow.generated.rest.resources.flowservice.requests.V1FlowTokenizeRequest batch = batches.get(index); CompletableFuture future = CompletableFuture - .supplyAsync(() -> processTokenizeBatch(batch), executor) + .supplyAsync(() -> processTokenizeBatch(batch, customHeaders), executor) .handle((result, ex) -> { if (ex != null) { List batchErrors = Utils.handleTokenizeBatchException(ex, batch, index, tokenizeBatchSize); return new TokenizeResponse(new ArrayList<>(), batchErrors); } - return Utils.formatTokenizeResponse(result, batch, index, tokenizeBatchSize); + return Utils.formatTokenizeResponse(result.body(), batch, index, tokenizeBatchSize, result.headers()); }); futures.add(future); } return futures; } - private com.skyflow.generated.rest.types.V1FlowTokenizeResponse processTokenizeBatch( - com.skyflow.generated.rest.resources.flowservice.requests.V1FlowTokenizeRequest batch) { - RequestOptions requestOptions = RequestOptions.builder() - .addHeader(Constants.SDK_METRICS_HEADER_KEY, metrics.toString()) - .build(); - return this.getRecordsApi().tokenize(batch, requestOptions); + private ApiClientHttpResponse processTokenizeBatch( + com.skyflow.generated.rest.resources.flowservice.requests.V1FlowTokenizeRequest batch, + Map customHeaders) { + return this.getRecordsApi().withRawResponse().tokenize(batch, buildRequestOptions(customHeaders)); } private void configureTokenizeConcurrencyAndBatchSize(int totalRequests) { @@ -608,12 +724,13 @@ private void configureTokenizeConcurrencyAndBatchSize(int totalRequests) { private com.skyflow.vault.data.InsertResponse processSync( com.skyflow.generated.rest.resources.flowservice.requests.V1InsertRequest insertRequest, - ArrayList originalPayload + ArrayList originalPayload, + Map customHeaders ) throws ExecutionException, InterruptedException { LogUtil.printInfoLog(InfoLogs.PROCESSING_BATCHES.getLog()); List successRecords = new ArrayList<>(); List errorRecords = Collections.synchronizedList(new ArrayList<>()); - List> futures = this.insertBatchFutures(insertRequest, errorRecords); + List> futures = this.insertBatchFutures(insertRequest, errorRecords, customHeaders); CompletableFuture allFutures = CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])); allFutures.join(); @@ -636,7 +753,8 @@ private com.skyflow.vault.data.InsertResponse processSync( private DetokenizeResponse processDetokenizeSync( com.skyflow.generated.rest.resources.flowservice.requests.V1FlowDetokenizeRequest detokenizeRequest, - List originalTokens + List originalTokens, + Map customHeaders ) throws ExecutionException, InterruptedException, SkyflowException { LogUtil.printInfoLog(InfoLogs.PROCESSING_BATCHES.getLog()); List errorTokens = Collections.synchronizedList(new ArrayList<>()); @@ -644,12 +762,12 @@ private DetokenizeResponse processDetokenizeSync( ExecutorService executor = Executors.newFixedThreadPool(detokenizeConcurrencyLimit); List batches = Utils.createDetokenizeBatches(detokenizeRequest, detokenizeBatchSize); try { - List> futures = this.detokenizeBatchFutures(executor, batches, errorTokens); + List> futures = this.detokenizeBatchFutures(executor, batches, errorTokens, customHeaders); try { - CompletableFuture allFutures = CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])); allFutures.join(); } catch (Exception e) { + // individual batch errors are already captured } for (CompletableFuture future : futures) { DetokenizeResponse futureResponse = future.get(); @@ -673,16 +791,19 @@ private DetokenizeResponse processDetokenizeSync( return response; } - private List> detokenizeBatchFutures(ExecutorService executor, List batches, List errorTokens) { + private List> detokenizeBatchFutures( + ExecutorService executor, + List batches, + List errorTokens, + Map customHeaders) { List> futures = new ArrayList<>(); try { - for (int batchIndex = 0; batchIndex < batches.size(); batchIndex++) { com.skyflow.generated.rest.resources.flowservice.requests.V1FlowDetokenizeRequest batch = batches.get(batchIndex); int batchNumber = batchIndex; CompletableFuture future = CompletableFuture - .supplyAsync(() -> processDetokenizeBatch(batch), executor) - .thenApply(response -> Utils.formatDetokenizeResponse(response, batchNumber, detokenizeBatchSize)) + .supplyAsync(() -> processDetokenizeBatch(batch, customHeaders), executor) + .thenApply(response -> Utils.formatDetokenizeResponse(response.body(), batchNumber, detokenizeBatchSize, response.headers())) .exceptionally(ex -> { errorTokens.addAll(Utils.handleDetokenizeBatchException(ex, batch, batchNumber, detokenizeBatchSize)); return null; @@ -696,17 +817,16 @@ private List> detokenizeBatchFutures(Execu return futures; } - private com.skyflow.generated.rest.types.V1FlowDetokenizeResponse processDetokenizeBatch(com.skyflow.generated.rest.resources.flowservice.requests.V1FlowDetokenizeRequest batch) { - RequestOptions requestOptions = RequestOptions.builder() - .addHeader(Constants.SDK_METRICS_HEADER_KEY, metrics.toString()) - .build(); - return this.getRecordsApi().detokenize(batch, requestOptions); + private ApiClientHttpResponse processDetokenizeBatch( + com.skyflow.generated.rest.resources.flowservice.requests.V1FlowDetokenizeRequest batch, + Map customHeaders) { + return this.getRecordsApi().withRawResponse().detokenize(batch, buildRequestOptions(customHeaders)); } - private List> - insertBatchFutures( + private List> insertBatchFutures( com.skyflow.generated.rest.resources.flowservice.requests.V1InsertRequest insertRequest, - List errorRecords) { + List errorRecords, + Map customHeaders) { List records = insertRequest.getRecords().get(); ExecutorService executor = Executors.newFixedThreadPool(insertConcurrencyLimit); @@ -719,8 +839,8 @@ private com.skyflow.generated.rest.types.V1FlowDetokenizeResponse processDetoken List batch = batches.get(batchIndex); int batchNumber = batchIndex; CompletableFuture future = CompletableFuture - .supplyAsync(() -> insertBatch(batch, insertRequest.getTableName().isPresent() ? insertRequest.getTableName().get() : null, upsert), executor) - .thenApply(response -> Utils.formatResponse(response, batchNumber, insertBatchSize)) + .supplyAsync(() -> insertBatch(batch, insertRequest.getTableName().isPresent() ? insertRequest.getTableName().get() : null, upsert, customHeaders), executor) + .thenApply(response -> Utils.formatResponse(response.body(), batchNumber, insertBatchSize, response.headers())) .exceptionally(ex -> { errorRecords.addAll(Utils.handleBatchException(ex, batch, batchNumber, insertBatchSize)); return null; @@ -733,20 +853,16 @@ private com.skyflow.generated.rest.types.V1FlowDetokenizeResponse processDetoken return futures; } - private V1InsertResponse insertBatch(List batch, String tableName, V1Upsert upsert) { + private ApiClientHttpResponse insertBatch(List batch, String tableName, V1Upsert upsert, Map customHeaders) { com.skyflow.generated.rest.resources.flowservice.requests.V1InsertRequest.Builder req = com.skyflow.generated.rest.resources.flowservice.requests.V1InsertRequest.builder() .vaultId(this.getVaultConfig().getVaultId()) .records(batch) .upsert(upsert); -// .build(); if (tableName != null && !tableName.isEmpty()) { req.tableName(tableName); } com.skyflow.generated.rest.resources.flowservice.requests.V1InsertRequest request = req.build(); - RequestOptions requestOptions = RequestOptions.builder() - .addHeader(Constants.SDK_METRICS_HEADER_KEY, metrics.toString()) - .build(); - return this.getRecordsApi().insert(request, requestOptions); + return this.getRecordsApi().withRawResponse().insert(request, buildRequestOptions(customHeaders)); } private void configureInsertConcurrencyAndBatchSize(int totalRequests) { diff --git a/v3/src/main/java/com/skyflow/vault/data/DeleteTokensOptions.java b/v3/src/main/java/com/skyflow/vault/data/DeleteTokensOptions.java new file mode 100644 index 00000000..fdb05633 --- /dev/null +++ b/v3/src/main/java/com/skyflow/vault/data/DeleteTokensOptions.java @@ -0,0 +1,36 @@ +package com.skyflow.vault.data; + +import com.skyflow.enums.CustomHeaderKey; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +public final class DeleteTokensOptions { + private final Map customHeaders; + + private DeleteTokensOptions(Builder builder) { + this.customHeaders = Collections.unmodifiableMap(new HashMap<>(builder.customHeaders)); + } + + public Map getCustomHeaders() { + return customHeaders; + } + + public static Builder builder() { + return new Builder(); + } + + public static final class Builder { + private final Map customHeaders = new HashMap<>(); + + public Builder addCustomHeader(CustomHeaderKey key, String value) { + this.customHeaders.put(key, value); + return this; + } + + public DeleteTokensOptions build() { + return new DeleteTokensOptions(this); + } + } +} \ No newline at end of file diff --git a/v3/src/main/java/com/skyflow/vault/data/DetokenizeOptions.java b/v3/src/main/java/com/skyflow/vault/data/DetokenizeOptions.java new file mode 100644 index 00000000..fe85ad96 --- /dev/null +++ b/v3/src/main/java/com/skyflow/vault/data/DetokenizeOptions.java @@ -0,0 +1,36 @@ +package com.skyflow.vault.data; + +import com.skyflow.enums.CustomHeaderKey; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +public final class DetokenizeOptions { + private final Map customHeaders; + + private DetokenizeOptions(Builder builder) { + this.customHeaders = Collections.unmodifiableMap(new HashMap<>(builder.customHeaders)); + } + + public Map getCustomHeaders() { + return customHeaders; + } + + public static Builder builder() { + return new Builder(); + } + + public static final class Builder { + private final Map customHeaders = new HashMap<>(); + + public Builder addCustomHeader(CustomHeaderKey key, String value) { + this.customHeaders.put(key, value); + return this; + } + + public DetokenizeOptions build() { + return new DetokenizeOptions(this); + } + } +} \ No newline at end of file diff --git a/v3/src/main/java/com/skyflow/vault/data/ErrorRecord.java b/v3/src/main/java/com/skyflow/vault/data/ErrorRecord.java index 9044f189..732952f3 100644 --- a/v3/src/main/java/com/skyflow/vault/data/ErrorRecord.java +++ b/v3/src/main/java/com/skyflow/vault/data/ErrorRecord.java @@ -10,14 +10,22 @@ public class ErrorRecord { private String error; @Expose(serialize = true) private int code; -// public ErrorRecord() { -// } + @Expose(serialize = true) + private String requestId; public ErrorRecord(int index, String error, int code) { this.index = index; this.error = error; this.code = code; } + + public ErrorRecord(int index, String error, int code, String requestId) { + this.index = index; + this.error = error; + this.code = code; + this.requestId = requestId; + } + public String getError() { return error; } @@ -30,6 +38,10 @@ public int getIndex() { return index; } + public String getRequestId() { + return requestId; + } + @Override public String toString() { diff --git a/v3/src/main/java/com/skyflow/vault/data/InsertOptions.java b/v3/src/main/java/com/skyflow/vault/data/InsertOptions.java new file mode 100644 index 00000000..8373351b --- /dev/null +++ b/v3/src/main/java/com/skyflow/vault/data/InsertOptions.java @@ -0,0 +1,36 @@ +package com.skyflow.vault.data; + +import com.skyflow.enums.CustomHeaderKey; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +public final class InsertOptions { + private final Map customHeaders; + + private InsertOptions(Builder builder) { + this.customHeaders = Collections.unmodifiableMap(new HashMap<>(builder.customHeaders)); + } + + public Map getCustomHeaders() { + return customHeaders; + } + + public static Builder builder() { + return new Builder(); + } + + public static final class Builder { + private final Map customHeaders = new HashMap<>(); + + public Builder addCustomHeader(CustomHeaderKey key, String value) { + this.customHeaders.put(key, value); + return this; + } + + public InsertOptions build() { + return new InsertOptions(this); + } + } +} \ No newline at end of file diff --git a/v3/src/main/java/com/skyflow/vault/data/TokenizeOptions.java b/v3/src/main/java/com/skyflow/vault/data/TokenizeOptions.java new file mode 100644 index 00000000..1184b2a3 --- /dev/null +++ b/v3/src/main/java/com/skyflow/vault/data/TokenizeOptions.java @@ -0,0 +1,36 @@ +package com.skyflow.vault.data; + +import com.skyflow.enums.CustomHeaderKey; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +public final class TokenizeOptions { + private final Map customHeaders; + + private TokenizeOptions(Builder builder) { + this.customHeaders = Collections.unmodifiableMap(new HashMap<>(builder.customHeaders)); + } + + public Map getCustomHeaders() { + return customHeaders; + } + + public static Builder builder() { + return new Builder(); + } + + public static final class Builder { + private final Map customHeaders = new HashMap<>(); + + public Builder addCustomHeader(CustomHeaderKey key, String value) { + this.customHeaders.put(key, value); + return this; + } + + public TokenizeOptions build() { + return new TokenizeOptions(this); + } + } +} \ No newline at end of file diff --git a/v3/src/test/java/com/skyflow/SkyflowTests.java b/v3/src/test/java/com/skyflow/SkyflowTests.java index 0904461e..2e454207 100644 --- a/v3/src/test/java/com/skyflow/SkyflowTests.java +++ b/v3/src/test/java/com/skyflow/SkyflowTests.java @@ -193,6 +193,139 @@ public void testSetLogLevelReturnsBuilder() { } } + @Test + public void testSetLogLevelNullDefaultsToError() { + try { + VaultConfig config = new VaultConfig(); + config.setVaultId(vaultID); + config.setClusterId(clusterID); + config.setEnv(Env.SANDBOX); + + Skyflow skyflow = Skyflow.builder() + .setLogLevel(null) + .addVaultConfig(config) + .build(); + + Assert.assertEquals(LogLevel.ERROR, skyflow.getLogLevel()); + } catch (Exception e) { + Assert.fail("Unexpected exception: " + e.getMessage()); + } + } + + @Test + public void testAddSkyflowCredentials_invalidCredentials_throws() { + try { + VaultConfig config = new VaultConfig(); + config.setVaultId(vaultID); + config.setClusterId(clusterID); + config.setEnv(Env.SANDBOX); + + Credentials badCreds = new Credentials(); + badCreds.setToken(""); // empty token — invalid + + Skyflow.builder() + .addVaultConfig(config) + .addSkyflowCredentials(badCreds); + Assert.fail(EXCEPTION_NOT_THROWN); + } catch (SkyflowException e) { + Assert.assertEquals(ErrorCode.INVALID_INPUT.getCode(), e.getHttpCode()); + Assert.assertEquals(ErrorMessage.EmptyToken.getMessage(), e.getMessage()); + } + } + + @Test + public void testAddSkyflowCredentials_propagatesToVaultController() { + try { + VaultConfig config = new VaultConfig(); + config.setVaultId(vaultID); + config.setClusterId(clusterID); + config.setEnv(Env.SANDBOX); + + Credentials creds = new Credentials(); + creds.setToken(token); + + Skyflow skyflow = Skyflow.builder() + .addVaultConfig(config) + .addSkyflowCredentials(creds) + .build(); + + VaultController controller = skyflow.vault(); + Assert.assertNotNull(controller); + + // verify that common credentials were propagated — the controller should + // hold the credentials we passed, not null + Object builder = getField(skyflow, "builder"); + Credentials storedCreds = (Credentials) getField(builder.getClass().getSuperclass(), builder, "skyflowCredentials"); + Assert.assertEquals(token, storedCreds.getToken()); + } catch (Exception e) { + Assert.fail("Unexpected exception: " + e.getMessage()); + } + } + + @Test + public void testAddSkyflowCredentials_returnsBuilderForChaining() { + try { + VaultConfig config = new VaultConfig(); + config.setVaultId(vaultID); + config.setClusterId(clusterID); + config.setEnv(Env.SANDBOX); + + Credentials creds = new Credentials(); + creds.setToken(token); + + Skyflow.SkyflowClientBuilder builder = Skyflow.builder().addVaultConfig(config); + Skyflow.SkyflowClientBuilder returned = builder.addSkyflowCredentials(creds); + Assert.assertSame(builder, returned); + } catch (Exception e) { + Assert.fail("Unexpected exception: " + e.getMessage()); + } + } + + @Test + public void testGetVaultConfig_returnsStoredConfig() { + try { + VaultConfig config = new VaultConfig(); + config.setVaultId(vaultID); + config.setClusterId(clusterID); + config.setEnv(Env.SANDBOX); + + Skyflow skyflow = Skyflow.builder().addVaultConfig(config).build(); + VaultConfig stored = skyflow.getVaultConfig(); + + Assert.assertNotNull(stored); + Assert.assertEquals(vaultID, stored.getVaultId()); + Assert.assertEquals(clusterID, stored.getClusterId()); + Assert.assertEquals(Env.SANDBOX, stored.getEnv()); + } catch (Exception e) { + Assert.fail("Unexpected exception: " + e.getMessage()); + } + } + + @Test + public void testGetVaultConfig_returnsCloneNotOriginal() { + try { + VaultConfig config = new VaultConfig(); + config.setVaultId(vaultID); + config.setClusterId(clusterID); + config.setEnv(Env.SANDBOX); + + Skyflow skyflow = Skyflow.builder().addVaultConfig(config).build(); + VaultConfig stored = skyflow.getVaultConfig(); + + // The stored config must be a different object from the one passed in + Assert.assertNotSame(config, stored); + } catch (Exception e) { + Assert.fail("Unexpected exception: " + e.getMessage()); + } + } + + @Test + public void testBuilderReturnsNewInstanceEachCall() { + Skyflow.SkyflowClientBuilder b1 = Skyflow.builder(); + Skyflow.SkyflowClientBuilder b2 = Skyflow.builder(); + Assert.assertNotSame(b1, b2); + } + private Object getField(Object instance, String fieldName) throws Exception { Field f = instance.getClass().getDeclaredField(fieldName); f.setAccessible(true); diff --git a/v3/src/test/java/com/skyflow/enums/CustomHeaderKeyTests.java b/v3/src/test/java/com/skyflow/enums/CustomHeaderKeyTests.java new file mode 100644 index 00000000..5ca546df --- /dev/null +++ b/v3/src/test/java/com/skyflow/enums/CustomHeaderKeyTests.java @@ -0,0 +1,34 @@ +package com.skyflow.enums; + +import org.junit.Assert; +import org.junit.Test; + +public class CustomHeaderKeyTests { + + @Test + public void values_hasExactlyThreeEntries() { + Assert.assertEquals(3, CustomHeaderKey.values().length); + } + + @Test + public void skyflowAccountID_toStringReturnsCorrectHeader() { + Assert.assertEquals("x-skyflow-account-id", CustomHeaderKey.SkyflowAccountID.toString()); + } + + @Test + public void skyflowAccountName_toStringReturnsCorrectHeader() { + Assert.assertEquals("x-skyflow-account-name", CustomHeaderKey.SkyflowAccountName.toString()); + } + + @Test + public void requestIDHeader_toStringReturnsCorrectHeader() { + Assert.assertEquals("x-request-id", CustomHeaderKey.RequestIDHeader.toString()); + } + + @Test + public void valueOf_returnsCorrectConstants() { + Assert.assertEquals(CustomHeaderKey.SkyflowAccountID, CustomHeaderKey.valueOf("SkyflowAccountID")); + Assert.assertEquals(CustomHeaderKey.SkyflowAccountName, CustomHeaderKey.valueOf("SkyflowAccountName")); + Assert.assertEquals(CustomHeaderKey.RequestIDHeader, CustomHeaderKey.valueOf("RequestIDHeader")); + } +} \ No newline at end of file diff --git a/v3/src/test/java/com/skyflow/utils/UtilsTests.java b/v3/src/test/java/com/skyflow/utils/UtilsTests.java index 46d28164..be1456b1 100644 --- a/v3/src/test/java/com/skyflow/utils/UtilsTests.java +++ b/v3/src/test/java/com/skyflow/utils/UtilsTests.java @@ -1,5 +1,6 @@ package com.skyflow.utils; +import com.fasterxml.jackson.databind.ObjectMapper; import com.google.gson.JsonObject; import com.skyflow.config.Credentials; import com.skyflow.enums.Env; @@ -7,11 +8,18 @@ import com.skyflow.errors.ErrorMessage; import com.skyflow.errors.SkyflowException; import com.skyflow.generated.auth.rest.core.ApiClientApiException; +import com.skyflow.generated.rest.types.V1DeleteTokenResponseObject; +import com.skyflow.generated.rest.types.V1FlowDeleteTokenResponse; +import com.skyflow.generated.rest.types.V1FlowDetokenizeResponseObject; +import com.skyflow.generated.rest.types.V1FlowTokenizeResponseObject; import com.skyflow.generated.rest.types.V1InsertRecordData; import com.skyflow.generated.rest.types.V1InsertResponse; import com.skyflow.generated.rest.types.V1RecordResponseObject; import com.skyflow.utils.validations.Validations; import com.skyflow.vault.data.*; +import okhttp3.Protocol; +import okhttp3.Request; +import okhttp3.Response; import org.junit.After; import org.junit.AfterClass; import org.junit.Before; @@ -897,6 +905,339 @@ public void testCreateDetokenizeBatchesWithBatchSizeGreaterThanTokens() { Assert.assertEquals(Arrays.asList("token1"), batches.get(0).getTokens().get()); } + // ── handleBatchException — rest.ApiClientApiException paths ────────────── + + @Test + public void handleBatchException_restException_nullBody_createsOneErrorPerRecord() { + List batch = Arrays.asList( + V1InsertRecordData.builder().build(), V1InsertRecordData.builder().build()); + com.skyflow.generated.rest.core.ApiClientApiException apiEx = + new com.skyflow.generated.rest.core.ApiClientApiException("server error", 503, null); + Exception wrapper = new Exception("outer", apiEx); + + List errors = Utils.handleBatchException(wrapper, batch, 0, 2); + + Assert.assertEquals(2, errors.size()); + Assert.assertEquals(0, errors.get(0).getIndex()); + Assert.assertEquals(1, errors.get(1).getIndex()); + Assert.assertEquals(503, errors.get(0).getCode()); + } + + @Test + public void handleBatchException_restException_stringBody_doesNotThrowAndCreatesErrors() { + List batch = Collections.singletonList(V1InsertRecordData.builder().build()); + com.skyflow.generated.rest.core.ApiClientApiException apiEx = + new com.skyflow.generated.rest.core.ApiClientApiException("Unauthorized", 401, "plain string body"); + Exception wrapper = new Exception("outer", apiEx); + + List errors = Utils.handleBatchException(wrapper, batch, 0, 1); + + Assert.assertEquals(1, errors.size()); + Assert.assertEquals(0, errors.get(0).getIndex()); + Assert.assertEquals(401, errors.get(0).getCode()); + } + + @Test + public void handleBatchException_restException_errorFieldIsString_usesStringAsMessage() { + List batch = Arrays.asList( + V1InsertRecordData.builder().build(), V1InsertRecordData.builder().build()); + Map body = new HashMap<>(); + body.put("error", "Access denied"); + com.skyflow.generated.rest.core.ApiClientApiException apiEx = + new com.skyflow.generated.rest.core.ApiClientApiException("Forbidden", 403, body); + Exception wrapper = new Exception("outer", apiEx); + + List errors = Utils.handleBatchException(wrapper, batch, 0, 2); + + Assert.assertEquals(2, errors.size()); + Assert.assertEquals("Access denied", errors.get(0).getError()); + Assert.assertEquals(403, errors.get(0).getCode()); + Assert.assertEquals(0, errors.get(0).getIndex()); + Assert.assertEquals(1, errors.get(1).getIndex()); + } + + @Test + public void handleBatchException_recordsListWithNonMapEntry_skipsNonMapItem() { + List batch = Arrays.asList( + V1InsertRecordData.builder().build(), V1InsertRecordData.builder().build()); + Map rec = new HashMap<>(); + rec.put("error", "Err"); + rec.put("http_code", 400); + List mixedList = new ArrayList<>(Arrays.asList(rec, "not-a-map")); + Map body = new HashMap<>(); + body.put("records", mixedList); + com.skyflow.generated.rest.core.ApiClientApiException apiEx = + new com.skyflow.generated.rest.core.ApiClientApiException("Error", 400, body); + Exception wrapper = new Exception("outer", apiEx); + + List errors = Utils.handleBatchException(wrapper, batch, 0, 2); + + Assert.assertEquals(1, errors.size()); + Assert.assertEquals("Err", errors.get(0).getError()); + Assert.assertEquals(400, errors.get(0).getCode()); + Assert.assertEquals(0, errors.get(0).getIndex()); + } + + @Test + public void handleBatchException_restException_nullBody_batchNumber1_indexOffset() { + List batch = Arrays.asList( + V1InsertRecordData.builder().build(), V1InsertRecordData.builder().build()); + com.skyflow.generated.rest.core.ApiClientApiException apiEx = + new com.skyflow.generated.rest.core.ApiClientApiException("error", 500, null); + Exception wrapper = new Exception("outer", apiEx); + + List errors = Utils.handleBatchException(wrapper, batch, 2, 3); + + Assert.assertEquals(2, errors.size()); + Assert.assertEquals(6, errors.get(0).getIndex()); // 2 * 3 = 6 + Assert.assertEquals(7, errors.get(1).getIndex()); + } + + // ── handleDetokenizeBatchException — non-Map items in response list ──────── + + @Test + public void handleDetokenizeBatchException_responseListWithNonMapEntry_skipsNonMapItem() { + List tokens = Arrays.asList("t1", "t2"); + com.skyflow.generated.rest.resources.flowservice.requests.V1FlowDetokenizeRequest batch = + com.skyflow.generated.rest.resources.flowservice.requests.V1FlowDetokenizeRequest.builder() + .tokens(tokens).vaultId("v1").build(); + + Map rec = new HashMap<>(); + rec.put("error", "bad token"); + rec.put("http_code", 400); + List mixedList = new ArrayList<>(Arrays.asList(rec, "not-a-map")); + Map body = new HashMap<>(); + body.put("response", mixedList); + com.skyflow.generated.rest.core.ApiClientApiException apiEx = + new com.skyflow.generated.rest.core.ApiClientApiException("Error", 400, body); + Exception wrapper = new Exception("outer", apiEx); + + List errors = Utils.handleDetokenizeBatchException(wrapper, batch, 0, 2); + + Assert.assertEquals(1, errors.size()); + Assert.assertEquals("bad token", errors.get(0).getError()); + Assert.assertEquals(400, errors.get(0).getCode()); + Assert.assertEquals(0, errors.get(0).getIndex()); + } + + @Test + public void handleDetokenizeBatchException_restException_nullBody_createsOneErrorPerToken() { + List tokens = Arrays.asList("t1", "t2", "t3"); + com.skyflow.generated.rest.resources.flowservice.requests.V1FlowDetokenizeRequest batch = + com.skyflow.generated.rest.resources.flowservice.requests.V1FlowDetokenizeRequest.builder() + .tokens(tokens).vaultId("v1").build(); + com.skyflow.generated.rest.core.ApiClientApiException apiEx = + new com.skyflow.generated.rest.core.ApiClientApiException("gateway timeout", 504, null); + Exception wrapper = new Exception("outer", apiEx); + + List errors = Utils.handleDetokenizeBatchException(wrapper, batch, 1, 3); + + Assert.assertEquals(3, errors.size()); + Assert.assertEquals(3, errors.get(0).getIndex()); // 1 * 3 = 3 + Assert.assertEquals(4, errors.get(1).getIndex()); + Assert.assertEquals(5, errors.get(2).getIndex()); + Assert.assertEquals(504, errors.get(0).getCode()); + } + + @Test + public void handleDetokenizeBatchException_restException_errorFieldIsString_usesStringAsMessage() { + List tokens = Arrays.asList("t1", "t2"); + com.skyflow.generated.rest.resources.flowservice.requests.V1FlowDetokenizeRequest batch = + com.skyflow.generated.rest.resources.flowservice.requests.V1FlowDetokenizeRequest.builder() + .tokens(tokens).vaultId("v1").build(); + Map body = new HashMap<>(); + body.put("error", "token not found"); + com.skyflow.generated.rest.core.ApiClientApiException apiEx = + new com.skyflow.generated.rest.core.ApiClientApiException("Not found", 404, body); + Exception wrapper = new Exception("outer", apiEx); + + List errors = Utils.handleDetokenizeBatchException(wrapper, batch, 0, 2); + + Assert.assertEquals(2, errors.size()); + Assert.assertEquals("token not found", errors.get(0).getError()); + Assert.assertEquals(404, errors.get(0).getCode()); + } + + // ── handleDeleteTokensBatchException — rest exception + null body ────────── + + @Test + public void handleDeleteTokensBatchException_restException_nullBody_createsOneErrorPerToken() { + List tokens = Arrays.asList("tok1", "tok2", "tok3"); + com.skyflow.generated.rest.resources.flowservice.requests.V1FlowDeleteTokenRequest batch = + com.skyflow.generated.rest.resources.flowservice.requests.V1FlowDeleteTokenRequest.builder() + .tokens(tokens).vaultId("v1").build(); + com.skyflow.generated.rest.core.ApiClientApiException apiEx = + new com.skyflow.generated.rest.core.ApiClientApiException("Service unavailable", 503, null); + Exception wrapper = new Exception("outer", apiEx); + + List errors = Utils.handleDeleteTokensBatchException(wrapper, batch, 0, 3); + + Assert.assertEquals(3, errors.size()); + Assert.assertEquals(0, errors.get(0).getIndex()); + Assert.assertEquals(1, errors.get(1).getIndex()); + Assert.assertEquals(2, errors.get(2).getIndex()); + Assert.assertEquals(503, errors.get(0).getCode()); + } + + @Test + public void handleDeleteTokensBatchException_restException_stringBody_doesNotThrow() { + List tokens = Collections.singletonList("tok1"); + com.skyflow.generated.rest.resources.flowservice.requests.V1FlowDeleteTokenRequest batch = + com.skyflow.generated.rest.resources.flowservice.requests.V1FlowDeleteTokenRequest.builder() + .tokens(tokens).vaultId("v1").build(); + com.skyflow.generated.rest.core.ApiClientApiException apiEx = + new com.skyflow.generated.rest.core.ApiClientApiException("Forbidden", 403, "string error body"); + Exception wrapper = new Exception("outer", apiEx); + + List errors = Utils.handleDeleteTokensBatchException(wrapper, batch, 0, 1); + + Assert.assertEquals(1, errors.size()); + Assert.assertEquals(403, errors.get(0).getCode()); + } + + @Test + public void handleDeleteTokensBatchException_restException_errorFieldIsString_usesStringAsMessage() { + List tokens = Arrays.asList("tok1", "tok2"); + com.skyflow.generated.rest.resources.flowservice.requests.V1FlowDeleteTokenRequest batch = + com.skyflow.generated.rest.resources.flowservice.requests.V1FlowDeleteTokenRequest.builder() + .tokens(tokens).vaultId("v1").build(); + Map body = new HashMap<>(); + body.put("error", "quota exceeded"); + com.skyflow.generated.rest.core.ApiClientApiException apiEx = + new com.skyflow.generated.rest.core.ApiClientApiException("TooManyRequests", 429, body); + Exception wrapper = new Exception("outer", apiEx); + + List errors = Utils.handleDeleteTokensBatchException(wrapper, batch, 0, 2); + + Assert.assertEquals(2, errors.size()); + Assert.assertEquals("quota exceeded", errors.get(0).getError()); + Assert.assertEquals(429, errors.get(0).getCode()); + } + + // ── handleTokenizeBatchException — rest exception + null body ───────────── + + @Test + public void handleTokenizeBatchException_restException_nullBody_createsOneErrorPerDataItem() { + com.skyflow.generated.rest.resources.flowservice.requests.V1FlowTokenizeRequest batch = + com.skyflow.generated.rest.resources.flowservice.requests.V1FlowTokenizeRequest.builder() + .vaultId("v1") + .data(Arrays.asList( + com.skyflow.generated.rest.types.V1FlowTokenizeRequestObject.builder().value("v1").build(), + com.skyflow.generated.rest.types.V1FlowTokenizeRequestObject.builder().value("v2").build())) + .build(); + com.skyflow.generated.rest.core.ApiClientApiException apiEx = + new com.skyflow.generated.rest.core.ApiClientApiException("Gateway timeout", 504, null); + Exception wrapper = new Exception("outer", apiEx); + + List errors = Utils.handleTokenizeBatchException(wrapper, batch, 1, 2); + + Assert.assertEquals(2, errors.size()); + Assert.assertEquals(2, errors.get(0).getIndex()); // batchNumber(1) * batchSize(2) = 2 + Assert.assertEquals(3, errors.get(1).getIndex()); + Assert.assertEquals(504, errors.get(0).getCode()); + } + + @Test + public void handleTokenizeBatchException_restException_stringBody_doesNotThrow() { + com.skyflow.generated.rest.resources.flowservice.requests.V1FlowTokenizeRequest batch = + com.skyflow.generated.rest.resources.flowservice.requests.V1FlowTokenizeRequest.builder() + .vaultId("v1") + .data(Collections.singletonList( + com.skyflow.generated.rest.types.V1FlowTokenizeRequestObject.builder().value("val").build())) + .build(); + com.skyflow.generated.rest.core.ApiClientApiException apiEx = + new com.skyflow.generated.rest.core.ApiClientApiException("Unauthorized", 401, "raw string"); + Exception wrapper = new Exception("outer", apiEx); + + List errors = Utils.handleTokenizeBatchException(wrapper, batch, 0, 1); + + Assert.assertEquals(1, errors.size()); + Assert.assertEquals(401, errors.get(0).getCode()); + } + + @Test + public void handleTokenizeBatchException_restException_errorFieldIsString_usesStringAsMessage() { + com.skyflow.generated.rest.resources.flowservice.requests.V1FlowTokenizeRequest batch = + com.skyflow.generated.rest.resources.flowservice.requests.V1FlowTokenizeRequest.builder() + .vaultId("v1") + .data(Arrays.asList( + com.skyflow.generated.rest.types.V1FlowTokenizeRequestObject.builder().value("a").build(), + com.skyflow.generated.rest.types.V1FlowTokenizeRequestObject.builder().value("b").build())) + .build(); + Map body = new HashMap<>(); + body.put("error", "vault is locked"); + com.skyflow.generated.rest.core.ApiClientApiException apiEx = + new com.skyflow.generated.rest.core.ApiClientApiException("Locked", 423, body); + Exception wrapper = new Exception("outer", apiEx); + + List errors = Utils.handleTokenizeBatchException(wrapper, batch, 0, 2); + + Assert.assertEquals(2, errors.size()); + Assert.assertEquals("vault is locked", errors.get(0).getError()); + Assert.assertEquals(423, errors.get(0).getCode()); + } + + // ── formatDeleteTokensResponse — absent tokens Optional ─────────────────── + + @Test + public void formatDeleteTokensResponse_absentTokensOptional_returnsNull() { + com.skyflow.generated.rest.types.V1FlowDeleteTokenResponse response = + com.skyflow.generated.rest.types.V1FlowDeleteTokenResponse.builder().build(); + + DeleteTokensResponse result = Utils.formatDeleteTokensResponse(response, 0, 10); + + Assert.assertNull(result); + } + + @Test + public void formatDeleteTokensResponse_nullResponse_returnsNull() { + Assert.assertNull(Utils.formatDeleteTokensResponse(null, 0, 10)); + } + + // ── formatDetokenizeResponse — absent response Optional ─────────────────── + + @Test + public void formatDetokenizeResponse_absentResponseOptional_returnsNull() { + com.skyflow.generated.rest.types.V1FlowDetokenizeResponse response = + com.skyflow.generated.rest.types.V1FlowDetokenizeResponse.builder().build(); + + DetokenizeResponse result = Utils.formatDetokenizeResponse(response, 0, 10); + + Assert.assertNull(result); + } + + // ── formatTokenizeResponse — null response ──────────────────────────────── + + @Test + public void formatTokenizeResponse_nullResponse_returnsNull() { + com.skyflow.generated.rest.resources.flowservice.requests.V1FlowTokenizeRequest batchRequest = + com.skyflow.generated.rest.resources.flowservice.requests.V1FlowTokenizeRequest.builder() + .vaultId("v1") + .data(Collections.singletonList( + com.skyflow.generated.rest.types.V1FlowTokenizeRequestObject.builder().value("v").build())) + .build(); + + TokenizeResponse result = Utils.formatTokenizeResponse(null, batchRequest, 0, 1); + + Assert.assertNull(result); + } + + @Test + public void formatTokenizeResponse_responseOptionalAbsent_returnsNull() { + com.skyflow.generated.rest.resources.flowservice.requests.V1FlowTokenizeRequest batchRequest = + com.skyflow.generated.rest.resources.flowservice.requests.V1FlowTokenizeRequest.builder() + .vaultId("v1") + .data(Collections.singletonList( + com.skyflow.generated.rest.types.V1FlowTokenizeRequestObject.builder().value("v").build())) + .build(); + com.skyflow.generated.rest.types.V1FlowTokenizeResponse response = + com.skyflow.generated.rest.types.V1FlowTokenizeResponse.builder().build(); + + TokenizeResponse result = Utils.formatTokenizeResponse(response, batchRequest, 0, 1); + + Assert.assertNull(result); + } + private DetokenizeResponseObject createResponseObject(String token, String value, String groupName, String error, Integer httpCode) { DetokenizeResponseObject responseObject = new DetokenizeResponseObject( 0, @@ -909,6 +1250,19 @@ private DetokenizeResponseObject createResponseObject(String token, String value return responseObject; } + private static Response buildOkHttpResponse(int code, String requestId) { + Request request = new Request.Builder().url("https://example.com").build(); + Response.Builder builder = new Response.Builder() + .request(request) + .protocol(Protocol.HTTP_1_1) + .code(code) + .message("Test"); + if (requestId != null) { + builder.header("x-request-id", requestId); + } + return builder.build(); + } + @Test public void testCreateErrorRecordWithHttpCodeKey() { Map recordMap = new HashMap<>(); @@ -1214,4 +1568,647 @@ public void testGetEnvVaultURLInvalidFormat() { } } } + + // ── createErrorRecord with requestId ───────────────────────────────────── + + @Test + public void testCreateErrorRecordWithRequestId() { + Map recordMap = new HashMap<>(); + recordMap.put("error", "Unauthorized"); + recordMap.put("http_code", 401); + + ErrorRecord err = Utils.createErrorRecord(recordMap, 2, "req-id-abc"); + + Assert.assertEquals(2, err.getIndex()); + Assert.assertEquals("Unauthorized", err.getError()); + Assert.assertEquals(401, err.getCode()); + Assert.assertEquals("req-id-abc", err.getRequestId()); + } + + @Test + public void testCreateErrorRecordLegacyOverload_requestIdIsNull() { + Map recordMap = new HashMap<>(); + recordMap.put("message", "Server error"); + recordMap.put("http_code", 500); + + ErrorRecord err = Utils.createErrorRecord(recordMap, 0); + + Assert.assertNull(err.getRequestId()); + } + + @Test + public void testCreateErrorRecordWithNullRequestId() { + Map recordMap = new HashMap<>(); + recordMap.put("error", "Not found"); + recordMap.put("http_code", 404); + + ErrorRecord err = Utils.createErrorRecord(recordMap, 1, null); + + Assert.assertNull(err.getRequestId()); + Assert.assertEquals("Not found", err.getError()); + } + + // ── handleBatchException with x-request-id ──────────────────────────────── + + @Test + public void testHandleBatchExceptionRequestIdExtractedFromHeaders() { + List batch = Collections.singletonList(V1InsertRecordData.builder().build()); + Map errorMap = new HashMap<>(); + errorMap.put("message", "Unauthorized"); + errorMap.put("http_code", 401); + Map responseBody = new HashMap<>(); + responseBody.put("error", errorMap); + + Response okResponse = buildOkHttpResponse(401, "insert-req-id-123"); + com.skyflow.generated.rest.core.ApiClientApiException apiException = + new com.skyflow.generated.rest.core.ApiClientApiException("Unauthorized", 401, responseBody, okResponse); + Exception exception = new Exception("Outer", apiException); + + List errors = Utils.handleBatchException(exception, batch, 0, 1); + + Assert.assertEquals(1, errors.size()); + Assert.assertEquals("Unauthorized", errors.get(0).getError()); + Assert.assertEquals(401, errors.get(0).getCode()); + Assert.assertEquals("insert-req-id-123", errors.get(0).getRequestId()); + } + + @Test + public void testHandleBatchExceptionWithRecordsList_requestIdPropagated() { + List batch = Arrays.asList( + V1InsertRecordData.builder().build(), V1InsertRecordData.builder().build()); + Map rec1 = new HashMap<>(); + rec1.put("error", "Err1"); + rec1.put("http_code", 400); + Map rec2 = new HashMap<>(); + rec2.put("error", "Err2"); + rec2.put("http_code", 422); + Map responseBody = new HashMap<>(); + responseBody.put("records", Arrays.asList(rec1, rec2)); + + Response okResponse = buildOkHttpResponse(400, "batch-req-id-456"); + com.skyflow.generated.rest.core.ApiClientApiException apiException = + new com.skyflow.generated.rest.core.ApiClientApiException("Error", 400, responseBody, okResponse); + Exception exception = new Exception("Outer", apiException); + + List errors = Utils.handleBatchException(exception, batch, 0, 2); + + Assert.assertEquals(2, errors.size()); + Assert.assertEquals("batch-req-id-456", errors.get(0).getRequestId()); + Assert.assertEquals("batch-req-id-456", errors.get(1).getRequestId()); + } + + @Test + public void testHandleBatchExceptionNoRequestIdHeader_requestIdIsNull() { + List batch = Collections.singletonList(V1InsertRecordData.builder().build()); + Map errorMap = new HashMap<>(); + errorMap.put("message", "Forbidden"); + errorMap.put("http_code", 403); + Map responseBody = new HashMap<>(); + responseBody.put("error", errorMap); + + Response okResponse = buildOkHttpResponse(403, null); + com.skyflow.generated.rest.core.ApiClientApiException apiException = + new com.skyflow.generated.rest.core.ApiClientApiException("Forbidden", 403, responseBody, okResponse); + Exception exception = new Exception("Outer", apiException); + + List errors = Utils.handleBatchException(exception, batch, 0, 1); + + Assert.assertEquals(1, errors.size()); + Assert.assertNull(errors.get(0).getRequestId()); + } + + // ── handleDetokenizeBatchException with x-request-id ───────────────────── + + @Test + public void testHandleDetokenizeBatchExceptionRequestIdExtracted() { + List tokens = Arrays.asList("t1", "t2"); + com.skyflow.generated.rest.resources.flowservice.requests.V1FlowDetokenizeRequest batch = + com.skyflow.generated.rest.resources.flowservice.requests.V1FlowDetokenizeRequest.builder() + .tokens(tokens).vaultId("v1").build(); + + Map errorBody = new HashMap<>(); + errorBody.put("error", "invalid token"); + errorBody.put("http_code", 400); + Map responseBody = new HashMap<>(); + responseBody.put("error", errorBody); + + Response okResponse = buildOkHttpResponse(400, "detok-req-id-789"); + com.skyflow.generated.rest.core.ApiClientApiException apiException = + new com.skyflow.generated.rest.core.ApiClientApiException("Error", 400, responseBody, okResponse); + Exception exception = new Exception("Outer", apiException); + + List errors = Utils.handleDetokenizeBatchException(exception, batch, 0, 2); + + Assert.assertEquals(2, errors.size()); + Assert.assertEquals("detok-req-id-789", errors.get(0).getRequestId()); + Assert.assertEquals("detok-req-id-789", errors.get(1).getRequestId()); + } + + @Test + public void testHandleDetokenizeBatchExceptionNoRequestIdHeader_isNull() { + List tokens = Collections.singletonList("t1"); + com.skyflow.generated.rest.resources.flowservice.requests.V1FlowDetokenizeRequest batch = + com.skyflow.generated.rest.resources.flowservice.requests.V1FlowDetokenizeRequest.builder() + .tokens(tokens).vaultId("v1").build(); + + Map errorBody = new HashMap<>(); + errorBody.put("error", "invalid token"); + errorBody.put("http_code", 400); + Map responseBody = new HashMap<>(); + responseBody.put("error", errorBody); + + Response okResponse = buildOkHttpResponse(400, null); + com.skyflow.generated.rest.core.ApiClientApiException apiException = + new com.skyflow.generated.rest.core.ApiClientApiException("Error", 400, responseBody, okResponse); + Exception exception = new Exception("Outer", apiException); + + List errors = Utils.handleDetokenizeBatchException(exception, batch, 0, 1); + + Assert.assertEquals(1, errors.size()); + Assert.assertNull(errors.get(0).getRequestId()); + } + + // ── handleDeleteTokensBatchException ───────────────────────────────────── + + @Test + public void testHandleDeleteTokensBatchExceptionWithTokensList() { + List tokens = Arrays.asList("tok1", "tok2"); + com.skyflow.generated.rest.resources.flowservice.requests.V1FlowDeleteTokenRequest batch = + com.skyflow.generated.rest.resources.flowservice.requests.V1FlowDeleteTokenRequest.builder() + .tokens(tokens).vaultId("v1").build(); + + Map rec1 = new HashMap<>(); + rec1.put("error", "Token expired"); + rec1.put("http_code", 400); + Map rec2 = new HashMap<>(); + rec2.put("message", "Token not found"); + rec2.put("statusCode", 404); + Map responseBody = new HashMap<>(); + responseBody.put("tokens", Arrays.asList(rec1, rec2)); + + com.skyflow.generated.rest.core.ApiClientApiException apiException = + new com.skyflow.generated.rest.core.ApiClientApiException("Error", 400, responseBody); + Exception exception = new Exception("Outer", apiException); + + List errors = Utils.handleDeleteTokensBatchException(exception, batch, 0, 2); + + Assert.assertEquals(2, errors.size()); + Assert.assertEquals("Token expired", errors.get(0).getError()); + Assert.assertEquals(400, errors.get(0).getCode()); + Assert.assertEquals(0, errors.get(0).getIndex()); + Assert.assertEquals("Token not found", errors.get(1).getError()); + Assert.assertEquals(404, errors.get(1).getCode()); + } + + @Test + public void testHandleDeleteTokensBatchExceptionWithErrorField() { + List tokens = Arrays.asList("tok1", "tok2"); + com.skyflow.generated.rest.resources.flowservice.requests.V1FlowDeleteTokenRequest batch = + com.skyflow.generated.rest.resources.flowservice.requests.V1FlowDeleteTokenRequest.builder() + .tokens(tokens).vaultId("v1").build(); + + Map errorMap = new HashMap<>(); + errorMap.put("error", "Unauthorized"); + errorMap.put("http_code", 401); + Map responseBody = new HashMap<>(); + responseBody.put("error", errorMap); + + com.skyflow.generated.rest.core.ApiClientApiException apiException = + new com.skyflow.generated.rest.core.ApiClientApiException("Error", 401, responseBody); + Exception exception = new Exception("Outer", apiException); + + List errors = Utils.handleDeleteTokensBatchException(exception, batch, 1, 2); + + Assert.assertEquals(2, errors.size()); + Assert.assertEquals(2, errors.get(0).getIndex()); + Assert.assertEquals("Unauthorized", errors.get(0).getError()); + Assert.assertEquals(401, errors.get(0).getCode()); + } + + @Test + public void testHandleDeleteTokensBatchExceptionWithNonApiCause() { + List tokens = Arrays.asList("tok1", "tok2"); + com.skyflow.generated.rest.resources.flowservice.requests.V1FlowDeleteTokenRequest batch = + com.skyflow.generated.rest.resources.flowservice.requests.V1FlowDeleteTokenRequest.builder() + .tokens(tokens).vaultId("v1").build(); + + RuntimeException exception = new RuntimeException("network failure"); + + List errors = Utils.handleDeleteTokensBatchException(exception, batch, 0, 2); + + Assert.assertEquals(2, errors.size()); + Assert.assertEquals("network failure", errors.get(0).getError()); + Assert.assertEquals(500, errors.get(0).getCode()); + Assert.assertEquals(0, errors.get(0).getIndex()); + Assert.assertEquals(1, errors.get(1).getIndex()); + } + + @Test + public void testHandleDeleteTokensBatchExceptionRequestIdExtracted() { + List tokens = Collections.singletonList("tok1"); + com.skyflow.generated.rest.resources.flowservice.requests.V1FlowDeleteTokenRequest batch = + com.skyflow.generated.rest.resources.flowservice.requests.V1FlowDeleteTokenRequest.builder() + .tokens(tokens).vaultId("v1").build(); + + Map errorMap = new HashMap<>(); + errorMap.put("error", "Forbidden"); + errorMap.put("http_code", 403); + Map responseBody = new HashMap<>(); + responseBody.put("error", errorMap); + + Response okResponse = buildOkHttpResponse(403, "del-req-id-111"); + com.skyflow.generated.rest.core.ApiClientApiException apiException = + new com.skyflow.generated.rest.core.ApiClientApiException("Error", 403, responseBody, okResponse); + Exception exception = new Exception("Outer", apiException); + + List errors = Utils.handleDeleteTokensBatchException(exception, batch, 0, 1); + + Assert.assertEquals(1, errors.size()); + Assert.assertEquals("del-req-id-111", errors.get(0).getRequestId()); + } + + // ── handleTokenizeBatchException ───────────────────────────────────────── + + @Test + public void testHandleTokenizeBatchExceptionWithResponseList() { + com.skyflow.generated.rest.resources.flowservice.requests.V1FlowTokenizeRequest batch = + com.skyflow.generated.rest.resources.flowservice.requests.V1FlowTokenizeRequest.builder() + .vaultId("v1") + .data(Arrays.asList( + com.skyflow.generated.rest.types.V1FlowTokenizeRequestObject.builder().value("val1").build(), + com.skyflow.generated.rest.types.V1FlowTokenizeRequestObject.builder().value("val2").build())) + .build(); + + Map rec1 = new HashMap<>(); + rec1.put("error", "Error A"); + rec1.put("http_code", 400); + Map rec2 = new HashMap<>(); + rec2.put("message", "Error B"); + rec2.put("statusCode", 422); + Map responseBody = new HashMap<>(); + responseBody.put("response", Arrays.asList(rec1, rec2)); + + com.skyflow.generated.rest.core.ApiClientApiException apiException = + new com.skyflow.generated.rest.core.ApiClientApiException("Error", 400, responseBody); + Exception exception = new Exception("Outer", apiException); + + List errors = Utils.handleTokenizeBatchException(exception, batch, 0, 2); + + Assert.assertEquals(2, errors.size()); + Assert.assertEquals("Error A", errors.get(0).getError()); + Assert.assertEquals(400, errors.get(0).getCode()); + Assert.assertEquals("Error B", errors.get(1).getError()); + Assert.assertEquals(422, errors.get(1).getCode()); + } + + @Test + public void testHandleTokenizeBatchExceptionWithErrorField() { + com.skyflow.generated.rest.resources.flowservice.requests.V1FlowTokenizeRequest batch = + com.skyflow.generated.rest.resources.flowservice.requests.V1FlowTokenizeRequest.builder() + .vaultId("v1") + .data(Arrays.asList( + com.skyflow.generated.rest.types.V1FlowTokenizeRequestObject.builder().value("v1").build(), + com.skyflow.generated.rest.types.V1FlowTokenizeRequestObject.builder().value("v2").build())) + .build(); + + Map errorMap = new HashMap<>(); + errorMap.put("error", "Unauthorized"); + errorMap.put("http_code", 401); + Map responseBody = new HashMap<>(); + responseBody.put("error", errorMap); + + com.skyflow.generated.rest.core.ApiClientApiException apiException = + new com.skyflow.generated.rest.core.ApiClientApiException("Error", 401, responseBody); + Exception exception = new Exception("Outer", apiException); + + List errors = Utils.handleTokenizeBatchException(exception, batch, 1, 2); + + Assert.assertEquals(2, errors.size()); + Assert.assertEquals(2, errors.get(0).getIndex()); + Assert.assertEquals("Unauthorized", errors.get(0).getError()); + } + + @Test + public void testHandleTokenizeBatchExceptionWithNonApiCause() { + com.skyflow.generated.rest.resources.flowservice.requests.V1FlowTokenizeRequest batch = + com.skyflow.generated.rest.resources.flowservice.requests.V1FlowTokenizeRequest.builder() + .vaultId("v1") + .data(Collections.singletonList( + com.skyflow.generated.rest.types.V1FlowTokenizeRequestObject.builder().value("val").build())) + .build(); + + RuntimeException exception = new RuntimeException("connection refused"); + + List errors = Utils.handleTokenizeBatchException(exception, batch, 0, 1); + + Assert.assertEquals(1, errors.size()); + Assert.assertEquals("connection refused", errors.get(0).getError()); + Assert.assertEquals(500, errors.get(0).getCode()); + Assert.assertNull(errors.get(0).getRequestId()); + } + + @Test + public void testHandleTokenizeBatchExceptionRequestIdExtracted() { + com.skyflow.generated.rest.resources.flowservice.requests.V1FlowTokenizeRequest batch = + com.skyflow.generated.rest.resources.flowservice.requests.V1FlowTokenizeRequest.builder() + .vaultId("v1") + .data(Collections.singletonList( + com.skyflow.generated.rest.types.V1FlowTokenizeRequestObject.builder().value("val").build())) + .build(); + + Map errorMap = new HashMap<>(); + errorMap.put("error", "Quota exceeded"); + errorMap.put("http_code", 429); + Map responseBody = new HashMap<>(); + responseBody.put("error", errorMap); + + Response okResponse = buildOkHttpResponse(429, "tok-req-id-222"); + com.skyflow.generated.rest.core.ApiClientApiException apiException = + new com.skyflow.generated.rest.core.ApiClientApiException("Error", 429, responseBody, okResponse); + Exception exception = new Exception("Outer", apiException); + + List errors = Utils.handleTokenizeBatchException(exception, batch, 0, 1); + + Assert.assertEquals(1, errors.size()); + Assert.assertEquals("tok-req-id-222", errors.get(0).getRequestId()); + } + + // ── formatResponse with headers ─────────────────────────────────────────── + + @Test + public void testFormatResponseWithRequestIdFromHeaders() { + V1RecordResponseObject errorRecord = V1RecordResponseObject.builder() + .error(Optional.of("Duplicate record")) + .httpCode(Optional.of(409)) + .build(); + V1InsertResponse response = V1InsertResponse.builder() + .records(Optional.of(Collections.singletonList(errorRecord))) + .build(); + + Map> headers = new HashMap<>(); + headers.put("x-request-id", Collections.singletonList("ins-req-id-333")); + + com.skyflow.vault.data.InsertResponse result = Utils.formatResponse(response, 0, 1, headers); + + Assert.assertNotNull(result); + Assert.assertEquals(1, result.getErrors().size()); + Assert.assertEquals("Duplicate record", result.getErrors().get(0).getError()); + Assert.assertEquals("ins-req-id-333", result.getErrors().get(0).getRequestId()); + } + + @Test + public void testFormatResponseNullHeaders_requestIdIsNull() { + V1RecordResponseObject errorRecord = V1RecordResponseObject.builder() + .error(Optional.of("Not found")) + .httpCode(Optional.of(404)) + .build(); + V1InsertResponse response = V1InsertResponse.builder() + .records(Optional.of(Collections.singletonList(errorRecord))) + .build(); + + com.skyflow.vault.data.InsertResponse result = Utils.formatResponse(response, 0, 1, null); + + Assert.assertNotNull(result); + Assert.assertEquals(1, result.getErrors().size()); + Assert.assertNull(result.getErrors().get(0).getRequestId()); + } + + // ── formatDetokenizeResponse with headers ───────────────────────────────── + + @Test + public void testFormatDetokenizeResponseWithRequestIdFromHeaders() { + com.skyflow.generated.rest.types.V1FlowDetokenizeResponseObject errorObj = + com.skyflow.generated.rest.types.V1FlowDetokenizeResponseObject.builder() + .error("Token invalid").httpCode(400).build(); + com.skyflow.generated.rest.types.V1FlowDetokenizeResponse response = + com.skyflow.generated.rest.types.V1FlowDetokenizeResponse.builder() + .response(Optional.of(Collections.singletonList(errorObj))).build(); + + Map> headers = new HashMap<>(); + headers.put("x-request-id", Collections.singletonList("det-req-id-444")); + + DetokenizeResponse result = Utils.formatDetokenizeResponse(response, 0, 1, headers); + + Assert.assertNotNull(result); + Assert.assertEquals(1, result.getErrors().size()); + Assert.assertEquals("Token invalid", result.getErrors().get(0).getError()); + Assert.assertEquals("det-req-id-444", result.getErrors().get(0).getRequestId()); + } + + @Test + public void testFormatDetokenizeResponseNullHeaders_requestIdIsNull() { + com.skyflow.generated.rest.types.V1FlowDetokenizeResponseObject errorObj = + com.skyflow.generated.rest.types.V1FlowDetokenizeResponseObject.builder() + .error("Token invalid").httpCode(400).build(); + com.skyflow.generated.rest.types.V1FlowDetokenizeResponse response = + com.skyflow.generated.rest.types.V1FlowDetokenizeResponse.builder() + .response(Optional.of(Collections.singletonList(errorObj))).build(); + + DetokenizeResponse result = Utils.formatDetokenizeResponse(response, 0, 1, null); + + Assert.assertNotNull(result); + Assert.assertNull(result.getErrors().get(0).getRequestId()); + } + + // ── formatDeleteTokensResponse with headers ─────────────────────────────── + + @Test + public void testFormatDeleteTokensResponseWithRequestIdFromHeaders() { + V1DeleteTokenResponseObject errorRecord = V1DeleteTokenResponseObject.builder() + .value("tok-abc") + .error("Token expired") + .httpCode(400) + .build(); + V1FlowDeleteTokenResponse response = V1FlowDeleteTokenResponse.builder() + .tokens(Collections.singletonList(errorRecord)) + .build(); + + Map> headers = new HashMap<>(); + headers.put("x-request-id", Collections.singletonList("del-req-id-555")); + + DeleteTokensResponse result = Utils.formatDeleteTokensResponse(response, 0, 1, headers); + + Assert.assertNotNull(result); + Assert.assertEquals(1, result.getErrors().size()); + Assert.assertEquals("Token expired", result.getErrors().get(0).getError()); + Assert.assertEquals("del-req-id-555", result.getErrors().get(0).getRequestId()); + } + + @Test + public void testFormatDeleteTokensResponseNullHeaders_requestIdIsNull() { + V1DeleteTokenResponseObject errorRecord = V1DeleteTokenResponseObject.builder() + .value("tok-abc") + .error("Token expired") + .httpCode(400) + .build(); + V1FlowDeleteTokenResponse response = V1FlowDeleteTokenResponse.builder() + .tokens(Collections.singletonList(errorRecord)) + .build(); + + DeleteTokensResponse result = Utils.formatDeleteTokensResponse(response, 0, 1, null); + + Assert.assertNotNull(result); + Assert.assertNull(result.getErrors().get(0).getRequestId()); + } + + @Test + public void testFormatDeleteTokensResponseSuccessRecord_noRequestId() { + V1DeleteTokenResponseObject successRecord = V1DeleteTokenResponseObject.builder() + .value("tok-success") + .build(); + V1FlowDeleteTokenResponse response = V1FlowDeleteTokenResponse.builder() + .tokens(Collections.singletonList(successRecord)) + .build(); + + Map> headers = new HashMap<>(); + headers.put("x-request-id", Collections.singletonList("req-id-xyz")); + + DeleteTokensResponse result = Utils.formatDeleteTokensResponse(response, 0, 1, headers); + + Assert.assertNotNull(result); + Assert.assertEquals(1, result.getSuccess().size()); + Assert.assertEquals(0, result.getErrors().size()); + } + + // ── formatTokenizeResponse with headers ─────────────────────────────────── + + @Test + public void testFormatTokenizeResponseWithRequestIdFromHeaders() throws Exception { + com.skyflow.generated.rest.resources.flowservice.requests.V1FlowTokenizeRequest batchRequest = + com.skyflow.generated.rest.resources.flowservice.requests.V1FlowTokenizeRequest.builder() + .vaultId("v1") + .data(Collections.singletonList( + com.skyflow.generated.rest.types.V1FlowTokenizeRequestObject.builder() + .value("sensitive").build())) + .build(); + + String json = "{\"error\":\"Tokenize failed\",\"httpCode\":403}"; + V1FlowTokenizeResponseObject errorObj = new ObjectMapper() + .readValue(json, V1FlowTokenizeResponseObject.class); + + com.skyflow.generated.rest.types.V1FlowTokenizeResponse response = + com.skyflow.generated.rest.types.V1FlowTokenizeResponse.builder() + .response(Optional.of(Collections.singletonList(errorObj))).build(); + + Map> headers = new HashMap<>(); + headers.put("x-request-id", Collections.singletonList("tok-req-id-666")); + + TokenizeResponse result = Utils.formatTokenizeResponse(response, batchRequest, 0, 1, headers); + + Assert.assertNotNull(result); + Assert.assertEquals(1, result.getErrors().size()); + Assert.assertEquals("Tokenize failed", result.getErrors().get(0).getError()); + Assert.assertEquals("tok-req-id-666", result.getErrors().get(0).getRequestId()); + } + + @Test + public void testFormatTokenizeResponseNullHeaders_requestIdIsNull() throws Exception { + com.skyflow.generated.rest.resources.flowservice.requests.V1FlowTokenizeRequest batchRequest = + com.skyflow.generated.rest.resources.flowservice.requests.V1FlowTokenizeRequest.builder() + .vaultId("v1") + .data(Collections.singletonList( + com.skyflow.generated.rest.types.V1FlowTokenizeRequestObject.builder() + .value("data").build())) + .build(); + + String json = "{\"error\":\"Quota exceeded\",\"httpCode\":429}"; + V1FlowTokenizeResponseObject errorObj = new ObjectMapper() + .readValue(json, V1FlowTokenizeResponseObject.class); + + com.skyflow.generated.rest.types.V1FlowTokenizeResponse response = + com.skyflow.generated.rest.types.V1FlowTokenizeResponse.builder() + .response(Optional.of(Collections.singletonList(errorObj))).build(); + + TokenizeResponse result = Utils.formatTokenizeResponse(response, batchRequest, 0, 1, null); + + Assert.assertNotNull(result); + Assert.assertEquals(1, result.getErrors().size()); + Assert.assertNull(result.getErrors().get(0).getRequestId()); + } + + // ── formatTokenizeResponse — success path ──────────────────────────────── + + @Test + public void testFormatTokenizeResponseSuccessPath_returnsSuccessRecord() throws Exception { + com.skyflow.generated.rest.resources.flowservice.requests.V1FlowTokenizeRequest batchRequest = + com.skyflow.generated.rest.resources.flowservice.requests.V1FlowTokenizeRequest.builder() + .vaultId("v1") + .data(Collections.singletonList( + com.skyflow.generated.rest.types.V1FlowTokenizeRequestObject.builder() + .value("sensitive-value").build())) + .build(); + + // No "error" key → success path + String json = "{\"token\":\"tok-abc\"}"; + V1FlowTokenizeResponseObject successObj = new ObjectMapper() + .readValue(json, V1FlowTokenizeResponseObject.class); + + com.skyflow.generated.rest.types.V1FlowTokenizeResponse response = + com.skyflow.generated.rest.types.V1FlowTokenizeResponse.builder() + .response(Optional.of(Collections.singletonList(successObj))).build(); + + TokenizeResponse result = Utils.formatTokenizeResponse(response, batchRequest, 0, 1, null); + + Assert.assertNotNull(result); + Assert.assertEquals(0, result.getErrors().size()); + Assert.assertEquals(1, result.getSuccess().size()); + Assert.assertEquals(0, result.getSuccess().get(0).getIndex()); + Assert.assertEquals("tok-abc", result.getSuccess().get(0).getTokens().get(null)); + } + + @Test + public void testFormatTokenizeResponseWithTokenGroupNames_multiGroupConsumed() throws Exception { + // Request with one data item having 2 tokenGroupNames → consumes 2 response entries + com.skyflow.generated.rest.types.V1FlowTokenizeRequestObject reqObj = + com.skyflow.generated.rest.types.V1FlowTokenizeRequestObject.builder() + .value("sensitive") + .tokenGroupNames(Arrays.asList("group1", "group2")) + .build(); + com.skyflow.generated.rest.resources.flowservice.requests.V1FlowTokenizeRequest batchRequest = + com.skyflow.generated.rest.resources.flowservice.requests.V1FlowTokenizeRequest.builder() + .vaultId("v1") + .data(Collections.singletonList(reqObj)) + .build(); + + V1FlowTokenizeResponseObject resp1 = new ObjectMapper() + .readValue("{\"token\":\"tok1\",\"tokenGroupName\":\"group1\"}", V1FlowTokenizeResponseObject.class); + V1FlowTokenizeResponseObject resp2 = new ObjectMapper() + .readValue("{\"token\":\"tok2\",\"tokenGroupName\":\"group2\"}", V1FlowTokenizeResponseObject.class); + + com.skyflow.generated.rest.types.V1FlowTokenizeResponse response = + com.skyflow.generated.rest.types.V1FlowTokenizeResponse.builder() + .response(Optional.of(Arrays.asList(resp1, resp2))).build(); + + TokenizeResponse result = Utils.formatTokenizeResponse(response, batchRequest, 0, 1, null); + + Assert.assertNotNull(result); + Assert.assertEquals(1, result.getSuccess().size()); // 1 input record → 1 success entry + Assert.assertEquals(0, result.getErrors().size()); + Map tokens = result.getSuccess().get(0).getTokens(); + Assert.assertEquals("tok1", tokens.get("group1")); + Assert.assertEquals("tok2", tokens.get("group2")); + } + + @Test + public void testFormatTokenizeResponseAbsentBatchData_returnsEmptySuccessAndErrors() throws Exception { + // batchRequest.getData() is absent → requestData defaults to empty list + com.skyflow.generated.rest.resources.flowservice.requests.V1FlowTokenizeRequest batchRequest = + com.skyflow.generated.rest.resources.flowservice.requests.V1FlowTokenizeRequest.builder() + .vaultId("v1") + .build(); // no data set + + String json = "{\"token\":\"tok-abc\"}"; + V1FlowTokenizeResponseObject obj = new ObjectMapper() + .readValue(json, V1FlowTokenizeResponseObject.class); + + com.skyflow.generated.rest.types.V1FlowTokenizeResponse response = + com.skyflow.generated.rest.types.V1FlowTokenizeResponse.builder() + .response(Optional.of(Collections.singletonList(obj))).build(); + + TokenizeResponse result = Utils.formatTokenizeResponse(response, batchRequest, 0, 1, null); + + Assert.assertNotNull(result); + Assert.assertTrue(result.getSuccess().isEmpty()); + Assert.assertTrue(result.getErrors().isEmpty()); + } } \ No newline at end of file diff --git a/v3/src/test/java/com/skyflow/utils/validations/ValidationsTests.java b/v3/src/test/java/com/skyflow/utils/validations/ValidationsTests.java index c6f602c1..78cbb40c 100644 --- a/v3/src/test/java/com/skyflow/utils/validations/ValidationsTests.java +++ b/v3/src/test/java/com/skyflow/utils/validations/ValidationsTests.java @@ -1,6 +1,8 @@ package com.skyflow.utils.validations; import com.skyflow.config.VaultConfig; +import com.skyflow.enums.CustomHeaderKey; +import com.skyflow.enums.InterfaceName; import com.skyflow.errors.ErrorMessage; import com.skyflow.errors.SkyflowException; import com.skyflow.vault.data.DetokenizeRequest; @@ -167,6 +169,13 @@ public void validateDetokenizeRequest_nullRequest_throws() { ErrorMessage.DetokenizeRequestNull.getMessage()); } + @Test + public void validateDetokenizeRequest_nullTokens_throws() { + DetokenizeRequest request = DetokenizeRequest.builder().build(); + assertSkyflowException(() -> Validations.validateDetokenizeRequest(request), + ErrorMessage.EmptyDetokenizeData.getMessage()); + } + @Test public void validateDetokenizeRequest_emptyTokens_throws() { DetokenizeRequest request = DetokenizeRequest.builder() @@ -309,6 +318,121 @@ public void validateVaultConfig_valid_passes() { } } + // ── validateCustomHeaders ───────────────────────────────────────────────── + + @Test + public void validateCustomHeaders_nullMap_passes() { + try { + Validations.validateCustomHeaders(null, InterfaceName.INSERT); + } catch (Exception e) { + Assert.fail("Should not throw for null map: " + e.getMessage()); + } + } + + @Test + public void validateCustomHeaders_emptyMap_passes() { + try { + Validations.validateCustomHeaders(new HashMap<>(), InterfaceName.INSERT); + } catch (Exception e) { + Assert.fail("Should not throw for empty map: " + e.getMessage()); + } + } + + @Test + public void validateCustomHeaders_validEntry_passes() { + Map headers = new HashMap<>(); + headers.put(CustomHeaderKey.SkyflowAccountID, "acct-123"); + try { + Validations.validateCustomHeaders(headers, InterfaceName.INSERT); + } catch (Exception e) { + Assert.fail("Should not throw for valid headers: " + e.getMessage()); + } + } + + @Test + public void validateCustomHeaders_allValidKeys_passes() { + Map headers = new HashMap<>(); + headers.put(CustomHeaderKey.SkyflowAccountID, "acct-123"); + headers.put(CustomHeaderKey.SkyflowAccountName, "my-account"); + headers.put(CustomHeaderKey.RequestIDHeader, "req-abc"); + try { + Validations.validateCustomHeaders(headers, InterfaceName.INSERT); + } catch (Exception e) { + Assert.fail("Should not throw for all valid keys: " + e.getMessage()); + } + } + + @Test + public void validateCustomHeaders_nullKey_throws() { + Map headers = new HashMap<>(); + headers.put(null, "some-value"); + assertSkyflowException( + () -> Validations.validateCustomHeaders(headers, InterfaceName.INSERT), + ErrorMessage.NullCustomHeaderKey.getMessage()); + } + + @Test + public void validateCustomHeaders_nullValue_throws() { + Map headers = new HashMap<>(); + headers.put(CustomHeaderKey.SkyflowAccountID, null); + assertSkyflowException( + () -> Validations.validateCustomHeaders(headers, InterfaceName.INSERT), + ErrorMessage.EmptyValueInCustomHeaders.getMessage()); + } + + @Test + public void validateCustomHeaders_emptyValue_throws() { + Map headers = new HashMap<>(); + headers.put(CustomHeaderKey.RequestIDHeader, ""); + assertSkyflowException( + () -> Validations.validateCustomHeaders(headers, InterfaceName.DETOKENIZE), + ErrorMessage.EmptyValueInCustomHeaders.getMessage()); + } + + @Test + public void validateCustomHeaders_blankValue_throws() { + Map headers = new HashMap<>(); + headers.put(CustomHeaderKey.SkyflowAccountName, " "); + assertSkyflowException( + () -> Validations.validateCustomHeaders(headers, InterfaceName.TOKENIZE), + ErrorMessage.EmptyValueInCustomHeaders.getMessage()); + } + + @Test + public void validateCustomHeaders_stopsAtFirstInvalidEntry() { + Map headers = new HashMap<>(); + headers.put(CustomHeaderKey.SkyflowAccountID, "valid"); + headers.put(CustomHeaderKey.RequestIDHeader, ""); // invalid — empty + headers.put(CustomHeaderKey.SkyflowAccountName, "also-valid"); + assertSkyflowException( + () -> Validations.validateCustomHeaders(headers, InterfaceName.INSERT), + ErrorMessage.EmptyValueInCustomHeaders.getMessage()); + } + + @Test + public void validateCustomHeaders_nullValuePrecedesNullKey_throwsHeaderError() { + Map headers = new HashMap<>(); + headers.put(CustomHeaderKey.SkyflowAccountID, null); // null value + assertSkyflowException( + () -> Validations.validateCustomHeaders(headers, InterfaceName.DETOKENIZE), + ErrorMessage.EmptyValueInCustomHeaders.getMessage()); + } + + @Test + public void validateCustomHeaders_allInterfaceNames_doNotAffectOutcome() { + Map headers = new HashMap<>(); + headers.put(CustomHeaderKey.RequestIDHeader, "r1"); + for (InterfaceName name : new InterfaceName[]{ + InterfaceName.INSERT, InterfaceName.DETOKENIZE, + InterfaceName.TOKENIZE, InterfaceName.DELETE}) { + try { + Validations.validateCustomHeaders(headers, name); + } catch (Exception e) { + Assert.fail("Should not throw for interface " + name + ": " + e.getMessage()); + } + } + } + private interface ThrowingRunnable { void run() throws Exception; } diff --git a/v3/src/test/java/com/skyflow/vault/controller/VaultControllerDeleteTokensTests.java b/v3/src/test/java/com/skyflow/vault/controller/VaultControllerDeleteTokensTests.java index 63221a12..aeffd202 100644 --- a/v3/src/test/java/com/skyflow/vault/controller/VaultControllerDeleteTokensTests.java +++ b/v3/src/test/java/com/skyflow/vault/controller/VaultControllerDeleteTokensTests.java @@ -408,13 +408,13 @@ public void testDeleteTokensBatchFutures_catchBranchAddsErrorRecord() throws Exc List batches = null; Method method = VaultController.class.getDeclaredMethod( - "deleteTokensBatchFutures", ExecutorService.class, List.class); + "deleteTokensBatchFutures", ExecutorService.class, List.class, java.util.Map.class); method.setAccessible(true); ExecutorService executor = Executors.newFixedThreadPool(1); @SuppressWarnings("unchecked") List> futures = - (List>) method.invoke(controller, executor, batches); + (List>) method.invoke(controller, executor, batches, Collections.emptyMap()); // errors are now returned via futures, not a shared list Assert.assertNotNull(futures); @@ -448,12 +448,13 @@ public void testProcessDeleteTokensSyncNormalPath() throws Exception { Method processSync = VaultController.class.getDeclaredMethod( "processDeleteTokensSync", com.skyflow.generated.rest.resources.flowservice.requests.V1FlowDeleteTokenRequest.class, - List.class + List.class, + java.util.Map.class ); processSync.setAccessible(true); try { - processSync.invoke(controller, requestObj, tokens); + processSync.invoke(controller, requestObj, tokens, Collections.emptyMap()); } catch (java.lang.reflect.InvocationTargetException e) { Throwable cause = e.getCause(); assertTrue(cause instanceof SkyflowException diff --git a/v3/src/test/java/com/skyflow/vault/controller/VaultControllerTests.java b/v3/src/test/java/com/skyflow/vault/controller/VaultControllerTests.java index 4e8a5e4a..b9f4663f 100644 --- a/v3/src/test/java/com/skyflow/vault/controller/VaultControllerTests.java +++ b/v3/src/test/java/com/skyflow/vault/controller/VaultControllerTests.java @@ -8,11 +8,21 @@ import com.skyflow.enums.Env; import com.skyflow.utils.Constants; import com.skyflow.utils.validations.Validations; +import com.skyflow.enums.CustomHeaderKey; +import com.skyflow.vault.data.DeleteTokensOptions; +import com.skyflow.vault.data.DeleteTokensRequest; +import com.skyflow.vault.data.DeleteTokensResponse; +import com.skyflow.vault.data.DetokenizeOptions; import com.skyflow.vault.data.DetokenizeRequest; +import com.skyflow.vault.data.DetokenizeResponse; +import com.skyflow.vault.data.InsertOptions; import com.skyflow.vault.data.InsertRecord; import com.skyflow.vault.data.InsertRequest; import com.skyflow.vault.data.ErrorRecord; -import com.skyflow.vault.data.DetokenizeResponse; +import com.skyflow.vault.data.TokenizeOptions; +import com.skyflow.vault.data.TokenizeRecord; +import com.skyflow.vault.data.TokenizeRequest; +import com.skyflow.vault.data.TokenizeResponse; import com.skyflow.generated.rest.core.ApiClientApiException; import com.sun.net.httpserver.HttpServer; import org.junit.After; @@ -27,8 +37,10 @@ import java.net.InetSocketAddress; import java.nio.charset.StandardCharsets; import java.util.ArrayList; +import java.util.Collections; import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; @@ -313,6 +325,68 @@ public void testConcurrencyExceedsMax() throws Exception { assertEquals(1, getPrivateInt(controller, "insertConcurrencyLimit")); } + @Test + public void testNonNumericInsertBatchSize_usesDefault() throws Exception { + writeEnv("INSERT_BATCH_SIZE=abc"); + VaultController controller = createController(); + InsertRequest insertRequest = InsertRequest.builder().table("table1").records(generateValues(10)).build(); + + try { + controller.bulkInsert(insertRequest); + } catch (Exception ignored) { + } + + assertEquals(Constants.INSERT_BATCH_SIZE.intValue(), getPrivateInt(controller, "insertBatchSize")); + } + + @Test + public void testNonNumericInsertConcurrencyLimit_usesDefault() throws Exception { + writeEnv("INSERT_CONCURRENCY_LIMIT=xyz"); + VaultController controller = createController(); + InsertRequest insertRequest = InsertRequest.builder().table("table1").records(generateValues(10)).build(); + + try { + controller.bulkInsert(insertRequest); + } catch (Exception ignored) { + } + + int expected = Math.min(Constants.INSERT_CONCURRENCY_LIMIT, + (10 + Constants.INSERT_BATCH_SIZE - 1) / Constants.INSERT_BATCH_SIZE); + assertEquals(expected, getPrivateInt(controller, "insertConcurrencyLimit")); + } + + @Test + public void testNonNumericDetokenizeBatchSize_usesDefault() throws Exception { + writeEnv("DETOKENIZE_BATCH_SIZE=abc"); + VaultController controller = createController(); + List tokens = getTokens(10); + DetokenizeRequest request = DetokenizeRequest.builder().tokens(tokens).build(); + + try { + controller.bulkDetokenize(request); + } catch (Exception ignored) { + } + + assertEquals(Constants.DETOKENIZE_BATCH_SIZE.intValue(), getPrivateInt(controller, "detokenizeBatchSize")); + } + + @Test + public void testNonNumericDetokenizeConcurrencyLimit_usesDefault() throws Exception { + writeEnv("DETOKENIZE_CONCURRENCY_LIMIT=xyz"); + VaultController controller = createController(); + List tokens = getTokens(10); + DetokenizeRequest request = DetokenizeRequest.builder().tokens(tokens).build(); + + try { + controller.bulkDetokenize(request); + } catch (Exception ignored) { + } + + int expected = Math.min(Constants.DETOKENIZE_CONCURRENCY_LIMIT, + (10 + Constants.DETOKENIZE_BATCH_SIZE - 1) / Constants.DETOKENIZE_BATCH_SIZE); + assertEquals(expected, getPrivateInt(controller, "detokenizeConcurrencyLimit")); + } + @Test public void testBatchSizeZeroOrNegative() throws Exception { writeEnv("INSERT_BATCH_SIZE=0"); @@ -621,12 +695,12 @@ public void testDetokenizeBatchFuturesCatchBranchAddsErrorRecord() throws Except List batches = null; // trigger catch List errors = new ArrayList<>(); - Method method = VaultController.class.getDeclaredMethod("detokenizeBatchFutures", ExecutorService.class, List.class, List.class); + Method method = VaultController.class.getDeclaredMethod("detokenizeBatchFutures", ExecutorService.class, List.class, List.class, Map.class); method.setAccessible(true); ExecutorService executor = Executors.newFixedThreadPool(1); @SuppressWarnings("unchecked") List> futures = - (List>) method.invoke(controller, executor, batches, errors); + (List>) method.invoke(controller, executor, batches, errors, Collections.emptyMap()); Assert.assertTrue(errors.size() == 1); Assert.assertEquals(0, errors.get(0).getIndex()); @@ -672,12 +746,13 @@ public void testProcessDetokenizeSyncNormalPath() throws Exception { java.lang.reflect.Method processDetokenizeSync = VaultController.class.getDeclaredMethod( "processDetokenizeSync", com.skyflow.generated.rest.resources.flowservice.requests.V1FlowDetokenizeRequest.class, - List.class + List.class, + Map.class ); processDetokenizeSync.setAccessible(true); try { - processDetokenizeSync.invoke(controller, requestObj, tokens); + processDetokenizeSync.invoke(controller, requestObj, tokens, Collections.emptyMap()); } catch (java.lang.reflect.InvocationTargetException e) { Throwable cause = e.getCause(); assertTrue(cause instanceof SkyflowException || cause instanceof ExecutionException || cause instanceof RuntimeException); @@ -709,7 +784,8 @@ public void testProcessDetokenizeSyncErrorPath() throws Exception { java.lang.reflect.Method processDetokenizeSync = VaultController.class.getDeclaredMethod( "processDetokenizeSync", com.skyflow.generated.rest.resources.flowservice.requests.V1FlowDetokenizeRequest.class, - List.class + List.class, + Map.class ); processDetokenizeSync.setAccessible(true); @@ -717,17 +793,613 @@ public void testProcessDetokenizeSyncErrorPath() throws Exception { "detokenizeBatchFutures", ExecutorService.class, List.class, - List.class + List.class, + Map.class ); detokenizeBatchFutures.setAccessible(true); ExecutorService executor = Executors.newFixedThreadPool(1); List batches = null; // will trigger catch List errors = new ArrayList<>(); @SuppressWarnings("unchecked") - List> futures = (List>) detokenizeBatchFutures.invoke(controller, executor, batches, errors); + List> futures = (List>) detokenizeBatchFutures.invoke(controller, executor, batches, errors, Collections.emptyMap()); assertTrue(errors.size() == 1); assertEquals(0, errors.get(0).getIndex()); assertEquals(500, errors.get(0).getCode()); executor.shutdownNow(); } + + // ── Custom header options validation via VaultController ────────────────── + + @Test + public void bulkInsert_optionsWithEmptyHeaderValue_throwsSkyflowException() throws SkyflowException { + VaultController controller = createController(); + InsertRequest request = InsertRequest.builder() + .table("tbl") + .records(generateValues(1)) + .build(); + InsertOptions opts = InsertOptions.builder() + .addCustomHeader(CustomHeaderKey.SkyflowAccountID, "") + .build(); + try { + controller.bulkInsert(request, opts); + fail("Expected SkyflowException for empty header value"); + } catch (SkyflowException e) { + assertEquals(ErrorMessage.EmptyValueInCustomHeaders.getMessage(), e.getMessage()); + } + } + + @Test + public void bulkInsert_optionsWithBlankHeaderValue_throwsSkyflowException() throws SkyflowException { + VaultController controller = createController(); + InsertRequest request = InsertRequest.builder() + .table("tbl") + .records(generateValues(1)) + .build(); + InsertOptions opts = InsertOptions.builder() + .addCustomHeader(CustomHeaderKey.RequestIDHeader, " ") + .build(); + try { + controller.bulkInsert(request, opts); + fail("Expected SkyflowException for blank header value"); + } catch (SkyflowException e) { + assertEquals(ErrorMessage.EmptyValueInCustomHeaders.getMessage(), e.getMessage()); + } + } + + @Test + public void bulkInsert_nullOptions_doesNotThrowForHeaderValidation() throws SkyflowException { + VaultController controller = createController(); + InsertRequest request = InsertRequest.builder() + .table("tbl") + .records(generateValues(1)) + .build(); + try { + controller.bulkInsert(request, (InsertOptions) null); + } catch (SkyflowException e) { + assertFalse("Should not throw EmptyValueInCustomHeaders for null options", + e.getMessage().equals(ErrorMessage.EmptyValueInCustomHeaders.getMessage())); + assertFalse("Should not throw NullCustomHeaderKey for null options", + e.getMessage().equals(ErrorMessage.NullCustomHeaderKey.getMessage())); + } + } + + @Test + public void bulkDetokenize_optionsWithEmptyHeaderValue_throwsSkyflowException() throws SkyflowException { + VaultController controller = createController(); + DetokenizeRequest request = DetokenizeRequest.builder() + .tokens(getTokens(1)) + .build(); + DetokenizeOptions opts = DetokenizeOptions.builder() + .addCustomHeader(CustomHeaderKey.SkyflowAccountName, "") + .build(); + try { + controller.bulkDetokenize(request, opts); + fail("Expected SkyflowException for empty header value"); + } catch (SkyflowException e) { + assertEquals(ErrorMessage.EmptyValueInCustomHeaders.getMessage(), e.getMessage()); + } + } + + @Test + public void bulkDetokenize_nullOptions_doesNotThrowForHeaderValidation() throws SkyflowException { + VaultController controller = createController(); + DetokenizeRequest request = DetokenizeRequest.builder() + .tokens(getTokens(1)) + .build(); + try { + controller.bulkDetokenize(request, (DetokenizeOptions) null); + } catch (SkyflowException e) { + assertFalse("Should not throw header error for null options", + e.getMessage().equals(ErrorMessage.EmptyValueInCustomHeaders.getMessage())); + } + } + + @Test + public void bulkDeleteTokens_optionsWithEmptyHeaderValue_throwsSkyflowException() throws SkyflowException { + VaultController controller = createController(); + DeleteTokensRequest request = DeleteTokensRequest.builder() + .tokens(getTokens(1)) + .build(); + DeleteTokensOptions opts = DeleteTokensOptions.builder() + .addCustomHeader(CustomHeaderKey.RequestIDHeader, "") + .build(); + try { + controller.bulkDeleteTokens(request, opts); + fail("Expected SkyflowException for empty header value"); + } catch (SkyflowException e) { + assertEquals(ErrorMessage.EmptyValueInCustomHeaders.getMessage(), e.getMessage()); + } + } + + @Test + public void bulkTokenize_optionsWithEmptyHeaderValue_throwsSkyflowException() throws SkyflowException { + VaultController controller = createController(); + ArrayList data = new ArrayList<>(); + data.add(TokenizeRecord.builder().value("val").build()); + TokenizeRequest request = TokenizeRequest.builder().data(data).build(); + TokenizeOptions opts = TokenizeOptions.builder() + .addCustomHeader(CustomHeaderKey.SkyflowAccountID, "") + .build(); + try { + controller.bulkTokenize(request, opts); + fail("Expected SkyflowException for empty header value"); + } catch (SkyflowException e) { + assertEquals(ErrorMessage.EmptyValueInCustomHeaders.getMessage(), e.getMessage()); + } + } + + @Test + public void bulkDeleteTokens_nullOptions_doesNotThrowForHeaderValidation() throws SkyflowException { + VaultController controller = createController(); + DeleteTokensRequest request = DeleteTokensRequest.builder() + .tokens(getTokens(1)) + .build(); + try { + controller.bulkDeleteTokens(request, (DeleteTokensOptions) null); + } catch (SkyflowException e) { + assertFalse("Should not throw header error for null options", + e.getMessage().equals(ErrorMessage.EmptyValueInCustomHeaders.getMessage())); + assertFalse("Should not throw NullCustomHeaderKey for null options", + e.getMessage().equals(ErrorMessage.NullCustomHeaderKey.getMessage())); + } + } + + @Test + public void bulkTokenize_nullOptions_doesNotThrowForHeaderValidation() throws SkyflowException { + VaultController controller = createController(); + ArrayList data = new ArrayList<>(); + data.add(TokenizeRecord.builder().value("val").build()); + TokenizeRequest request = TokenizeRequest.builder().data(data).build(); + try { + controller.bulkTokenize(request, (TokenizeOptions) null); + } catch (SkyflowException e) { + assertFalse("Should not throw header error for null options", + e.getMessage().equals(ErrorMessage.EmptyValueInCustomHeaders.getMessage())); + assertFalse("Should not throw NullCustomHeaderKey for null options", + e.getMessage().equals(ErrorMessage.NullCustomHeaderKey.getMessage())); + } + } + + @Test + public void bulkInsertAsync_optionsWithEmptyHeaderValue_throwsSkyflowException() throws SkyflowException { + VaultController controller = createController(); + InsertRequest request = InsertRequest.builder() + .table("tbl") + .records(generateValues(1)) + .build(); + InsertOptions opts = InsertOptions.builder() + .addCustomHeader(CustomHeaderKey.SkyflowAccountName, "") + .build(); + try { + controller.bulkInsertAsync(request, opts); + fail("Expected SkyflowException for empty header value"); + } catch (SkyflowException e) { + assertEquals(ErrorMessage.EmptyValueInCustomHeaders.getMessage(), e.getMessage()); + } + } + + @Test + public void bulkDetokenizeAsync_optionsWithEmptyHeaderValue_throwsSkyflowException() throws SkyflowException { + VaultController controller = createController(); + DetokenizeRequest request = DetokenizeRequest.builder() + .tokens(getTokens(1)) + .build(); + DetokenizeOptions opts = DetokenizeOptions.builder() + .addCustomHeader(CustomHeaderKey.RequestIDHeader, " ") + .build(); + try { + controller.bulkDetokenizeAsync(request, opts); + fail("Expected SkyflowException for blank header value"); + } catch (SkyflowException e) { + assertEquals(ErrorMessage.EmptyValueInCustomHeaders.getMessage(), e.getMessage()); + } + } + + @Test + public void bulkDeleteTokensAsync_optionsWithEmptyHeaderValue_throwsSkyflowException() throws SkyflowException { + VaultController controller = createController(); + DeleteTokensRequest request = DeleteTokensRequest.builder() + .tokens(getTokens(1)) + .build(); + DeleteTokensOptions opts = DeleteTokensOptions.builder() + .addCustomHeader(CustomHeaderKey.SkyflowAccountID, "") + .build(); + try { + controller.bulkDeleteTokensAsync(request, opts); + fail("Expected SkyflowException for empty header value"); + } catch (SkyflowException e) { + assertEquals(ErrorMessage.EmptyValueInCustomHeaders.getMessage(), e.getMessage()); + } + } + + @Test + public void bulkTokenizeAsync_optionsWithEmptyHeaderValue_throwsSkyflowException() throws SkyflowException { + VaultController controller = createController(); + ArrayList data = new ArrayList<>(); + data.add(TokenizeRecord.builder().value("val").build()); + TokenizeRequest request = TokenizeRequest.builder().data(data).build(); + TokenizeOptions opts = TokenizeOptions.builder() + .addCustomHeader(CustomHeaderKey.SkyflowAccountName, " ") + .build(); + try { + controller.bulkTokenizeAsync(request, opts); + fail("Expected SkyflowException for blank header value"); + } catch (SkyflowException e) { + assertEquals(ErrorMessage.EmptyValueInCustomHeaders.getMessage(), e.getMessage()); + } + } + + // ── configureDeleteTokensConcurrencyAndBatchSize ────────────────────────── + + @Test + public void testCustomValidBatchAndConcurrency_DELETE_TOKENS() throws Exception { + writeEnv("DELETE_TOKENS_BATCH_SIZE=5\nDELETE_TOKENS_CONCURRENCY_LIMIT=3"); + VaultController controller = createController(); + DeleteTokensRequest request = DeleteTokensRequest.builder().tokens(getTokens(20)).build(); + try { controller.bulkDeleteTokens(request); } catch (Exception ignored) {} + assertEquals(5, getPrivateInt(controller, "deleteTokensBatchSize")); + assertEquals(3, getPrivateInt(controller, "deleteTokensConcurrencyLimit")); + } + + @Test + public void testBatchSizeExceedsMax_DELETE_TOKENS() throws Exception { + writeEnv("DELETE_TOKENS_BATCH_SIZE=1100"); + VaultController controller = createController(); + DeleteTokensRequest request = DeleteTokensRequest.builder().tokens(getTokens(50)).build(); + try { controller.bulkDeleteTokens(request); } catch (Exception ignored) {} + assertEquals(Constants.MAX_DELETE_TOKENS_BATCH_SIZE.intValue(), getPrivateInt(controller, "deleteTokensBatchSize")); + } + + @Test + public void testConcurrencyExceedsMax_DELETE_TOKENS() throws Exception { + writeEnv("DELETE_TOKENS_CONCURRENCY_LIMIT=110"); + VaultController controller = createController(); + DeleteTokensRequest request = DeleteTokensRequest.builder().tokens(getTokens(50)).build(); + try { controller.bulkDeleteTokens(request); } catch (Exception ignored) {} + assertEquals(1, getPrivateInt(controller, "deleteTokensConcurrencyLimit")); + } + + @Test + public void testBatchSizeZeroOrNegative_DELETE_TOKENS() throws Exception { + writeEnv("DELETE_TOKENS_BATCH_SIZE=0"); + VaultController controller = createController(); + DeleteTokensRequest request = DeleteTokensRequest.builder().tokens(getTokens(10)).build(); + try { controller.bulkDeleteTokens(request); } catch (Exception ignored) {} + assertEquals(Constants.DELETE_TOKENS_BATCH_SIZE.intValue(), getPrivateInt(controller, "deleteTokensBatchSize")); + + writeEnv("DELETE_TOKENS_BATCH_SIZE=-5"); + try { controller.bulkDeleteTokens(request); } catch (Exception ignored) {} + assertEquals(Constants.DELETE_TOKENS_BATCH_SIZE.intValue(), getPrivateInt(controller, "deleteTokensBatchSize")); + } + + @Test + public void testConcurrencyZeroOrNegative_DELETE_TOKENS() throws Exception { + writeEnv("DELETE_TOKENS_CONCURRENCY_LIMIT=0"); + VaultController controller = createController(); + DeleteTokensRequest request = DeleteTokensRequest.builder().tokens(getTokens(10)).build(); + try { controller.bulkDeleteTokens(request); } catch (Exception ignored) {} + int min = Math.min(Constants.DELETE_TOKENS_CONCURRENCY_LIMIT, + (10 + Constants.DELETE_TOKENS_BATCH_SIZE - 1) / Constants.DELETE_TOKENS_BATCH_SIZE); + assertEquals(min, getPrivateInt(controller, "deleteTokensConcurrencyLimit")); + + writeEnv("DELETE_TOKENS_CONCURRENCY_LIMIT=-5"); + try { controller.bulkDeleteTokens(request); } catch (Exception ignored) {} + assertEquals(min, getPrivateInt(controller, "deleteTokensConcurrencyLimit")); + } + + @Test + public void testNonNumericDeleteTokensBatchSize_usesDefault() throws Exception { + writeEnv("DELETE_TOKENS_BATCH_SIZE=abc"); + VaultController controller = createController(); + DeleteTokensRequest request = DeleteTokensRequest.builder().tokens(getTokens(10)).build(); + try { controller.bulkDeleteTokens(request); } catch (Exception ignored) {} + assertEquals(Constants.DELETE_TOKENS_BATCH_SIZE.intValue(), getPrivateInt(controller, "deleteTokensBatchSize")); + } + + @Test + public void testNonNumericDeleteTokensConcurrencyLimit_usesDefault() throws Exception { + writeEnv("DELETE_TOKENS_CONCURRENCY_LIMIT=xyz"); + VaultController controller = createController(); + DeleteTokensRequest request = DeleteTokensRequest.builder().tokens(getTokens(10)).build(); + try { controller.bulkDeleteTokens(request); } catch (Exception ignored) {} + int expected = Math.min(Constants.DELETE_TOKENS_CONCURRENCY_LIMIT, + (10 + Constants.DELETE_TOKENS_BATCH_SIZE - 1) / Constants.DELETE_TOKENS_BATCH_SIZE); + assertEquals(expected, getPrivateInt(controller, "deleteTokensConcurrencyLimit")); + } + + // ── configureTokenizeConcurrencyAndBatchSize ────────────────────────────── + + @Test + public void testCustomValidBatchAndConcurrency_TOKENIZE() throws Exception { + writeEnv("TOKENIZE_BATCH_SIZE=5\nTOKENIZE_CONCURRENCY_LIMIT=3"); + VaultController controller = createController(); + TokenizeRequest request = TokenizeRequest.builder().data(generateTokenizeData(20)).build(); + try { controller.bulkTokenize(request); } catch (Exception ignored) {} + assertEquals(5, getPrivateInt(controller, "tokenizeBatchSize")); + assertEquals(3, getPrivateInt(controller, "tokenizeConcurrencyLimit")); + } + + @Test + public void testBatchSizeExceedsMax_TOKENIZE() throws Exception { + writeEnv("TOKENIZE_BATCH_SIZE=1100"); + VaultController controller = createController(); + TokenizeRequest request = TokenizeRequest.builder().data(generateTokenizeData(50)).build(); + try { controller.bulkTokenize(request); } catch (Exception ignored) {} + assertEquals(Constants.MAX_TOKENIZE_BATCH_SIZE.intValue(), getPrivateInt(controller, "tokenizeBatchSize")); + } + + @Test + public void testConcurrencyExceedsMax_TOKENIZE() throws Exception { + writeEnv("TOKENIZE_CONCURRENCY_LIMIT=110"); + VaultController controller = createController(); + TokenizeRequest request = TokenizeRequest.builder().data(generateTokenizeData(50)).build(); + try { controller.bulkTokenize(request); } catch (Exception ignored) {} + assertEquals(1, getPrivateInt(controller, "tokenizeConcurrencyLimit")); + } + + @Test + public void testBatchSizeZeroOrNegative_TOKENIZE() throws Exception { + writeEnv("TOKENIZE_BATCH_SIZE=0"); + VaultController controller = createController(); + TokenizeRequest request = TokenizeRequest.builder().data(generateTokenizeData(10)).build(); + try { controller.bulkTokenize(request); } catch (Exception ignored) {} + assertEquals(Constants.TOKENIZE_BATCH_SIZE.intValue(), getPrivateInt(controller, "tokenizeBatchSize")); + + writeEnv("TOKENIZE_BATCH_SIZE=-5"); + try { controller.bulkTokenize(request); } catch (Exception ignored) {} + assertEquals(Constants.TOKENIZE_BATCH_SIZE.intValue(), getPrivateInt(controller, "tokenizeBatchSize")); + } + + @Test + public void testConcurrencyZeroOrNegative_TOKENIZE() throws Exception { + writeEnv("TOKENIZE_CONCURRENCY_LIMIT=0"); + VaultController controller = createController(); + TokenizeRequest request = TokenizeRequest.builder().data(generateTokenizeData(10)).build(); + try { controller.bulkTokenize(request); } catch (Exception ignored) {} + int min = Math.min(Constants.TOKENIZE_CONCURRENCY_LIMIT, + (10 + Constants.TOKENIZE_BATCH_SIZE - 1) / Constants.TOKENIZE_BATCH_SIZE); + assertEquals(min, getPrivateInt(controller, "tokenizeConcurrencyLimit")); + + writeEnv("TOKENIZE_CONCURRENCY_LIMIT=-5"); + try { controller.bulkTokenize(request); } catch (Exception ignored) {} + assertEquals(min, getPrivateInt(controller, "tokenizeConcurrencyLimit")); + } + + @Test + public void testNonNumericTokenizeBatchSize_usesDefault() throws Exception { + writeEnv("TOKENIZE_BATCH_SIZE=abc"); + VaultController controller = createController(); + TokenizeRequest request = TokenizeRequest.builder().data(generateTokenizeData(10)).build(); + try { controller.bulkTokenize(request); } catch (Exception ignored) {} + assertEquals(Constants.TOKENIZE_BATCH_SIZE.intValue(), getPrivateInt(controller, "tokenizeBatchSize")); + } + + @Test + public void testNonNumericTokenizeConcurrencyLimit_usesDefault() throws Exception { + writeEnv("TOKENIZE_CONCURRENCY_LIMIT=xyz"); + VaultController controller = createController(); + TokenizeRequest request = TokenizeRequest.builder().data(generateTokenizeData(10)).build(); + try { controller.bulkTokenize(request); } catch (Exception ignored) {} + int expected = Math.min(Constants.TOKENIZE_CONCURRENCY_LIMIT, + (10 + Constants.TOKENIZE_BATCH_SIZE - 1) / Constants.TOKENIZE_BATCH_SIZE); + assertEquals(expected, getPrivateInt(controller, "tokenizeConcurrencyLimit")); + } + + // ── Null batch list early-return paths ──────────────────────────────────── + + @Test + public void deleteTokensBatchFutures_nullBatches_returnsEmptyList() throws Exception { + VaultController controller = createController(); + Method method = VaultController.class.getDeclaredMethod( + "deleteTokensBatchFutures", ExecutorService.class, List.class, Map.class); + method.setAccessible(true); + ExecutorService executor = Executors.newFixedThreadPool(1); + @SuppressWarnings("unchecked") + List> futures = + (List>) method.invoke( + controller, executor, null, Collections.emptyMap()); + assertTrue("Expected empty list for null batches", futures.isEmpty()); + executor.shutdownNow(); + } + + @Test + public void tokenizeBatchFutures_nullBatches_returnsEmptyList() throws Exception { + VaultController controller = createController(); + Method method = VaultController.class.getDeclaredMethod( + "tokenizeBatchFutures", ExecutorService.class, List.class, Map.class); + method.setAccessible(true); + ExecutorService executor = Executors.newFixedThreadPool(1); + @SuppressWarnings("unchecked") + List> futures = + (List>) method.invoke( + controller, executor, null, Collections.emptyMap()); + assertTrue("Expected empty list for null batches", futures.isEmpty()); + executor.shutdownNow(); + } + + // ── Async SkyflowException re-throw paths ───────────────────────────────── + + @Test + public void bulkDeleteTokensAsync_emptyTokens_throwsSkyflowException() throws Exception { + VaultController controller = createController(); + DeleteTokensRequest request = DeleteTokensRequest.builder().tokens(new ArrayList<>()).build(); + try { + controller.bulkDeleteTokensAsync(request); + fail("Expected SkyflowException"); + } catch (SkyflowException e) { + assertEquals(ErrorMessage.EmptyDeleteTokensData.getMessage(), e.getMessage()); + } + } + + @Test + public void bulkTokenizeAsync_emptyData_throwsSkyflowException() throws Exception { + VaultController controller = createController(); + TokenizeRequest request = TokenizeRequest.builder().data(new ArrayList<>()).build(); + try { + controller.bulkTokenizeAsync(request); + fail("Expected SkyflowException"); + } catch (SkyflowException e) { + assertEquals(ErrorMessage.EmptyTokenizeData.getMessage(), e.getMessage()); + } + } + + // ── extractCustomHeaders ────────────────────────────────────────────────── + + @Test + public void extractCustomHeaders_insertOptions_null_returnsEmptyMap() throws Exception { + VaultController controller = createController(); + Method method = VaultController.class.getDeclaredMethod("extractCustomHeaders", InsertOptions.class); + method.setAccessible(true); + @SuppressWarnings("unchecked") + Map result = (Map) method.invoke(controller, (InsertOptions) null); + assertTrue(result.isEmpty()); + } + + @Test + public void extractCustomHeaders_insertOptions_withHeaders_convertsToStringKeys() throws Exception { + VaultController controller = createController(); + Method method = VaultController.class.getDeclaredMethod("extractCustomHeaders", InsertOptions.class); + method.setAccessible(true); + InsertOptions opts = InsertOptions.builder() + .addCustomHeader(CustomHeaderKey.SkyflowAccountID, "acct-val") + .addCustomHeader(CustomHeaderKey.RequestIDHeader, "req-123") + .build(); + @SuppressWarnings("unchecked") + Map result = (Map) method.invoke(controller, opts); + assertEquals(2, result.size()); + assertEquals("acct-val", result.get(CustomHeaderKey.SkyflowAccountID.toString())); + assertEquals("req-123", result.get(CustomHeaderKey.RequestIDHeader.toString())); + } + + @Test + public void extractCustomHeaders_detokenizeOptions_null_returnsEmptyMap() throws Exception { + VaultController controller = createController(); + Method method = VaultController.class.getDeclaredMethod("extractCustomHeaders", DetokenizeOptions.class); + method.setAccessible(true); + @SuppressWarnings("unchecked") + Map result = (Map) method.invoke(controller, (DetokenizeOptions) null); + assertTrue(result.isEmpty()); + } + + @Test + public void extractCustomHeaders_detokenizeOptions_withHeaders_convertsToStringKeys() throws Exception { + VaultController controller = createController(); + Method method = VaultController.class.getDeclaredMethod("extractCustomHeaders", DetokenizeOptions.class); + method.setAccessible(true); + DetokenizeOptions opts = DetokenizeOptions.builder() + .addCustomHeader(CustomHeaderKey.SkyflowAccountName, "my-account") + .build(); + @SuppressWarnings("unchecked") + Map result = (Map) method.invoke(controller, opts); + assertEquals("my-account", result.get(CustomHeaderKey.SkyflowAccountName.toString())); + } + + @Test + public void extractCustomHeaders_deleteTokensOptions_null_returnsEmptyMap() throws Exception { + VaultController controller = createController(); + Method method = VaultController.class.getDeclaredMethod("extractCustomHeaders", DeleteTokensOptions.class); + method.setAccessible(true); + @SuppressWarnings("unchecked") + Map result = (Map) method.invoke(controller, (DeleteTokensOptions) null); + assertTrue(result.isEmpty()); + } + + @Test + public void extractCustomHeaders_deleteTokensOptions_withHeaders_convertsToStringKeys() throws Exception { + VaultController controller = createController(); + Method method = VaultController.class.getDeclaredMethod("extractCustomHeaders", DeleteTokensOptions.class); + method.setAccessible(true); + DeleteTokensOptions opts = DeleteTokensOptions.builder() + .addCustomHeader(CustomHeaderKey.RequestIDHeader, "del-req") + .build(); + @SuppressWarnings("unchecked") + Map result = (Map) method.invoke(controller, opts); + assertEquals("del-req", result.get(CustomHeaderKey.RequestIDHeader.toString())); + } + + @Test + public void extractCustomHeaders_tokenizeOptions_null_returnsEmptyMap() throws Exception { + VaultController controller = createController(); + Method method = VaultController.class.getDeclaredMethod("extractCustomHeaders", TokenizeOptions.class); + method.setAccessible(true); + @SuppressWarnings("unchecked") + Map result = (Map) method.invoke(controller, (TokenizeOptions) null); + assertTrue(result.isEmpty()); + } + + @Test + public void extractCustomHeaders_tokenizeOptions_withHeaders_convertsToStringKeys() throws Exception { + VaultController controller = createController(); + Method method = VaultController.class.getDeclaredMethod("extractCustomHeaders", TokenizeOptions.class); + method.setAccessible(true); + TokenizeOptions opts = TokenizeOptions.builder() + .addCustomHeader(CustomHeaderKey.SkyflowAccountID, "tok-acct") + .build(); + @SuppressWarnings("unchecked") + Map result = (Map) method.invoke(controller, opts); + assertEquals("tok-acct", result.get(CustomHeaderKey.SkyflowAccountID.toString())); + } + + // ── buildRequestOptions ─────────────────────────────────────────────────── + + @Test + public void buildRequestOptions_noCustomHeaders_containsMetricsHeader() throws Exception { + VaultController controller = createController(); + Method method = VaultController.class.getDeclaredMethod("buildRequestOptions", Map.class); + method.setAccessible(true); + com.skyflow.generated.rest.core.RequestOptions opts = + (com.skyflow.generated.rest.core.RequestOptions) method.invoke(controller, Collections.emptyMap()); + Map headers = opts.getHeaders(); + assertTrue("Metrics header must always be present", headers.containsKey(com.skyflow.utils.Constants.SDK_METRICS_HEADER_KEY)); + } + + @Test + public void buildRequestOptions_withCustomHeaders_containsBothMetricsAndCustom() throws Exception { + VaultController controller = createController(); + Method method = VaultController.class.getDeclaredMethod("buildRequestOptions", Map.class); + method.setAccessible(true); + Map custom = new HashMap<>(); + custom.put(CustomHeaderKey.SkyflowAccountID.toString(), "acct-999"); + custom.put(CustomHeaderKey.RequestIDHeader.toString(), "req-001"); + com.skyflow.generated.rest.core.RequestOptions opts = + (com.skyflow.generated.rest.core.RequestOptions) method.invoke(controller, custom); + Map headers = opts.getHeaders(); + assertTrue("Metrics header must be present", headers.containsKey(com.skyflow.utils.Constants.SDK_METRICS_HEADER_KEY)); + assertEquals("acct-999", headers.get(CustomHeaderKey.SkyflowAccountID.toString())); + assertEquals("req-001", headers.get(CustomHeaderKey.RequestIDHeader.toString())); + } + + @Test + public void buildRequestOptions_nullCustomHeaders_containsOnlyMetricsHeader() throws Exception { + VaultController controller = createController(); + Method method = VaultController.class.getDeclaredMethod("buildRequestOptions", Map.class); + method.setAccessible(true); + com.skyflow.generated.rest.core.RequestOptions opts = + (com.skyflow.generated.rest.core.RequestOptions) method.invoke(controller, (Map) null); + Map headers = opts.getHeaders(); + assertTrue("Metrics header must be present even for null custom headers", + headers.containsKey(com.skyflow.utils.Constants.SDK_METRICS_HEADER_KEY)); + } + + // ── Private helpers ─────────────────────────────────────────────────────── + + private void invokeConfigureDeleteTokensConcurrencyAndBatchSize(VaultController controller, int totalRequests) throws Exception { + Method method = VaultController.class.getDeclaredMethod("configureDeleteTokensConcurrencyAndBatchSize", int.class); + method.setAccessible(true); + method.invoke(controller, totalRequests); + } + + private void invokeConfigureTokenizeConcurrencyAndBatchSize(VaultController controller, int totalRequests) throws Exception { + Method method = VaultController.class.getDeclaredMethod("configureTokenizeConcurrencyAndBatchSize", int.class); + method.setAccessible(true); + method.invoke(controller, totalRequests); + } + + private ArrayList generateTokenizeData(int count) { + ArrayList data = new ArrayList<>(); + for (int i = 0; i < count; i++) { + data.add(TokenizeRecord.builder().value("val" + i).build()); + } + return data; + } } \ No newline at end of file diff --git a/v3/src/test/java/com/skyflow/vault/controller/VaultControllerTokenizeTests.java b/v3/src/test/java/com/skyflow/vault/controller/VaultControllerTokenizeTests.java index 44eac20b..c80955c6 100644 --- a/v3/src/test/java/com/skyflow/vault/controller/VaultControllerTokenizeTests.java +++ b/v3/src/test/java/com/skyflow/vault/controller/VaultControllerTokenizeTests.java @@ -391,13 +391,13 @@ public void testTokenizeBatchFutures_catchBranchAddsErrorRecord() throws Excepti List batches = null; Method method = VaultController.class.getDeclaredMethod( - "tokenizeBatchFutures", ExecutorService.class, List.class); + "tokenizeBatchFutures", ExecutorService.class, List.class, java.util.Map.class); method.setAccessible(true); ExecutorService executor = Executors.newFixedThreadPool(1); @SuppressWarnings("unchecked") List> futures = - (List>) method.invoke(controller, executor, batches); + (List>) method.invoke(controller, executor, batches, Collections.emptyMap()); // errors are now returned via futures, not a shared list Assert.assertNotNull(futures); @@ -432,12 +432,13 @@ public void testProcessTokenizeSyncNormalPath() throws Exception { Method processSync = VaultController.class.getDeclaredMethod( "processTokenizeSync", com.skyflow.generated.rest.resources.flowservice.requests.V1FlowTokenizeRequest.class, - ArrayList.class + ArrayList.class, + java.util.Map.class ); processSync.setAccessible(true); try { - processSync.invoke(controller, requestObj, data); + processSync.invoke(controller, requestObj, data, Collections.emptyMap()); } catch (java.lang.reflect.InvocationTargetException e) { Throwable cause = e.getCause(); assertTrue(cause instanceof SkyflowException diff --git a/v3/src/test/java/com/skyflow/vault/data/DetokenizeResponseTests.java b/v3/src/test/java/com/skyflow/vault/data/DetokenizeResponseTests.java index 2bf8bfb7..522482e2 100644 --- a/v3/src/test/java/com/skyflow/vault/data/DetokenizeResponseTests.java +++ b/v3/src/test/java/com/skyflow/vault/data/DetokenizeResponseTests.java @@ -81,4 +81,101 @@ public void testConstructorWithoutOriginalPayload() { Assert.assertEquals(errors, response.getErrors()); Assert.assertNull(response.getSummary()); } + + // ── DetokenizeSummary direct tests ──────────────────────────────────────── + + @Test + public void detokenizeSummary_paramConstructor_setsAllFields() { + DetokenizeSummary summary = new DetokenizeSummary(10, 7, 3); + Assert.assertEquals(10, summary.getTotalTokens()); + Assert.assertEquals(7, summary.getTotalDetokenized()); + Assert.assertEquals(3, summary.getTotalFailed()); + } + + @Test + public void detokenizeSummary_defaultConstructor_allZero() { + DetokenizeSummary summary = new DetokenizeSummary(); + Assert.assertEquals(0, summary.getTotalTokens()); + Assert.assertEquals(0, summary.getTotalDetokenized()); + Assert.assertEquals(0, summary.getTotalFailed()); + } + + @Test + public void detokenizeSummary_toString_containsAllFields() { + DetokenizeSummary summary = new DetokenizeSummary(5, 4, 1); + String json = summary.toString(); + Assert.assertTrue(json.contains("totalTokens")); + Assert.assertTrue(json.contains("totalDetokenized")); + Assert.assertTrue(json.contains("totalFailed")); + Assert.assertTrue(json.contains("5")); + Assert.assertTrue(json.contains("4")); + Assert.assertTrue(json.contains("1")); + } + + @Test + public void detokenizeSummary_allSuccess_zeroFailed() { + DetokenizeSummary summary = new DetokenizeSummary(3, 3, 0); + Assert.assertEquals(3, summary.getTotalTokens()); + Assert.assertEquals(3, summary.getTotalDetokenized()); + Assert.assertEquals(0, summary.getTotalFailed()); + } + + @Test + public void detokenizeSummary_allFailed_zeroDetokenized() { + DetokenizeSummary summary = new DetokenizeSummary(4, 0, 4); + Assert.assertEquals(4, summary.getTotalTokens()); + Assert.assertEquals(0, summary.getTotalDetokenized()); + Assert.assertEquals(4, summary.getTotalFailed()); + } + + // ── DetokenizeResponseObject direct tests ───────────────────────────────── + + @Test + public void detokenizeResponseObject_constructor_setsAllFields() { + Map meta = new HashMap<>(); + meta.put("key", "val"); + DetokenizeResponseObject obj = new DetokenizeResponseObject(2, "tok-x", "plain", "grp1", "some error", meta); + Assert.assertEquals(2, obj.getIndex()); + Assert.assertEquals("tok-x", obj.getToken()); + Assert.assertEquals("plain", obj.getValue()); + Assert.assertEquals("grp1", obj.getTokenGroupName()); + Assert.assertEquals("some error", obj.getError()); + Assert.assertEquals(meta, obj.getMetadata()); + } + + @Test + public void detokenizeResponseObject_nullFields_returnsNull() { + DetokenizeResponseObject obj = new DetokenizeResponseObject(0, null, null, null, null, null); + Assert.assertEquals(0, obj.getIndex()); + Assert.assertNull(obj.getToken()); + Assert.assertNull(obj.getValue()); + Assert.assertNull(obj.getTokenGroupName()); + Assert.assertNull(obj.getError()); + Assert.assertNull(obj.getMetadata()); + } + + @Test + public void detokenizeResponseObject_toString_containsToken() { + DetokenizeResponseObject obj = new DetokenizeResponseObject(1, "tok-abc", "secret-val", "group-x", null, null); + String json = obj.toString(); + Assert.assertTrue(json.contains("tok-abc")); + Assert.assertTrue(json.contains("secret-val")); + Assert.assertTrue(json.contains("group-x")); + } + + @Test + public void detokenizeResponseObject_toString_withMetadata() { + Map meta = new java.util.HashMap<>(); + meta.put("region", "us-east-1"); + DetokenizeResponseObject obj = new DetokenizeResponseObject(0, "tok1", "val1", "grp", null, meta); + String json = obj.toString(); + Assert.assertTrue(json.contains("us-east-1")); + } + + @Test + public void detokenizeResponseObject_withError_getterReturnsError() { + DetokenizeResponseObject obj = new DetokenizeResponseObject(3, "bad-tok", null, null, "Token not found", null); + Assert.assertEquals("Token not found", obj.getError()); + Assert.assertNull(obj.getValue()); + } } \ No newline at end of file diff --git a/v3/src/test/java/com/skyflow/vault/data/ErrorRecordTests.java b/v3/src/test/java/com/skyflow/vault/data/ErrorRecordTests.java index 32f4731e..14a68e35 100644 --- a/v3/src/test/java/com/skyflow/vault/data/ErrorRecordTests.java +++ b/v3/src/test/java/com/skyflow/vault/data/ErrorRecordTests.java @@ -21,4 +21,44 @@ public void testToStringJsonFormat() { Assert.assertTrue(json.contains("\"error\":\"Error occurred\"")); Assert.assertTrue(json.contains("\"code\":500")); } + + // ── requestId field ─────────────────────────────────────────────────────── + + @Test + public void testConstructorWithRequestId_setsAllFields() { + ErrorRecord record = new ErrorRecord(3, "auth error", 401, "req-id-abc"); + Assert.assertEquals(3, record.getIndex()); + Assert.assertEquals("auth error", record.getError()); + Assert.assertEquals(401, record.getCode()); + Assert.assertEquals("req-id-abc", record.getRequestId()); + } + + @Test + public void testThreeArgConstructor_requestIdIsNull() { + ErrorRecord record = new ErrorRecord(1, "error", 500); + Assert.assertNull(record.getRequestId()); + } + + @Test + public void testFourArgConstructor_nullRequestId() { + ErrorRecord record = new ErrorRecord(1, "error", 500, null); + Assert.assertNull(record.getRequestId()); + } + + @Test + public void testToString_includesRequestId() { + ErrorRecord record = new ErrorRecord(1, "err", 400, "req-xyz"); + String json = record.toString(); + Assert.assertTrue(json.contains("\"requestId\":\"req-xyz\"")); + Assert.assertTrue(json.contains("\"index\":1")); + Assert.assertTrue(json.contains("\"code\":400")); + } + + @Test + public void testToString_nullRequestIdNotSerialized() { + ErrorRecord record = new ErrorRecord(1, "err", 400); + String json = record.toString(); + Assert.assertTrue(json.contains("\"index\":1")); + Assert.assertFalse(json.contains("\"requestId\"")); + } } \ No newline at end of file diff --git a/v3/src/test/java/com/skyflow/vault/data/OptionsTests.java b/v3/src/test/java/com/skyflow/vault/data/OptionsTests.java new file mode 100644 index 00000000..7d6c55ea --- /dev/null +++ b/v3/src/test/java/com/skyflow/vault/data/OptionsTests.java @@ -0,0 +1,223 @@ +package com.skyflow.vault.data; + +import com.skyflow.enums.CustomHeaderKey; +import org.junit.Assert; +import org.junit.Test; + +import java.util.Map; + +public class OptionsTests { + + // ── InsertOptions ───────────────────────────────────────────────────────── + + @Test + public void insertOptions_emptyBuilder_returnsEmptyHeaders() { + InsertOptions opts = InsertOptions.builder().build(); + Assert.assertNotNull(opts.getCustomHeaders()); + Assert.assertTrue(opts.getCustomHeaders().isEmpty()); + } + + @Test + public void insertOptions_singleHeader_storedCorrectly() { + InsertOptions opts = InsertOptions.builder() + .addCustomHeader(CustomHeaderKey.SkyflowAccountID, "acct-123") + .build(); + Map headers = opts.getCustomHeaders(); + Assert.assertEquals(1, headers.size()); + Assert.assertEquals("acct-123", headers.get(CustomHeaderKey.SkyflowAccountID)); + } + + @Test + public void insertOptions_multipleHeaders_allStored() { + InsertOptions opts = InsertOptions.builder() + .addCustomHeader(CustomHeaderKey.SkyflowAccountID, "acct-123") + .addCustomHeader(CustomHeaderKey.SkyflowAccountName, "my-account") + .addCustomHeader(CustomHeaderKey.RequestIDHeader, "req-abc") + .build(); + Map headers = opts.getCustomHeaders(); + Assert.assertEquals(3, headers.size()); + Assert.assertEquals("acct-123", headers.get(CustomHeaderKey.SkyflowAccountID)); + Assert.assertEquals("my-account", headers.get(CustomHeaderKey.SkyflowAccountName)); + Assert.assertEquals("req-abc", headers.get(CustomHeaderKey.RequestIDHeader)); + } + + @Test + public void insertOptions_overwriteSameKey_keepsLastValue() { + InsertOptions opts = InsertOptions.builder() + .addCustomHeader(CustomHeaderKey.SkyflowAccountID, "first") + .addCustomHeader(CustomHeaderKey.SkyflowAccountID, "second") + .build(); + Assert.assertEquals("second", opts.getCustomHeaders().get(CustomHeaderKey.SkyflowAccountID)); + } + + @Test + public void insertOptions_builderMutationAfterBuild_doesNotAffectBuiltInstance() { + InsertOptions.Builder builder = InsertOptions.builder() + .addCustomHeader(CustomHeaderKey.RequestIDHeader, "original"); + InsertOptions opts = builder.build(); + builder.addCustomHeader(CustomHeaderKey.SkyflowAccountID, "added-after-build"); + Assert.assertEquals(1, opts.getCustomHeaders().size()); + Assert.assertEquals("original", opts.getCustomHeaders().get(CustomHeaderKey.RequestIDHeader)); + Assert.assertNull(opts.getCustomHeaders().get(CustomHeaderKey.SkyflowAccountID)); + } + + @Test + public void insertOptions_mapIsUnmodifiable() { + InsertOptions opts = InsertOptions.builder() + .addCustomHeader(CustomHeaderKey.RequestIDHeader, "r1") + .build(); + try { + opts.getCustomHeaders().put(CustomHeaderKey.SkyflowAccountID, "extra"); + Assert.fail("Expected UnsupportedOperationException"); + } catch (UnsupportedOperationException ignored) { + } + } + + // ── DetokenizeOptions ───────────────────────────────────────────────────── + + @Test + public void detokenizeOptions_emptyBuilder_returnsEmptyHeaders() { + DetokenizeOptions opts = DetokenizeOptions.builder().build(); + Assert.assertNotNull(opts.getCustomHeaders()); + Assert.assertTrue(opts.getCustomHeaders().isEmpty()); + } + + @Test + public void detokenizeOptions_singleHeader_storedCorrectly() { + DetokenizeOptions opts = DetokenizeOptions.builder() + .addCustomHeader(CustomHeaderKey.RequestIDHeader, "req-xyz") + .build(); + Assert.assertEquals("req-xyz", opts.getCustomHeaders().get(CustomHeaderKey.RequestIDHeader)); + } + + @Test + public void detokenizeOptions_multipleHeaders_allStored() { + DetokenizeOptions opts = DetokenizeOptions.builder() + .addCustomHeader(CustomHeaderKey.SkyflowAccountID, "a") + .addCustomHeader(CustomHeaderKey.SkyflowAccountName, "b") + .build(); + Assert.assertEquals(2, opts.getCustomHeaders().size()); + } + + @Test + public void detokenizeOptions_builderMutationAfterBuild_doesNotAffectBuiltInstance() { + DetokenizeOptions.Builder builder = DetokenizeOptions.builder() + .addCustomHeader(CustomHeaderKey.RequestIDHeader, "original"); + DetokenizeOptions opts = builder.build(); + builder.addCustomHeader(CustomHeaderKey.SkyflowAccountID, "added-after-build"); + Assert.assertEquals(1, opts.getCustomHeaders().size()); + Assert.assertEquals("original", opts.getCustomHeaders().get(CustomHeaderKey.RequestIDHeader)); + Assert.assertNull(opts.getCustomHeaders().get(CustomHeaderKey.SkyflowAccountID)); + } + + @Test + public void detokenizeOptions_mapIsUnmodifiable() { + DetokenizeOptions opts = DetokenizeOptions.builder() + .addCustomHeader(CustomHeaderKey.RequestIDHeader, "r1") + .build(); + try { + opts.getCustomHeaders().put(CustomHeaderKey.SkyflowAccountID, "extra"); + Assert.fail("Expected UnsupportedOperationException"); + } catch (UnsupportedOperationException ignored) { + } + } + + // ── TokenizeOptions ─────────────────────────────────────────────────────── + + @Test + public void tokenizeOptions_emptyBuilder_returnsEmptyHeaders() { + TokenizeOptions opts = TokenizeOptions.builder().build(); + Assert.assertNotNull(opts.getCustomHeaders()); + Assert.assertTrue(opts.getCustomHeaders().isEmpty()); + } + + @Test + public void tokenizeOptions_singleHeader_storedCorrectly() { + TokenizeOptions opts = TokenizeOptions.builder() + .addCustomHeader(CustomHeaderKey.SkyflowAccountName, "my-vault") + .build(); + Assert.assertEquals("my-vault", opts.getCustomHeaders().get(CustomHeaderKey.SkyflowAccountName)); + } + + @Test + public void tokenizeOptions_multipleHeaders_allStored() { + TokenizeOptions opts = TokenizeOptions.builder() + .addCustomHeader(CustomHeaderKey.SkyflowAccountID, "a") + .addCustomHeader(CustomHeaderKey.RequestIDHeader, "b") + .build(); + Assert.assertEquals(2, opts.getCustomHeaders().size()); + } + + @Test + public void tokenizeOptions_builderMutationAfterBuild_doesNotAffectBuiltInstance() { + TokenizeOptions.Builder builder = TokenizeOptions.builder() + .addCustomHeader(CustomHeaderKey.RequestIDHeader, "original"); + TokenizeOptions opts = builder.build(); + builder.addCustomHeader(CustomHeaderKey.SkyflowAccountID, "added-after-build"); + Assert.assertEquals(1, opts.getCustomHeaders().size()); + Assert.assertEquals("original", opts.getCustomHeaders().get(CustomHeaderKey.RequestIDHeader)); + Assert.assertNull(opts.getCustomHeaders().get(CustomHeaderKey.SkyflowAccountID)); + } + + @Test + public void tokenizeOptions_mapIsUnmodifiable() { + TokenizeOptions opts = TokenizeOptions.builder() + .addCustomHeader(CustomHeaderKey.RequestIDHeader, "r1") + .build(); + try { + opts.getCustomHeaders().put(CustomHeaderKey.SkyflowAccountID, "extra"); + Assert.fail("Expected UnsupportedOperationException"); + } catch (UnsupportedOperationException ignored) { + } + } + + // ── DeleteTokensOptions ─────────────────────────────────────────────────── + + @Test + public void deleteTokensOptions_emptyBuilder_returnsEmptyHeaders() { + DeleteTokensOptions opts = DeleteTokensOptions.builder().build(); + Assert.assertNotNull(opts.getCustomHeaders()); + Assert.assertTrue(opts.getCustomHeaders().isEmpty()); + } + + @Test + public void deleteTokensOptions_singleHeader_storedCorrectly() { + DeleteTokensOptions opts = DeleteTokensOptions.builder() + .addCustomHeader(CustomHeaderKey.RequestIDHeader, "req-del") + .build(); + Assert.assertEquals("req-del", opts.getCustomHeaders().get(CustomHeaderKey.RequestIDHeader)); + } + + @Test + public void deleteTokensOptions_multipleHeaders_allStored() { + DeleteTokensOptions opts = DeleteTokensOptions.builder() + .addCustomHeader(CustomHeaderKey.SkyflowAccountID, "a") + .addCustomHeader(CustomHeaderKey.SkyflowAccountName, "b") + .addCustomHeader(CustomHeaderKey.RequestIDHeader, "c") + .build(); + Assert.assertEquals(3, opts.getCustomHeaders().size()); + } + + @Test + public void deleteTokensOptions_builderMutationAfterBuild_doesNotAffectBuiltInstance() { + DeleteTokensOptions.Builder builder = DeleteTokensOptions.builder() + .addCustomHeader(CustomHeaderKey.RequestIDHeader, "original"); + DeleteTokensOptions opts = builder.build(); + builder.addCustomHeader(CustomHeaderKey.SkyflowAccountID, "added-after-build"); + Assert.assertEquals(1, opts.getCustomHeaders().size()); + Assert.assertEquals("original", opts.getCustomHeaders().get(CustomHeaderKey.RequestIDHeader)); + Assert.assertNull(opts.getCustomHeaders().get(CustomHeaderKey.SkyflowAccountID)); + } + + @Test + public void deleteTokensOptions_mapIsUnmodifiable() { + DeleteTokensOptions opts = DeleteTokensOptions.builder() + .addCustomHeader(CustomHeaderKey.RequestIDHeader, "r1") + .build(); + try { + opts.getCustomHeaders().put(CustomHeaderKey.SkyflowAccountID, "extra"); + Assert.fail("Expected UnsupportedOperationException"); + } catch (UnsupportedOperationException ignored) { + } + } +} \ No newline at end of file diff --git a/v3/src/test/java/com/skyflow/vault/data/TokenDataTests.java b/v3/src/test/java/com/skyflow/vault/data/TokenDataTests.java new file mode 100644 index 00000000..20a0e8b7 --- /dev/null +++ b/v3/src/test/java/com/skyflow/vault/data/TokenDataTests.java @@ -0,0 +1,44 @@ +package com.skyflow.vault.data; + +import org.junit.Assert; +import org.junit.Test; + +public class TokenDataTests { + + @Test + public void constructor_setsTokenAndGroupName() { + Token token = new Token("tok-abc", "non_deterministic"); + Assert.assertEquals("tok-abc", token.getToken()); + Assert.assertEquals("non_deterministic", token.getTokenGroupName()); + } + + @Test + public void constructor_nullToken_allowsNull() { + Token token = new Token(null, "grp"); + Assert.assertNull(token.getToken()); + Assert.assertEquals("grp", token.getTokenGroupName()); + } + + @Test + public void constructor_nullGroupName_allowsNull() { + Token token = new Token("tok-123", null); + Assert.assertEquals("tok-123", token.getToken()); + Assert.assertNull(token.getTokenGroupName()); + } + + @Test + public void constructor_bothNull_allowsNull() { + Token token = new Token(null, null); + Assert.assertNull(token.getToken()); + Assert.assertNull(token.getTokenGroupName()); + } + + @Test + public void constructor_preservesExactValues() { + String tokenValue = "sky-tok-abcdef1234567890"; + String groupName = "deterministic_string_tg"; + Token token = new Token(tokenValue, groupName); + Assert.assertSame(tokenValue, token.getToken()); + Assert.assertSame(groupName, token.getTokenGroupName()); + } +}