diff --git a/v3/pom.xml b/v3/pom.xml index 140ae3c5..57626e12 100644 --- a/v3/pom.xml +++ b/v3/pom.xml @@ -11,7 +11,7 @@ skyflow-java - 3.0.0-beta.9-dev.e40880f + 2.0.4-dev.a98436e jar ${project.groupId}:${project.artifactId} Skyflow V3 SDK for the Java programming language 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..839c54ce 100644 --- a/v3/src/main/java/com/skyflow/utils/validations/Validations.java +++ b/v3/src/main/java/com/skyflow/utils/validations/Validations.java @@ -140,15 +140,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); 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..40afd90b 100644 --- a/v3/src/main/java/com/skyflow/vault/controller/VaultController.java +++ b/v3/src/main/java/com/skyflow/vault/controller/VaultController.java @@ -1,5 +1,13 @@ package com.skyflow.vault.controller; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + import com.google.gson.Gson; import com.google.gson.GsonBuilder; import com.google.gson.JsonObject; @@ -8,6 +16,7 @@ import com.skyflow.config.VaultConfig; 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 +28,29 @@ 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.RequestContext; +import com.skyflow.vault.data.RequestInterceptor; +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,8 +75,13 @@ 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()); @@ -68,20 +90,28 @@ public com.skyflow.vault.data.InsertResponse bulkInsert(InsertRequest insertRequ setBearerToken(); com.skyflow.generated.rest.resources.flowservice.requests.V1InsertRequest request = super.getBulkInsertRequestBody(insertRequest, super.getVaultConfig()); - - response = this.processSync(request, insertRequest.getRecords()); - return response; + RequestInterceptor interceptor = options != null ? options.getInterceptor() : null; + return this.processSync(request, insertRequest.getRecords(), interceptor); } 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()); @@ -90,13 +120,13 @@ public CompletableFuture bulkInsertAsync( setBearerToken(); com.skyflow.generated.rest.resources.flowservice.requests.V1InsertRequest request = super.getBulkInsertRequestBody(insertRequest, super.getVaultConfig()); + RequestInterceptor interceptor = options != null ? options.getInterceptor() : null; List errorRecords = Collections.synchronizedList(new ArrayList<>()); - List> futures = this.insertBatchFutures(request, errorRecords); + List> futures = this.insertBatchFutures(request, errorRecords, interceptor); 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,29 +149,39 @@ 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); setBearerToken(); com.skyflow.generated.rest.resources.flowservice.requests.V1FlowDetokenizeRequest request = super.getDetokenizeRequestBody(detokenizeRequest); - - response = this.processDetokenizeSync(request, detokenizeRequest.getTokens()); - return response; + RequestInterceptor interceptor = options != null ? options.getInterceptor() : null; + return this.processDetokenizeSync(request, detokenizeRequest.getTokens(), interceptor); } 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 (ExecutionException | InterruptedException e) { - LogUtil.printErrorLog(ErrorLogs.DETOKENIZE_REQUEST_REJECTED.getLog()); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new SkyflowException(e.getMessage()); + } catch (ExecutionException e) { throw new SkyflowException(e.getMessage()); } } 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 { @@ -150,16 +190,16 @@ public CompletableFuture bulkDetokenizeAsync(DetokenizeReque Validations.validateDetokenizeRequest(detokenizeRequest); setBearerToken(); com.skyflow.generated.rest.resources.flowservice.requests.V1FlowDetokenizeRequest request = super.getDetokenizeRequestBody(detokenizeRequest); + RequestInterceptor interceptor = options != null ? options.getInterceptor() : null; 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, interceptor); return CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])) .thenApply(v -> { for (CompletableFuture future : futures) { @@ -177,6 +217,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,7 +232,13 @@ 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()); @@ -194,7 +247,8 @@ public DeleteTokensResponse bulkDeleteTokens(DeleteTokensRequest deleteTokensReq setBearerToken(); com.skyflow.generated.rest.resources.flowservice.requests.V1FlowDeleteTokenRequest request = super.getDeleteTokensRequestBody(deleteTokensRequest); - return this.processDeleteTokensSync(request, deleteTokensRequest.getTokens()); + RequestInterceptor interceptor = options != null ? options.getInterceptor() : null; + return this.processDeleteTokensSync(request, deleteTokensRequest.getTokens(), interceptor); } catch (ApiClientApiException e) { String bodyString = gson.toJson(e.body()); LogUtil.printErrorLog(ErrorLogs.DELETE_REQUEST_REJECTED.getLog()); @@ -206,6 +260,10 @@ 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 { @@ -215,6 +273,7 @@ public CompletableFuture bulkDeleteTokensAsync(DeleteToken setBearerToken(); com.skyflow.generated.rest.resources.flowservice.requests.V1FlowDeleteTokenRequest request = super.getDeleteTokensRequestBody(deleteTokensRequest); + RequestInterceptor interceptor = options != null ? options.getInterceptor() : null; LogUtil.printInfoLog(InfoLogs.PROCESSING_BATCHES.getLog()); @@ -225,7 +284,7 @@ public CompletableFuture bulkDeleteTokensAsync(DeleteToken Utils.createDeleteTokensBatches(request, deleteTokensBatchSize); List> futures = - this.deleteTokensBatchFutures(executor, batches); + this.deleteTokensBatchFutures(executor, batches, interceptor); return CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])) .thenApply(v -> { @@ -259,9 +318,108 @@ 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); + configureTokenizeConcurrencyAndBatchSize(tokenizeRequest.getData().size()); + setBearerToken(); + com.skyflow.generated.rest.resources.flowservice.requests.V1FlowTokenizeRequest request = + super.getTokenizeRequestBody(tokenizeRequest); + RequestInterceptor interceptor = options != null ? options.getInterceptor() : null; + return this.processTokenizeSync(request, tokenizeRequest.getData(), interceptor); + } 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); + configureTokenizeConcurrencyAndBatchSize(tokenizeRequest.getData().size()); + setBearerToken(); + com.skyflow.generated.rest.resources.flowservice.requests.V1FlowTokenizeRequest request = + super.getTokenizeRequestBody(tokenizeRequest); + RequestInterceptor interceptor = options != null ? options.getInterceptor() : null; + + 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, interceptor); + + 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 RequestOptions buildRequestOptions(RequestContext context) { + RequestOptions.Builder builder = RequestOptions.builder() + .addHeader(Constants.SDK_METRICS_HEADER_KEY, metrics.toString()); + context.getHeaders().forEach((k, v) -> builder.addHeader(k.toString(), v)); + return builder.build(); + } + private DeleteTokensResponse processDeleteTokensSync( com.skyflow.generated.rest.resources.flowservice.requests.V1FlowDeleteTokenRequest deleteTokensRequest, - List originalTokens + List originalTokens, + RequestInterceptor interceptor ) throws ExecutionException, InterruptedException, SkyflowException { LogUtil.printInfoLog(InfoLogs.PROCESSING_BATCHES.getLog()); List errorRecords = Collections.synchronizedList(new ArrayList<>()); @@ -271,7 +429,7 @@ private DeleteTokensResponse processDeleteTokensSync( Utils.createDeleteTokensBatches(deleteTokensRequest, deleteTokensBatchSize); try { List> futures = - this.deleteTokensBatchFutures(executor, batches); + this.deleteTokensBatchFutures(executor, batches, interceptor); try { CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).get(); } catch (Exception e) { @@ -297,33 +455,35 @@ private DeleteTokensResponse processDeleteTokensSync( private List> deleteTokensBatchFutures( ExecutorService executor, - List batches) { + List batches, + RequestInterceptor interceptor) { List> futures = new ArrayList<>(); if (batches == null) return futures; + int totalBatches = batches.size(); for (int batchIndex = 0; batchIndex < batches.size(); batchIndex++) { final int index = batchIndex; com.skyflow.generated.rest.resources.flowservice.requests.V1FlowDeleteTokenRequest batch = batches.get(index); + RequestContext ctx = new RequestContext("DELETE_TOKENS", batchIndex, totalBatches); + if (interceptor != null) interceptor.intercept(ctx); CompletableFuture future = CompletableFuture - .supplyAsync(() -> processDeleteTokensBatch(batch), executor) + .supplyAsync(() -> processDeleteTokensBatch(batch, ctx), 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, + RequestContext ctx) { + return this.getRecordsApi().withRawResponse().deletetoken(batch, buildRequestOptions(ctx)); } private void configureDeleteTokensConcurrencyAndBatchSize(int totalRequests) { @@ -362,7 +522,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 +551,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, + RequestInterceptor interceptor ) throws ExecutionException, InterruptedException, SkyflowException { LogUtil.printInfoLog(InfoLogs.PROCESSING_BATCHES.getLog()); List errorRecords = Collections.synchronizedList(new ArrayList<>()); @@ -481,7 +564,7 @@ private TokenizeResponse processTokenizeSync( Utils.createTokenizeBatches(tokenizeRequest, tokenizeBatchSize); try { List> futures = - this.tokenizeBatchFutures(executor, batches); + this.tokenizeBatchFutures(executor, batches, interceptor); try { CompletableFuture allFutures = CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])); allFutures.join(); @@ -512,33 +595,35 @@ private TokenizeResponse processTokenizeSync( private List> tokenizeBatchFutures( ExecutorService executor, - List batches) { + List batches, + RequestInterceptor interceptor) { List> futures = new ArrayList<>(); if (batches == null) return futures; + int totalBatches = batches.size(); for (int batchIndex = 0; batchIndex < batches.size(); batchIndex++) { final int index = batchIndex; com.skyflow.generated.rest.resources.flowservice.requests.V1FlowTokenizeRequest batch = batches.get(index); + RequestContext ctx = new RequestContext("TOKENIZE", batchIndex, totalBatches); + if (interceptor != null) interceptor.intercept(ctx); CompletableFuture future = CompletableFuture - .supplyAsync(() -> processTokenizeBatch(batch), executor) + .supplyAsync(() -> processTokenizeBatch(batch, ctx), 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, + RequestContext ctx) { + return this.getRecordsApi().withRawResponse().tokenize(batch, buildRequestOptions(ctx)); } private void configureTokenizeConcurrencyAndBatchSize(int totalRequests) { @@ -608,12 +693,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, + RequestInterceptor interceptor ) 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, interceptor); CompletableFuture allFutures = CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])); allFutures.join(); @@ -636,7 +722,8 @@ private com.skyflow.vault.data.InsertResponse processSync( private DetokenizeResponse processDetokenizeSync( com.skyflow.generated.rest.resources.flowservice.requests.V1FlowDetokenizeRequest detokenizeRequest, - List originalTokens + List originalTokens, + RequestInterceptor interceptor ) throws ExecutionException, InterruptedException, SkyflowException { LogUtil.printInfoLog(InfoLogs.PROCESSING_BATCHES.getLog()); List errorTokens = Collections.synchronizedList(new ArrayList<>()); @@ -644,12 +731,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, interceptor); 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 +760,22 @@ private DetokenizeResponse processDetokenizeSync( return response; } - private List> detokenizeBatchFutures(ExecutorService executor, List batches, List errorTokens) { + private List> detokenizeBatchFutures( + ExecutorService executor, + List batches, + List errorTokens, + RequestInterceptor interceptor) { List> futures = new ArrayList<>(); try { - + int totalBatches = batches.size(); for (int batchIndex = 0; batchIndex < batches.size(); batchIndex++) { com.skyflow.generated.rest.resources.flowservice.requests.V1FlowDetokenizeRequest batch = batches.get(batchIndex); int batchNumber = batchIndex; + RequestContext ctx = new RequestContext("DETOKENIZE", batchIndex, totalBatches); + if (interceptor != null) interceptor.intercept(ctx); CompletableFuture future = CompletableFuture - .supplyAsync(() -> processDetokenizeBatch(batch), executor) - .thenApply(response -> Utils.formatDetokenizeResponse(response, batchNumber, detokenizeBatchSize)) + .supplyAsync(() -> processDetokenizeBatch(batch, ctx), executor) + .thenApply(response -> Utils.formatDetokenizeResponse(response.body(), batchNumber, detokenizeBatchSize, response.headers())) .exceptionally(ex -> { errorTokens.addAll(Utils.handleDetokenizeBatchException(ex, batch, batchNumber, detokenizeBatchSize)); return null; @@ -696,31 +789,33 @@ 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, + RequestContext ctx) { + return this.getRecordsApi().withRawResponse().detokenize(batch, buildRequestOptions(ctx)); } - private List> - insertBatchFutures( + private List> insertBatchFutures( com.skyflow.generated.rest.resources.flowservice.requests.V1InsertRequest insertRequest, - List errorRecords) { + List errorRecords, + RequestInterceptor interceptor) { List records = insertRequest.getRecords().get(); ExecutorService executor = Executors.newFixedThreadPool(insertConcurrencyLimit); List> batches = Utils.createBatches(records, insertBatchSize); List> futures = new ArrayList<>(); V1Upsert upsert = insertRequest.getUpsert().isPresent() ? insertRequest.getUpsert().get() : null; + int totalBatches = batches.size(); try { for (int batchIndex = 0; batchIndex < batches.size(); batchIndex++) { List batch = batches.get(batchIndex); int batchNumber = batchIndex; + RequestContext ctx = new RequestContext("INSERT", batchIndex, totalBatches); + if (interceptor != null) interceptor.intercept(ctx); 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, ctx), 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 +828,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, RequestContext ctx) { 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(ctx)); } 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..622f531e --- /dev/null +++ b/v3/src/main/java/com/skyflow/vault/data/DeleteTokensOptions.java @@ -0,0 +1,30 @@ +package com.skyflow.vault.data; + +public final class DeleteTokensOptions { + private final RequestInterceptor interceptor; + + private DeleteTokensOptions(Builder builder) { + this.interceptor = builder.interceptor; + } + + public RequestInterceptor getInterceptor() { + return interceptor; + } + + public static Builder builder() { + return new Builder(); + } + + public static final class Builder { + private RequestInterceptor interceptor; + + public Builder interceptor(RequestInterceptor interceptor) { + this.interceptor = interceptor; + return this; + } + + public DeleteTokensOptions build() { + return new DeleteTokensOptions(this); + } + } +} 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..267bd9bd --- /dev/null +++ b/v3/src/main/java/com/skyflow/vault/data/DetokenizeOptions.java @@ -0,0 +1,30 @@ +package com.skyflow.vault.data; + +public final class DetokenizeOptions { + private final RequestInterceptor interceptor; + + private DetokenizeOptions(Builder builder) { + this.interceptor = builder.interceptor; + } + + public RequestInterceptor getInterceptor() { + return interceptor; + } + + public static Builder builder() { + return new Builder(); + } + + public static final class Builder { + private RequestInterceptor interceptor; + + public Builder interceptor(RequestInterceptor interceptor) { + this.interceptor = interceptor; + return this; + } + + public DetokenizeOptions build() { + return new DetokenizeOptions(this); + } + } +} 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..73262b98 --- /dev/null +++ b/v3/src/main/java/com/skyflow/vault/data/InsertOptions.java @@ -0,0 +1,30 @@ +package com.skyflow.vault.data; + +public final class InsertOptions { + private final RequestInterceptor interceptor; + + private InsertOptions(Builder builder) { + this.interceptor = builder.interceptor; + } + + public RequestInterceptor getInterceptor() { + return interceptor; + } + + public static Builder builder() { + return new Builder(); + } + + public static final class Builder { + private RequestInterceptor interceptor; + + public Builder interceptor(RequestInterceptor interceptor) { + this.interceptor = interceptor; + return this; + } + + public InsertOptions build() { + return new InsertOptions(this); + } + } +} diff --git a/v3/src/main/java/com/skyflow/vault/data/RequestContext.java b/v3/src/main/java/com/skyflow/vault/data/RequestContext.java new file mode 100644 index 00000000..07d6fda3 --- /dev/null +++ b/v3/src/main/java/com/skyflow/vault/data/RequestContext.java @@ -0,0 +1,32 @@ +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 RequestContext { + private final String operation; + private final int batchIndex; + private final int totalBatches; + private final Map headers = new HashMap<>(); + + public RequestContext(String operation, int batchIndex, int totalBatches) { + this.operation = operation; + this.batchIndex = batchIndex; + this.totalBatches = totalBatches; + } + + public String getOperation() { return operation; } + public int getBatchIndex() { return batchIndex; } + public int getTotalBatches() { return totalBatches; } + + public void addHeader(CustomHeaderKey key, String value) { + headers.put(key, value); + } + + public Map getHeaders() { + return Collections.unmodifiableMap(headers); + } +} diff --git a/v3/src/main/java/com/skyflow/vault/data/RequestInterceptor.java b/v3/src/main/java/com/skyflow/vault/data/RequestInterceptor.java new file mode 100644 index 00000000..37f5261d --- /dev/null +++ b/v3/src/main/java/com/skyflow/vault/data/RequestInterceptor.java @@ -0,0 +1,6 @@ +package com.skyflow.vault.data; + +@FunctionalInterface +public interface RequestInterceptor { + void intercept(RequestContext context); +} 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..2ac5bcfd --- /dev/null +++ b/v3/src/main/java/com/skyflow/vault/data/TokenizeOptions.java @@ -0,0 +1,30 @@ +package com.skyflow.vault.data; + +public final class TokenizeOptions { + private final RequestInterceptor interceptor; + + private TokenizeOptions(Builder builder) { + this.interceptor = builder.interceptor; + } + + public RequestInterceptor getInterceptor() { + return interceptor; + } + + public static Builder builder() { + return new Builder(); + } + + public static final class Builder { + private RequestInterceptor interceptor; + + public Builder interceptor(RequestInterceptor interceptor) { + this.interceptor = interceptor; + return this; + } + + public TokenizeOptions build() { + return new TokenizeOptions(this); + } + } +} 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..407ea055 100644 --- a/v3/src/test/java/com/skyflow/utils/validations/ValidationsTests.java +++ b/v3/src/test/java/com/skyflow/utils/validations/ValidationsTests.java @@ -167,6 +167,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() 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..f8428aed 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,14 @@ public void testDeleteTokensBatchFutures_catchBranchAddsErrorRecord() throws Exc List batches = null; Method method = VaultController.class.getDeclaredMethod( - "deleteTokensBatchFutures", ExecutorService.class, List.class); + "deleteTokensBatchFutures", ExecutorService.class, List.class, + com.skyflow.vault.data.RequestInterceptor.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, null); // errors are now returned via futures, not a shared list Assert.assertNotNull(futures); @@ -448,12 +449,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, + com.skyflow.vault.data.RequestInterceptor.class ); processSync.setAccessible(true); try { - processSync.invoke(controller, requestObj, tokens); + processSync.invoke(controller, requestObj, tokens, null); } 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..ac7b59c8 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,22 @@ import com.skyflow.enums.Env; import com.skyflow.utils.Constants; import com.skyflow.utils.validations.Validations; +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.RequestContext; +import com.skyflow.vault.data.RequestInterceptor; +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,6 +38,8 @@ import java.net.InetSocketAddress; import java.nio.charset.StandardCharsets; import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.concurrent.CompletableFuture; @@ -313,6 +326,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 +696,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, RequestInterceptor.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, null); Assert.assertTrue(errors.size() == 1); Assert.assertEquals(0, errors.get(0).getIndex()); @@ -672,12 +747,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, + RequestInterceptor.class ); processDetokenizeSync.setAccessible(true); try { - processDetokenizeSync.invoke(controller, requestObj, tokens); + processDetokenizeSync.invoke(controller, requestObj, tokens, null); } catch (java.lang.reflect.InvocationTargetException e) { Throwable cause = e.getCause(); assertTrue(cause instanceof SkyflowException || cause instanceof ExecutionException || cause instanceof RuntimeException); @@ -709,7 +785,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, + RequestInterceptor.class ); processDetokenizeSync.setAccessible(true); @@ -717,17 +794,485 @@ public void testProcessDetokenizeSyncErrorPath() throws Exception { "detokenizeBatchFutures", ExecutorService.class, List.class, - List.class + List.class, + RequestInterceptor.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, null); assertTrue(errors.size() == 1); assertEquals(0, errors.get(0).getIndex()); assertEquals(500, errors.get(0).getCode()); executor.shutdownNow(); } + + // ── 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, RequestInterceptor.class); + method.setAccessible(true); + ExecutorService executor = Executors.newFixedThreadPool(1); + @SuppressWarnings("unchecked") + List> futures = + (List>) method.invoke( + controller, executor, null, null); + 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, RequestInterceptor.class); + method.setAccessible(true); + ExecutorService executor = Executors.newFixedThreadPool(1); + @SuppressWarnings("unchecked") + List> futures = + (List>) method.invoke( + controller, executor, null, null); + 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()); + } + } + + // ── Interceptor invocation per batch ────────────────────────────────────── + + @Test + public void interceptor_isCalledOncePerBatch_inDeleteTokensBatchFutures() throws Exception { + VaultController controller = createController(); + setPrivateField(controller, "deleteTokensBatchSize", 2); + + java.util.concurrent.atomic.AtomicInteger callCount = new java.util.concurrent.atomic.AtomicInteger(0); + java.util.List capturedOps = java.util.Collections.synchronizedList(new ArrayList<>()); + RequestInterceptor interceptor = ctx -> { + callCount.incrementAndGet(); + capturedOps.add(ctx.getOperation()); + }; + + List tokens = java.util.Arrays.asList("t1", "t2", "t3", "t4"); + DeleteTokensRequest deleteRequest = DeleteTokensRequest.builder().tokens(tokens).build(); + Method getRequestBody = VaultController.class.getSuperclass() + .getDeclaredMethod("getDeleteTokensRequestBody", DeleteTokensRequest.class); + getRequestBody.setAccessible(true); + com.skyflow.generated.rest.resources.flowservice.requests.V1FlowDeleteTokenRequest requestObj = + (com.skyflow.generated.rest.resources.flowservice.requests.V1FlowDeleteTokenRequest) + getRequestBody.invoke(controller, deleteRequest); + + List batches = + com.skyflow.utils.Utils.createDeleteTokensBatches(requestObj, 2); + + Method method = VaultController.class.getDeclaredMethod( + "deleteTokensBatchFutures", ExecutorService.class, List.class, + RequestInterceptor.class); + method.setAccessible(true); + ExecutorService executor = Executors.newFixedThreadPool(1); + @SuppressWarnings("unchecked") + List> futures = + (List>) method.invoke( + controller, executor, batches, interceptor); + + // interceptor is called synchronously in the loop before supplyAsync + Assert.assertEquals("Interceptor should be called once per batch", batches.size(), callCount.get()); + for (String op : capturedOps) { + Assert.assertEquals("DELETE_TOKENS", op); + } + executor.shutdownNow(); + } + + @Test + public void interceptor_contextHasCorrectBatchIndexAndTotal() throws Exception { + VaultController controller = createController(); + setPrivateField(controller, "tokenizeBatchSize", 1); + + java.util.List capturedIndices = java.util.Collections.synchronizedList(new ArrayList<>()); + java.util.concurrent.atomic.AtomicInteger capturedTotal = new java.util.concurrent.atomic.AtomicInteger(-1); + RequestInterceptor interceptor = ctx -> { + capturedIndices.add(ctx.getBatchIndex()); + capturedTotal.set(ctx.getTotalBatches()); + }; + + ArrayList data = new ArrayList<>(); + data.add(com.skyflow.vault.data.TokenizeRecord.builder().value("v1").tokenGroupNames(Collections.singletonList("g1")).build()); + data.add(com.skyflow.vault.data.TokenizeRecord.builder().value("v2").tokenGroupNames(Collections.singletonList("g1")).build()); + com.skyflow.vault.data.TokenizeRequest tokenizeRequest = com.skyflow.vault.data.TokenizeRequest.builder().data(data).build(); + + Method getRequestBody = VaultController.class.getSuperclass() + .getDeclaredMethod("getTokenizeRequestBody", com.skyflow.vault.data.TokenizeRequest.class); + getRequestBody.setAccessible(true); + com.skyflow.generated.rest.resources.flowservice.requests.V1FlowTokenizeRequest requestObj = + (com.skyflow.generated.rest.resources.flowservice.requests.V1FlowTokenizeRequest) + getRequestBody.invoke(controller, tokenizeRequest); + + List batches = + com.skyflow.utils.Utils.createTokenizeBatches(requestObj, 1); + + Method method = VaultController.class.getDeclaredMethod( + "tokenizeBatchFutures", ExecutorService.class, List.class, + RequestInterceptor.class); + method.setAccessible(true); + ExecutorService executor = Executors.newFixedThreadPool(1); + method.invoke(controller, executor, batches, interceptor); + + // 2 items / batchSize 1 = 2 batches → indices [0, 1], total = 2 + Assert.assertEquals(java.util.Arrays.asList(0, 1), capturedIndices); + Assert.assertEquals(2, capturedTotal.get()); + executor.shutdownNow(); + } + + @Test + public void interceptor_operationIsTokenize_inTokenizeBatchFutures() throws Exception { + VaultController controller = createController(); + setPrivateField(controller, "tokenizeBatchSize", 1); + + java.util.List capturedOps = java.util.Collections.synchronizedList(new ArrayList<>()); + RequestInterceptor interceptor = ctx -> capturedOps.add(ctx.getOperation()); + + ArrayList data = new ArrayList<>(); + data.add(com.skyflow.vault.data.TokenizeRecord.builder().value("v1").tokenGroupNames(Collections.singletonList("g1")).build()); + data.add(com.skyflow.vault.data.TokenizeRecord.builder().value("v2").tokenGroupNames(Collections.singletonList("g1")).build()); + com.skyflow.vault.data.TokenizeRequest tokenizeRequest = com.skyflow.vault.data.TokenizeRequest.builder().data(data).build(); + + Method getRequestBody = VaultController.class.getSuperclass() + .getDeclaredMethod("getTokenizeRequestBody", com.skyflow.vault.data.TokenizeRequest.class); + getRequestBody.setAccessible(true); + com.skyflow.generated.rest.resources.flowservice.requests.V1FlowTokenizeRequest requestObj = + (com.skyflow.generated.rest.resources.flowservice.requests.V1FlowTokenizeRequest) + getRequestBody.invoke(controller, tokenizeRequest); + + List batches = + com.skyflow.utils.Utils.createTokenizeBatches(requestObj, 1); + + Method method = VaultController.class.getDeclaredMethod( + "tokenizeBatchFutures", ExecutorService.class, List.class, RequestInterceptor.class); + method.setAccessible(true); + ExecutorService executor = Executors.newFixedThreadPool(1); + method.invoke(controller, executor, batches, interceptor); + + Assert.assertEquals(batches.size(), capturedOps.size()); + for (String op : capturedOps) { + Assert.assertEquals("TOKENIZE", op); + } + executor.shutdownNow(); + } + + @Test + public void interceptor_batchIndexAndTotal_inDeleteTokensBatchFutures() throws Exception { + VaultController controller = createController(); + setPrivateField(controller, "deleteTokensBatchSize", 1); + + java.util.List capturedIndices = java.util.Collections.synchronizedList(new ArrayList<>()); + java.util.concurrent.atomic.AtomicInteger capturedTotal = new java.util.concurrent.atomic.AtomicInteger(-1); + RequestInterceptor interceptor = ctx -> { + capturedIndices.add(ctx.getBatchIndex()); + capturedTotal.set(ctx.getTotalBatches()); + }; + + List tokens = Arrays.asList("t1", "t2", "t3"); + DeleteTokensRequest deleteRequest = DeleteTokensRequest.builder().tokens(tokens).build(); + Method getRequestBody = VaultController.class.getSuperclass() + .getDeclaredMethod("getDeleteTokensRequestBody", DeleteTokensRequest.class); + getRequestBody.setAccessible(true); + com.skyflow.generated.rest.resources.flowservice.requests.V1FlowDeleteTokenRequest requestObj = + (com.skyflow.generated.rest.resources.flowservice.requests.V1FlowDeleteTokenRequest) + getRequestBody.invoke(controller, deleteRequest); + + List batches = + com.skyflow.utils.Utils.createDeleteTokensBatches(requestObj, 1); + + Method method = VaultController.class.getDeclaredMethod( + "deleteTokensBatchFutures", ExecutorService.class, List.class, RequestInterceptor.class); + method.setAccessible(true); + ExecutorService executor = Executors.newFixedThreadPool(1); + method.invoke(controller, executor, batches, interceptor); + + Assert.assertEquals(Arrays.asList(0, 1, 2), capturedIndices); + Assert.assertEquals(3, capturedTotal.get()); + executor.shutdownNow(); + } + + @Test + public void interceptor_isCalledOncePerBatch_inDetokenizeBatchFutures() throws Exception { + VaultController controller = createController(); + setPrivateField(controller, "detokenizeBatchSize", 2); + + java.util.concurrent.atomic.AtomicInteger callCount = new java.util.concurrent.atomic.AtomicInteger(0); + java.util.List capturedOps = java.util.Collections.synchronizedList(new ArrayList<>()); + RequestInterceptor interceptor = ctx -> { + callCount.incrementAndGet(); + capturedOps.add(ctx.getOperation()); + }; + + DetokenizeRequest request = DetokenizeRequest.builder().tokens(getTokens(4)).build(); + Method getRequestBody = VaultController.class.getSuperclass() + .getDeclaredMethod("getDetokenizeRequestBody", DetokenizeRequest.class); + getRequestBody.setAccessible(true); + com.skyflow.generated.rest.resources.flowservice.requests.V1FlowDetokenizeRequest requestObj = + (com.skyflow.generated.rest.resources.flowservice.requests.V1FlowDetokenizeRequest) + getRequestBody.invoke(controller, request); + + List batches = + com.skyflow.utils.Utils.createDetokenizeBatches(requestObj, 2); + + Method method = VaultController.class.getDeclaredMethod( + "detokenizeBatchFutures", ExecutorService.class, List.class, List.class, RequestInterceptor.class); + method.setAccessible(true); + ExecutorService executor = Executors.newFixedThreadPool(1); + List errors = new ArrayList<>(); + method.invoke(controller, executor, batches, errors, interceptor); + + Assert.assertEquals("Interceptor called once per batch", batches.size(), callCount.get()); + for (String op : capturedOps) { + Assert.assertEquals("DETOKENIZE", op); + } + executor.shutdownNow(); + } + + @Test + public void interceptor_isCalledOncePerBatch_inInsertBatchFutures() throws Exception { + VaultController controller = createController(); + setPrivateField(controller, "insertBatchSize", 2); + + java.util.concurrent.atomic.AtomicInteger callCount = new java.util.concurrent.atomic.AtomicInteger(0); + java.util.List capturedOps = java.util.Collections.synchronizedList(new ArrayList<>()); + RequestInterceptor interceptor = ctx -> { + callCount.incrementAndGet(); + capturedOps.add(ctx.getOperation()); + }; + + InsertRequest insertRequest = InsertRequest.builder() + .table("test-table") + .records(generateValues(4)) + .build(); + Method getRequestBody = VaultController.class.getSuperclass() + .getDeclaredMethod("getBulkInsertRequestBody", InsertRequest.class, VaultConfig.class); + getRequestBody.setAccessible(true); + com.skyflow.generated.rest.resources.flowservice.requests.V1InsertRequest requestObj = + (com.skyflow.generated.rest.resources.flowservice.requests.V1InsertRequest) + getRequestBody.invoke(controller, insertRequest, vaultConfig); + + Method method = VaultController.class.getDeclaredMethod( + "insertBatchFutures", + com.skyflow.generated.rest.resources.flowservice.requests.V1InsertRequest.class, + List.class, + RequestInterceptor.class); + method.setAccessible(true); + List errorRecords = new ArrayList<>(); + method.invoke(controller, requestObj, errorRecords, interceptor); + + // 4 records / batchSize 2 = 2 batches + Assert.assertEquals(2, callCount.get()); + for (String op : capturedOps) { + Assert.assertEquals("INSERT", op); + } + } + + // ── 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..60bd63dd 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,14 @@ public void testTokenizeBatchFutures_catchBranchAddsErrorRecord() throws Excepti List batches = null; Method method = VaultController.class.getDeclaredMethod( - "tokenizeBatchFutures", ExecutorService.class, List.class); + "tokenizeBatchFutures", ExecutorService.class, List.class, + com.skyflow.vault.data.RequestInterceptor.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, null); // errors are now returned via futures, not a shared list Assert.assertNotNull(futures); @@ -432,12 +433,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, + com.skyflow.vault.data.RequestInterceptor.class ); processSync.setAccessible(true); try { - processSync.invoke(controller, requestObj, data); + processSync.invoke(controller, requestObj, data, null); } 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..e561b978 --- /dev/null +++ b/v3/src/test/java/com/skyflow/vault/data/OptionsTests.java @@ -0,0 +1,69 @@ +package com.skyflow.vault.data; + +import org.junit.Assert; +import org.junit.Test; + +public class OptionsTests { + + // ── InsertOptions ───────────────────────────────────────────────────────── + + // ── DetokenizeOptions ───────────────────────────────────────────────────── + + // ── TokenizeOptions ─────────────────────────────────────────────────────── + + // ── DeleteTokensOptions ─────────────────────────────────────────────────── + + // ── Interceptor getters ─────────────────────────────────────────────────── + + @Test + public void insertOptions_defaultInterceptor_isNull() { + InsertOptions opts = InsertOptions.builder().build(); + Assert.assertNull(opts.getInterceptor()); + } + + @Test + public void insertOptions_interceptor_storedAndRetrieved() { + RequestInterceptor interceptor = ctx -> {}; + InsertOptions opts = InsertOptions.builder().interceptor(interceptor).build(); + Assert.assertSame(interceptor, opts.getInterceptor()); + } + + @Test + public void detokenizeOptions_defaultInterceptor_isNull() { + DetokenizeOptions opts = DetokenizeOptions.builder().build(); + Assert.assertNull(opts.getInterceptor()); + } + + @Test + public void detokenizeOptions_interceptor_storedAndRetrieved() { + RequestInterceptor interceptor = ctx -> {}; + DetokenizeOptions opts = DetokenizeOptions.builder().interceptor(interceptor).build(); + Assert.assertSame(interceptor, opts.getInterceptor()); + } + + @Test + public void tokenizeOptions_defaultInterceptor_isNull() { + TokenizeOptions opts = TokenizeOptions.builder().build(); + Assert.assertNull(opts.getInterceptor()); + } + + @Test + public void tokenizeOptions_interceptor_storedAndRetrieved() { + RequestInterceptor interceptor = ctx -> {}; + TokenizeOptions opts = TokenizeOptions.builder().interceptor(interceptor).build(); + Assert.assertSame(interceptor, opts.getInterceptor()); + } + + @Test + public void deleteTokensOptions_defaultInterceptor_isNull() { + DeleteTokensOptions opts = DeleteTokensOptions.builder().build(); + Assert.assertNull(opts.getInterceptor()); + } + + @Test + public void deleteTokensOptions_interceptor_storedAndRetrieved() { + RequestInterceptor interceptor = ctx -> {}; + DeleteTokensOptions opts = DeleteTokensOptions.builder().interceptor(interceptor).build(); + Assert.assertSame(interceptor, opts.getInterceptor()); + } +} diff --git a/v3/src/test/java/com/skyflow/vault/data/RequestContextTests.java b/v3/src/test/java/com/skyflow/vault/data/RequestContextTests.java new file mode 100644 index 00000000..1a582e9d --- /dev/null +++ b/v3/src/test/java/com/skyflow/vault/data/RequestContextTests.java @@ -0,0 +1,92 @@ +package com.skyflow.vault.data; + +import com.skyflow.enums.CustomHeaderKey; +import org.junit.Assert; +import org.junit.Test; + +import java.util.Map; + +public class RequestContextTests { + + @Test + public void constructor_setsOperationBatchIndexAndTotalBatches() { + RequestContext ctx = new RequestContext("INSERT", 2, 5); + Assert.assertEquals("INSERT", ctx.getOperation()); + Assert.assertEquals(2, ctx.getBatchIndex()); + Assert.assertEquals(5, ctx.getTotalBatches()); + } + + @Test + public void constructor_headersMapIsInitiallyEmpty() { + RequestContext ctx = new RequestContext("DETOKENIZE", 0, 1); + Assert.assertTrue(ctx.getHeaders().isEmpty()); + } + + @Test + public void addHeader_singleEntry_appearsInGetHeaders() { + RequestContext ctx = new RequestContext("TOKENIZE", 0, 1); + ctx.addHeader(CustomHeaderKey.RequestIDHeader, "req-abc"); + Map headers = ctx.getHeaders(); + Assert.assertEquals(1, headers.size()); + Assert.assertEquals("req-abc", headers.get(CustomHeaderKey.RequestIDHeader)); + } + + @Test + public void addHeader_multipleEntries_allPresentInGetHeaders() { + RequestContext ctx = new RequestContext("DELETE_TOKENS", 1, 3); + ctx.addHeader(CustomHeaderKey.SkyflowAccountID, "acct-1"); + ctx.addHeader(CustomHeaderKey.SkyflowAccountName, "my-account"); + ctx.addHeader(CustomHeaderKey.RequestIDHeader, "req-xyz"); + Map headers = ctx.getHeaders(); + Assert.assertEquals(3, headers.size()); + Assert.assertEquals("acct-1", headers.get(CustomHeaderKey.SkyflowAccountID)); + Assert.assertEquals("my-account", headers.get(CustomHeaderKey.SkyflowAccountName)); + Assert.assertEquals("req-xyz", headers.get(CustomHeaderKey.RequestIDHeader)); + } + + @Test + public void getHeaders_returnsUnmodifiableView() { + RequestContext ctx = new RequestContext("INSERT", 0, 1); + ctx.addHeader(CustomHeaderKey.RequestIDHeader, "val"); + Map headers = ctx.getHeaders(); + try { + headers.put(CustomHeaderKey.SkyflowAccountID, "extra"); + Assert.fail("Expected UnsupportedOperationException"); + } catch (UnsupportedOperationException e) { + // expected — map is unmodifiable + } + } + + @Test + public void addHeader_overwrites_existingKey() { + RequestContext ctx = new RequestContext("INSERT", 0, 1); + ctx.addHeader(CustomHeaderKey.RequestIDHeader, "first"); + ctx.addHeader(CustomHeaderKey.RequestIDHeader, "second"); + Assert.assertEquals("second", ctx.getHeaders().get(CustomHeaderKey.RequestIDHeader)); + } + + @Test + public void interceptor_receivesCorrectContext() { + RequestContext[] captured = new RequestContext[1]; + RequestInterceptor interceptor = ctx -> captured[0] = ctx; + + RequestContext ctx = new RequestContext("TOKENIZE", 3, 7); + interceptor.intercept(ctx); + + Assert.assertSame(ctx, captured[0]); + Assert.assertEquals("TOKENIZE", captured[0].getOperation()); + Assert.assertEquals(3, captured[0].getBatchIndex()); + Assert.assertEquals(7, captured[0].getTotalBatches()); + } + + @Test + public void interceptor_canAddHeadersToContext() { + RequestInterceptor interceptor = ctx -> + ctx.addHeader(CustomHeaderKey.SkyflowAccountID, "injected-account"); + + RequestContext ctx = new RequestContext("INSERT", 0, 2); + interceptor.intercept(ctx); + + Assert.assertEquals("injected-account", ctx.getHeaders().get(CustomHeaderKey.SkyflowAccountID)); + } +} 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()); + } +}