diff --git a/.openapi-generator/FILES b/.openapi-generator/FILES index 0237b40..c7936ad 100644 --- a/.openapi-generator/FILES +++ b/.openapi-generator/FILES @@ -131,7 +131,11 @@ src/main/java/dev/openfga/sdk/api/client/ApiResponse.java src/main/java/dev/openfga/sdk/api/client/ClientAssertion.java src/main/java/dev/openfga/sdk/api/client/HttpRequestAttempt.java src/main/java/dev/openfga/sdk/api/client/OpenFgaClient.java +src/main/java/dev/openfga/sdk/api/client/model/ClientBatchCheckClientResponse.java +src/main/java/dev/openfga/sdk/api/client/model/ClientBatchCheckItem.java +src/main/java/dev/openfga/sdk/api/client/model/ClientBatchCheckRequest.java src/main/java/dev/openfga/sdk/api/client/model/ClientBatchCheckResponse.java +src/main/java/dev/openfga/sdk/api/client/model/ClientBatchCheckSingleResponse.java src/main/java/dev/openfga/sdk/api/client/model/ClientCheckRequest.java src/main/java/dev/openfga/sdk/api/client/model/ClientCheckResponse.java src/main/java/dev/openfga/sdk/api/client/model/ClientCreateStoreResponse.java @@ -163,6 +167,7 @@ src/main/java/dev/openfga/sdk/api/client/model/ClientWriteResponse.java src/main/java/dev/openfga/sdk/api/configuration/AdditionalHeadersSupplier.java src/main/java/dev/openfga/sdk/api/configuration/ApiToken.java src/main/java/dev/openfga/sdk/api/configuration/BaseConfiguration.java +src/main/java/dev/openfga/sdk/api/configuration/ClientBatchCheckClientOptions.java src/main/java/dev/openfga/sdk/api/configuration/ClientBatchCheckOptions.java src/main/java/dev/openfga/sdk/api/configuration/ClientCheckOptions.java src/main/java/dev/openfga/sdk/api/configuration/ClientConfiguration.java @@ -286,6 +291,7 @@ src/main/java/dev/openfga/sdk/errors/FgaApiRateLimitExceededError.java src/main/java/dev/openfga/sdk/errors/FgaApiValidationError.java src/main/java/dev/openfga/sdk/errors/FgaError.java src/main/java/dev/openfga/sdk/errors/FgaInvalidParameterException.java +src/main/java/dev/openfga/sdk/errors/FgaValidationError.java src/main/java/dev/openfga/sdk/errors/HttpStatusCode.java src/main/java/dev/openfga/sdk/telemetry/Attribute.java src/main/java/dev/openfga/sdk/telemetry/Attributes.java diff --git a/CHANGELOG.md b/CHANGELOG.md index 7360ee8..0819828 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,8 +2,13 @@ ## [Unreleased](https://github.com/openfga/java-sdk/compare/v0.7.2...HEAD) +- feat!: add support for server-side `BatchCheck` method - feat: add support for `start_time` parameter in `ReadChanges` endpoint +BREAKING CHANGES: + +- Usage of the existing `batchCheck` method should now use the `clientBatchCheck` method. + ## v0.7.2 ### [0.7.2](https://github.com/openfga/java-sdk/compare/v0.7.1...v0.7.2) (2024-12-18) diff --git a/README.md b/README.md index 86fd5ba..f4d7e92 100644 --- a/README.md +++ b/README.md @@ -604,7 +604,7 @@ var response = fgaClient.check(request, options).get(); Run a set of [checks](#check). Batch Check will return `allowed: false` if it encounters an error, and will return the error in the body. If 429s or 5xxs are encountered, the underlying check will retry up to 3 times before giving up. -> Passing `ClientBatchCheckOptions` is optional. All fields of `ClientBatchCheckOptions` are optional. +> Passing `ClientBatchCheckClientOptions` is optional. All fields of `ClientBatchCheckClientOptions` are optional. ```java var request = List.of( @@ -637,7 +637,7 @@ var request = List.of( .relation("deleter") ._object("document:0192ab2a-d83f-756d-9397-c5ed9f3cb69a") ); -var options = new ClientBatchCheckOptions() +var options = new ClientBatchCheckClientOptions() .additionalHeaders(Map.of("Some-Http-Header", "Some value")) // You can rely on the model id set in the configuration or override it for this specific request .authorizationModelId("01GXSA8YR785C4FYS3C0RTG7B1") @@ -1093,7 +1093,7 @@ If you have found a bug or if you have a feature request, please report them on ### Pull Requests -While we accept Pull Requests on this repository, the SDKs are autogenerated so please consider additionally submitting your Pull Requests to the [sdk-generator](https://github.com/openfga/sdk-generator) and linking the two PRs together and to the corresponding issue. This will greatly assist the OpenFGA team in being able to give timely reviews as well as deploying fixes and updates to our other SDKs as well. +While we accept Pull Requests on this repository, the SDKs are autogenerated so please consider additionally submitting your Pull Requests to the [sdk-generator](https://github.com/openfga/sdk-generator) and linking the two PRs together and to the corresponding issue. This will greatly assist the OpenFGA team in being able to give timely reviews as well as deploying fixes and updates to our other SDKs as well. ## Author diff --git a/build.gradle b/build.gradle index 4148c9d..c26049a 100644 --- a/build.gradle +++ b/build.gradle @@ -4,7 +4,7 @@ plugins { // Quality id 'jacoco' id 'jvm-test-suite' - id 'com.diffplug.spotless' version '7.0.2' + id 'com.diffplug.spotless' version '6.25.0' // IDE id 'idea' @@ -66,7 +66,7 @@ dependencies { implementation "com.fasterxml.jackson.core:jackson-databind:$jackson_version" implementation "com.fasterxml.jackson.datatype:jackson-datatype-jsr310:$jackson_version" implementation "org.openapitools:jackson-databind-nullable:0.2.6" - implementation platform("io.opentelemetry:opentelemetry-bom:1.46.0") + implementation platform("io.opentelemetry:opentelemetry-bom:1.45.0") implementation "io.opentelemetry:opentelemetry-api" } @@ -78,9 +78,9 @@ testing { dependencies { implementation project() implementation "org.junit.jupiter:junit-jupiter:$junit_version" - implementation "org.mockito:mockito-core:5.15.2" + implementation "org.mockito:mockito-core:5.14.2" runtimeOnly "org.junit.platform:junit-platform-launcher" - implementation "org.wiremock:wiremock:3.11.0" + implementation "org.wiremock:wiremock:3.10.0" // This test-only dependency is convenient but not widely used. // Review project activity before updating the version here. diff --git a/example/example1/build.gradle b/example/example1/build.gradle index 9ca41a6..d4c6aa7 100644 --- a/example/example1/build.gradle +++ b/example/example1/build.gradle @@ -1,7 +1,7 @@ plugins { id 'application' - id 'com.diffplug.spotless' version '7.0.2' - id 'org.jetbrains.kotlin.jvm' version '2.1.10' + id 'com.diffplug.spotless' version '6.25.0' + id 'org.jetbrains.kotlin.jvm' version '2.1.0' } application { diff --git a/src/main/java/dev/openfga/sdk/api/client/OpenFgaClient.java b/src/main/java/dev/openfga/sdk/api/client/OpenFgaClient.java index 51c3f51..e4b5210 100644 --- a/src/main/java/dev/openfga/sdk/api/client/OpenFgaClient.java +++ b/src/main/java/dev/openfga/sdk/api/client/OpenFgaClient.java @@ -23,9 +23,11 @@ import java.util.ArrayList; import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.concurrent.*; import java.util.function.Consumer; import java.util.stream.Collectors; +import java.util.stream.IntStream; import java.util.stream.Stream; public class OpenFgaClient { @@ -36,6 +38,7 @@ public class OpenFgaClient { private static final String CLIENT_BULK_REQUEST_ID_HEADER = "X-OpenFGA-Client-Bulk-Request-Id"; private static final String CLIENT_METHOD_HEADER = "X-OpenFGA-Client-Method"; private static final int DEFAULT_MAX_METHOD_PARALLEL_REQS = 10; + private static final int DEFAULT_MAX_BATCH_SIZE = 50; public OpenFgaClient(ClientConfiguration configuration) throws FgaInvalidParameterException { this(configuration, new ApiClient()); @@ -574,9 +577,9 @@ public CompletableFuture check(ClientCheckRequest request, * * @throws FgaInvalidParameterException When the Store ID is null, empty, or whitespace */ - public CompletableFuture> batchCheck(List requests) + public CompletableFuture> clientBatchCheck(List requests) throws FgaInvalidParameterException { - return batchCheck(requests, null); + return clientBatchCheck(requests, null); } /** @@ -584,19 +587,19 @@ public CompletableFuture> batchCheck(List> batchCheck( - List requests, ClientBatchCheckOptions batchCheckOptions) + public CompletableFuture> clientBatchCheck( + List requests, ClientBatchCheckClientOptions batchCheckOptions) throws FgaInvalidParameterException { configuration.assertValid(); configuration.assertValidStoreId(); var options = batchCheckOptions != null ? batchCheckOptions - : new ClientBatchCheckOptions().maxParallelRequests(DEFAULT_MAX_METHOD_PARALLEL_REQS); + : new ClientBatchCheckClientOptions().maxParallelRequests(DEFAULT_MAX_METHOD_PARALLEL_REQS); if (options.getAdditionalHeaders() == null) { options.additionalHeaders(new HashMap<>()); } - options.getAdditionalHeaders().putIfAbsent(CLIENT_METHOD_HEADER, "BatchCheck"); + options.getAdditionalHeaders().putIfAbsent(CLIENT_METHOD_HEADER, "ClientBatchCheck"); options.getAdditionalHeaders() .putIfAbsent(CLIENT_BULK_REQUEST_ID_HEADER, randomUUID().toString()); @@ -606,13 +609,13 @@ public CompletableFuture> batchCheck( var executor = Executors.newScheduledThreadPool(maxParallelRequests); var latch = new CountDownLatch(requests.size()); - var responses = new ConcurrentLinkedQueue(); + var responses = new ConcurrentLinkedQueue(); final var clientCheckOptions = options.asClientCheckOptions(); Consumer singleClientCheckRequest = request -> call(() -> this.check(request, clientCheckOptions)) - .handleAsync(ClientBatchCheckResponse.asyncHandler(request)) + .handleAsync(ClientBatchCheckClientResponse.asyncHandler(request)) .thenAccept(responses::add) .thenRun(latch::countDown); @@ -627,6 +630,117 @@ public CompletableFuture> batchCheck( } } + /** + * BatchCheck - Run a set of checks (evaluates) + * + * @throws FgaInvalidParameterException When the Store ID is null, empty, or whitespace + */ + public CompletableFuture batchCheck(ClientBatchCheckRequest request) + throws FgaInvalidParameterException, FgaValidationError { + return batchCheck(request, null); + } + + /** + * BatchCheck - Run a set of checks (evaluates) + * + * @throws FgaInvalidParameterException When the Store ID is null, empty, or whitespace + */ + public CompletableFuture batchCheck( + ClientBatchCheckRequest requests, ClientBatchCheckOptions batchCheckOptions) + throws FgaInvalidParameterException, FgaValidationError { + configuration.assertValid(); + configuration.assertValidStoreId(); + + var options = batchCheckOptions != null + ? batchCheckOptions + : new ClientBatchCheckOptions() + .maxParallelRequests(DEFAULT_MAX_METHOD_PARALLEL_REQS) + .maxBatchSize(DEFAULT_MAX_BATCH_SIZE); + if (options.getAdditionalHeaders() == null) { + options.additionalHeaders(new HashMap<>()); + } + options.getAdditionalHeaders().putIfAbsent(CLIENT_METHOD_HEADER, "BatchCheck"); + options.getAdditionalHeaders() + .putIfAbsent(CLIENT_BULK_REQUEST_ID_HEADER, randomUUID().toString()); + + Map correlationIdToCheck = new HashMap<>(); + + List collect = new ArrayList<>(); + for (ClientBatchCheckItem check : requests.getChecks()) { + String correlationId = check.getCorrelationId(); + correlationId = correlationId == null || correlationId.isBlank() + ? randomUUID().toString() + : correlationId; + + BatchCheckItem batchCheckItem = new BatchCheckItem() + .tupleKey(new CheckRequestTupleKey() + .user(check.getUser()) + .relation(check.getRelation()) + ._object(check.getObject())) + .context(check.getContext()) + .correlationId(correlationId); + + List contextualTuples = check.getContextualTuples(); + if (contextualTuples != null && !contextualTuples.isEmpty()) { + batchCheckItem.contextualTuples(ClientTupleKey.asContextualTupleKeys(contextualTuples)); + } + + collect.add(batchCheckItem); + + if (correlationIdToCheck.containsKey(correlationId)) { + throw new FgaValidationError( + "correlationId", "When calling batchCheck, correlation IDs must be unique"); + } + + correlationIdToCheck.put(correlationId, check); + } + + int maxBatchSize = options.getMaxBatchSize() != null ? options.getMaxBatchSize() : DEFAULT_MAX_BATCH_SIZE; + List> batchedChecks = IntStream.range( + 0, (collect.size() + maxBatchSize - 1) / maxBatchSize) + .mapToObj(i -> collect.subList(i * maxBatchSize, Math.min((i + 1) * maxBatchSize, collect.size()))) + .collect(Collectors.toList()); + + int maxParallelRequests = options.getMaxParallelRequests() != null + ? options.getMaxParallelRequests() + : DEFAULT_MAX_METHOD_PARALLEL_REQS; + var executor = Executors.newScheduledThreadPool(maxParallelRequests); + var latch = new CountDownLatch(batchedChecks.size()); + + var responses = new ConcurrentLinkedQueue(); + + var override = new ConfigurationOverride().addHeaders(options); + + Consumer> singleBatchCheckRequest = request -> call(() -> + api.batchCheck(configuration.getStoreId(), new BatchCheckRequest().checks(request), override)) + .handleAsync((batchCheckResponseApiResponse, throwable) -> { + Map response = + batchCheckResponseApiResponse.getData().getResult(); + + List batchResults = new ArrayList<>(); + response.forEach((key, result) -> { + boolean allowed = Boolean.TRUE.equals(result.getAllowed()); + ClientBatchCheckItem checkItem = correlationIdToCheck.get(key); + var singleResponse = + new ClientBatchCheckSingleResponse(allowed, checkItem, key, result.getError()); + batchResults.add(singleResponse); + }); + return batchResults; + }) + .thenAccept(responses::addAll) + .thenRun(latch::countDown); + + try { + batchedChecks.forEach(batch -> executor.execute(() -> singleBatchCheckRequest.accept(batch))); + latch.await(); + return CompletableFuture.completedFuture(new ClientBatchCheckResponse(new ArrayList<>(responses))); + } catch (Exception e) { + return CompletableFuture.failedFuture(e); + } finally { + executor.shutdown(); + } + } + /** * Expand - Expands the relationships in userset tree format (evaluates) * @@ -764,7 +878,7 @@ public CompletableFuture listRelations( .context(request.getContext())) .collect(Collectors.toList()); - return this.batchCheck(batchCheckRequests, options.asClientBatchCheckOptions()) + return this.clientBatchCheck(batchCheckRequests, options.asClientBatchCheckClientOptions()) .thenCompose(responses -> call(() -> ClientListRelationsResponse.fromBatchCheckResponses(responses))); } diff --git a/src/main/java/dev/openfga/sdk/api/client/model/ClientBatchCheckClientResponse.java b/src/main/java/dev/openfga/sdk/api/client/model/ClientBatchCheckClientResponse.java new file mode 100644 index 0000000..8c14d34 --- /dev/null +++ b/src/main/java/dev/openfga/sdk/api/client/model/ClientBatchCheckClientResponse.java @@ -0,0 +1,103 @@ +/* + * OpenFGA + * A high performance and flexible authorization/permission engine built for developers and inspired by Google Zanzibar. + * + * The version of the OpenAPI document: 1.x + * Contact: community@openfga.dev + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + +package dev.openfga.sdk.api.client.model; + +import dev.openfga.sdk.api.model.CheckResponse; +import dev.openfga.sdk.errors.FgaError; +import java.util.List; +import java.util.Map; +import java.util.function.BiFunction; + +public class ClientBatchCheckClientResponse extends CheckResponse { + private final ClientCheckRequest request; + private final Throwable throwable; + private final Integer statusCode; + private final Map> headers; + private final String rawResponse; + + public ClientBatchCheckClientResponse( + ClientCheckRequest request, ClientCheckResponse clientCheckResponse, Throwable throwable) { + this.request = request; + this.throwable = throwable; + + if (clientCheckResponse != null) { + this.statusCode = clientCheckResponse.getStatusCode(); + this.headers = clientCheckResponse.getHeaders(); + this.rawResponse = clientCheckResponse.getRawResponse(); + this.setAllowed(clientCheckResponse.getAllowed()); + this.setResolution(clientCheckResponse.getResolution()); + } else if (throwable instanceof FgaError) { + FgaError error = (FgaError) throwable; + this.statusCode = error.getStatusCode(); + this.headers = error.getResponseHeaders().map(); + this.rawResponse = error.getResponseData(); + } else { + // Should be unreachable, but required for type completion + this.statusCode = null; + this.headers = null; + this.rawResponse = null; + } + } + + public ClientCheckRequest getRequest() { + return request; + } + + /** + * Returns the result of the check. + *

+ * If the HTTP request was unsuccessful, this result will be null. If this is the case, you can examine the + * original request with {@link ClientBatchCheckClientResponse#getRequest()} and the exception with + * {@link ClientBatchCheckClientResponse#getThrowable()}. + * + * @return the check result. Is null if the HTTP request was unsuccessful. + */ + @Override + public Boolean getAllowed() { + return super.getAllowed(); + } + + /** + * Returns the caught exception if the HTTP request was unsuccessful. + *

+ * If the HTTP request was unsuccessful, this result will be null. If this is the case, you can examine the + * original request with {@link ClientBatchCheckClientResponse#getRequest()} and the exception with + * {@link ClientBatchCheckClientResponse#getThrowable()}. + * + * @return the caught exception. Is null if the HTTP request was successful. + */ + public Throwable getThrowable() { + return throwable; + } + + public int getStatusCode() { + return statusCode; + } + + public Map> getHeaders() { + return headers; + } + + public String getRawResponse() { + return rawResponse; + } + + public String getRelation() { + return request == null ? null : request.getRelation(); + } + + public static BiFunction asyncHandler( + ClientCheckRequest request) { + return (response, throwable) -> new ClientBatchCheckClientResponse(request, response, throwable); + } +} diff --git a/src/main/java/dev/openfga/sdk/api/client/model/ClientBatchCheckItem.java b/src/main/java/dev/openfga/sdk/api/client/model/ClientBatchCheckItem.java new file mode 100644 index 0000000..9438abd --- /dev/null +++ b/src/main/java/dev/openfga/sdk/api/client/model/ClientBatchCheckItem.java @@ -0,0 +1,78 @@ +/* + * OpenFGA + * A high performance and flexible authorization/permission engine built for developers and inspired by Google Zanzibar. + * + * The version of the OpenAPI document: 1.x + * Contact: community@openfga.dev + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + +package dev.openfga.sdk.api.client.model; + +import java.util.List; + +public class ClientBatchCheckItem { + private String user; + private String relation; + private String _object; + private List contextualTuples; + private Object context; + private String correlationId; + + public ClientBatchCheckItem user(String user) { + this.user = user; + return this; + } + + public String getUser() { + return user; + } + + public ClientBatchCheckItem relation(String relation) { + this.relation = relation; + return this; + } + + public String getRelation() { + return relation; + } + + public ClientBatchCheckItem _object(String _object) { + this._object = _object; + return this; + } + + public String getObject() { + return _object; + } + + public ClientBatchCheckItem contextualTuples(List contextualTuples) { + this.contextualTuples = contextualTuples; + return this; + } + + public List getContextualTuples() { + return contextualTuples; + } + + public ClientBatchCheckItem context(Object context) { + this.context = context; + return this; + } + + public Object getContext() { + return context; + } + + public ClientBatchCheckItem correlationId(String correlationId) { + this.correlationId = correlationId; + return this; + } + + public String getCorrelationId() { + return correlationId; + } +} diff --git a/src/main/java/dev/openfga/sdk/api/client/model/ClientBatchCheckRequest.java b/src/main/java/dev/openfga/sdk/api/client/model/ClientBatchCheckRequest.java new file mode 100644 index 0000000..b1d3df0 --- /dev/null +++ b/src/main/java/dev/openfga/sdk/api/client/model/ClientBatchCheckRequest.java @@ -0,0 +1,32 @@ +/* + * OpenFGA + * A high performance and flexible authorization/permission engine built for developers and inspired by Google Zanzibar. + * + * The version of the OpenAPI document: 1.x + * Contact: community@openfga.dev + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + +package dev.openfga.sdk.api.client.model; + +import java.util.List; + +public class ClientBatchCheckRequest { + private List checks; + + public static ClientBatchCheckRequest ofChecks(List checks) { + return new ClientBatchCheckRequest().checks(checks); + } + + public ClientBatchCheckRequest checks(List checks) { + this.checks = checks; + return this; + } + + public List getChecks() { + return checks; + } +} diff --git a/src/main/java/dev/openfga/sdk/api/client/model/ClientBatchCheckResponse.java b/src/main/java/dev/openfga/sdk/api/client/model/ClientBatchCheckResponse.java index dfc4661..39546b9 100644 --- a/src/main/java/dev/openfga/sdk/api/client/model/ClientBatchCheckResponse.java +++ b/src/main/java/dev/openfga/sdk/api/client/model/ClientBatchCheckResponse.java @@ -12,92 +12,16 @@ package dev.openfga.sdk.api.client.model; -import dev.openfga.sdk.api.model.CheckResponse; -import dev.openfga.sdk.errors.FgaError; import java.util.List; -import java.util.Map; -import java.util.function.BiFunction; -public class ClientBatchCheckResponse extends CheckResponse { - private final ClientCheckRequest request; - private final Throwable throwable; - private final Integer statusCode; - private final Map> headers; - private final String rawResponse; +public class ClientBatchCheckResponse { + private final List result; - public ClientBatchCheckResponse( - ClientCheckRequest request, ClientCheckResponse clientCheckResponse, Throwable throwable) { - this.request = request; - this.throwable = throwable; - - if (clientCheckResponse != null) { - this.statusCode = clientCheckResponse.getStatusCode(); - this.headers = clientCheckResponse.getHeaders(); - this.rawResponse = clientCheckResponse.getRawResponse(); - this.setAllowed(clientCheckResponse.getAllowed()); - this.setResolution(clientCheckResponse.getResolution()); - } else if (throwable instanceof FgaError) { - FgaError error = (FgaError) throwable; - this.statusCode = error.getStatusCode(); - this.headers = error.getResponseHeaders().map(); - this.rawResponse = error.getResponseData(); - } else { - // Should be unreachable, but required for type completion - this.statusCode = null; - this.headers = null; - this.rawResponse = null; - } - } - - public ClientCheckRequest getRequest() { - return request; - } - - /** - * Returns the result of the check. - *

- * If the HTTP request was unsuccessful, this result will be null. If this is the case, you can examine the - * original request with {@link ClientBatchCheckResponse#getRequest()} and the exception with - * {@link ClientBatchCheckResponse#getThrowable()}. - * - * @return the check result. Is null if the HTTP request was unsuccessful. - */ - @Override - public Boolean getAllowed() { - return super.getAllowed(); - } - - /** - * Returns the caught exception if the HTTP request was unsuccessful. - *

- * If the HTTP request was unsuccessful, this result will be null. If this is the case, you can examine the - * original request with {@link ClientBatchCheckResponse#getRequest()} and the exception with - * {@link ClientBatchCheckResponse#getThrowable()}. - * - * @return the caught exception. Is null if the HTTP request was successful. - */ - public Throwable getThrowable() { - return throwable; - } - - public int getStatusCode() { - return statusCode; - } - - public Map> getHeaders() { - return headers; - } - - public String getRawResponse() { - return rawResponse; - } - - public String getRelation() { - return request == null ? null : request.getRelation(); + public ClientBatchCheckResponse(List result) { + this.result = result; } - public static BiFunction asyncHandler( - ClientCheckRequest request) { - return (response, throwable) -> new ClientBatchCheckResponse(request, response, throwable); + public List getResult() { + return result; } } diff --git a/src/main/java/dev/openfga/sdk/api/client/model/ClientBatchCheckSingleResponse.java b/src/main/java/dev/openfga/sdk/api/client/model/ClientBatchCheckSingleResponse.java new file mode 100644 index 0000000..bb24a9d --- /dev/null +++ b/src/main/java/dev/openfga/sdk/api/client/model/ClientBatchCheckSingleResponse.java @@ -0,0 +1,46 @@ +/* + * OpenFGA + * A high performance and flexible authorization/permission engine built for developers and inspired by Google Zanzibar. + * + * The version of the OpenAPI document: 1.x + * Contact: community@openfga.dev + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + +package dev.openfga.sdk.api.client.model; + +import dev.openfga.sdk.api.model.CheckError; + +public class ClientBatchCheckSingleResponse { + private final boolean allowed; + private final ClientBatchCheckItem request; + private final String correlationId; + private final CheckError error; + + public ClientBatchCheckSingleResponse( + boolean allowed, ClientBatchCheckItem request, String correlationId, CheckError error) { + this.allowed = allowed; + this.request = request; + this.correlationId = correlationId; + this.error = error; + } + + public boolean isAllowed() { + return allowed; + } + + public ClientBatchCheckItem getRequest() { + return request; + } + + public String getCorrelationId() { + return correlationId; + } + + public CheckError getError() { + return error; + } +} diff --git a/src/main/java/dev/openfga/sdk/api/client/model/ClientListRelationsResponse.java b/src/main/java/dev/openfga/sdk/api/client/model/ClientListRelationsResponse.java index cb3fb07..0718c75 100644 --- a/src/main/java/dev/openfga/sdk/api/client/model/ClientListRelationsResponse.java +++ b/src/main/java/dev/openfga/sdk/api/client/model/ClientListRelationsResponse.java @@ -26,7 +26,7 @@ public List getRelations() { return relations; } - public static ClientListRelationsResponse fromBatchCheckResponses(List responses) + public static ClientListRelationsResponse fromBatchCheckResponses(List responses) throws Throwable { // If any response ultimately failed (with retries) we throw the first exception encountered. var failedResponse = responses.stream() @@ -37,8 +37,8 @@ public static ClientListRelationsResponse fromBatchCheckResponses(List additionalHeaders; + private Integer maxParallelRequests; + private String authorizationModelId; + private ConsistencyPreference consistency; + + public ClientBatchCheckClientOptions additionalHeaders(Map additionalHeaders) { + this.additionalHeaders = additionalHeaders; + return this; + } + + @Override + public Map getAdditionalHeaders() { + return this.additionalHeaders; + } + + public ClientBatchCheckClientOptions maxParallelRequests(Integer maxParallelRequests) { + this.maxParallelRequests = maxParallelRequests; + return this; + } + + public Integer getMaxParallelRequests() { + return maxParallelRequests; + } + + public ClientBatchCheckClientOptions authorizationModelId(String authorizationModelId) { + this.authorizationModelId = authorizationModelId; + return this; + } + + public String getAuthorizationModelId() { + return authorizationModelId; + } + + public ClientBatchCheckClientOptions consistency(ConsistencyPreference consistency) { + this.consistency = consistency; + return this; + } + + public ConsistencyPreference getConsistency() { + return consistency; + } + + public ClientCheckOptions asClientCheckOptions() { + return new ClientCheckOptions() + .additionalHeaders(additionalHeaders) + .authorizationModelId(authorizationModelId) + .consistency(consistency); + } +} diff --git a/src/main/java/dev/openfga/sdk/api/configuration/ClientBatchCheckOptions.java b/src/main/java/dev/openfga/sdk/api/configuration/ClientBatchCheckOptions.java index 1a706a4..68ac55b 100644 --- a/src/main/java/dev/openfga/sdk/api/configuration/ClientBatchCheckOptions.java +++ b/src/main/java/dev/openfga/sdk/api/configuration/ClientBatchCheckOptions.java @@ -18,6 +18,7 @@ public class ClientBatchCheckOptions implements AdditionalHeadersSupplier { private Map additionalHeaders; private Integer maxParallelRequests; + private Integer maxBatchSize; private String authorizationModelId; private ConsistencyPreference consistency; @@ -40,6 +41,15 @@ public Integer getMaxParallelRequests() { return maxParallelRequests; } + public ClientBatchCheckOptions maxBatchSize(Integer maxBatchSize) { + this.maxBatchSize = maxBatchSize; + return this; + } + + public Integer getMaxBatchSize() { + return maxBatchSize; + } + public ClientBatchCheckOptions authorizationModelId(String authorizationModelId) { this.authorizationModelId = authorizationModelId; return this; @@ -57,11 +67,4 @@ public ClientBatchCheckOptions consistency(ConsistencyPreference consistency) { public ConsistencyPreference getConsistency() { return consistency; } - - public ClientCheckOptions asClientCheckOptions() { - return new ClientCheckOptions() - .additionalHeaders(additionalHeaders) - .authorizationModelId(authorizationModelId) - .consistency(consistency); - } } diff --git a/src/main/java/dev/openfga/sdk/api/configuration/ClientListRelationsOptions.java b/src/main/java/dev/openfga/sdk/api/configuration/ClientListRelationsOptions.java index 0ae80e5..73afd8c 100644 --- a/src/main/java/dev/openfga/sdk/api/configuration/ClientListRelationsOptions.java +++ b/src/main/java/dev/openfga/sdk/api/configuration/ClientListRelationsOptions.java @@ -58,8 +58,8 @@ public ConsistencyPreference getConsistency() { return consistency; } - public ClientBatchCheckOptions asClientBatchCheckOptions() { - return new ClientBatchCheckOptions() + public ClientBatchCheckClientOptions asClientBatchCheckClientOptions() { + return new ClientBatchCheckClientOptions() .authorizationModelId(authorizationModelId) .maxParallelRequests(maxParallelRequests) .consistency(consistency); diff --git a/src/main/java/dev/openfga/sdk/errors/FgaValidationError.java b/src/main/java/dev/openfga/sdk/errors/FgaValidationError.java new file mode 100644 index 0000000..ab0baa3 --- /dev/null +++ b/src/main/java/dev/openfga/sdk/errors/FgaValidationError.java @@ -0,0 +1,14 @@ +package dev.openfga.sdk.errors; + +public class FgaValidationError extends Exception { + private final String field; + + public FgaValidationError(String field, String message) { + super(message); + this.field = field; + } + + public String getField() { + return field; + } +} diff --git a/src/test/java/dev/openfga/sdk/api/client/OpenFgaClientTest.java b/src/test/java/dev/openfga/sdk/api/client/OpenFgaClientTest.java index 280080f..ca9feca 100644 --- a/src/test/java/dev/openfga/sdk/api/client/OpenFgaClientTest.java +++ b/src/test/java/dev/openfga/sdk/api/client/OpenFgaClientTest.java @@ -12,6 +12,7 @@ package dev.openfga.sdk.api.client; +import static com.github.tomakehurst.wiremock.client.WireMock.matchingJsonPath; import static org.hamcrest.Matchers.*; import static org.hamcrest.core.StringContains.containsString; import static org.junit.jupiter.api.Assertions.*; @@ -19,6 +20,9 @@ import static org.mockito.Mockito.*; import com.fasterxml.jackson.databind.ObjectMapper; +import com.github.tomakehurst.wiremock.client.WireMock; +import com.github.tomakehurst.wiremock.junit5.WireMockRuntimeInfo; +import com.github.tomakehurst.wiremock.junit5.WireMockTest; import com.pgssoft.httpclient.HttpClientMock; import dev.openfga.sdk.api.client.model.*; import dev.openfga.sdk.api.configuration.*; @@ -49,6 +53,7 @@ /** * API tests for OpenFgaClient. */ +@WireMockTest public class OpenFgaClientTest { private static final String DEFAULT_STORE_ID = "01YCP46JKYM8FJCQ37NMBYHE5X"; private static final String DEFAULT_STORE_NAME = "test_store"; @@ -1674,7 +1679,7 @@ public void check_500() throws Exception { * Check whether a user is authorized to access an object. */ @Test - public void batchCheck() throws Exception { + public void clientBatchCheck() throws Exception { // Given String postUrl = String.format("https://api.fga.example/stores/%s/check", DEFAULT_STORE_ID); String expectedBody = String.format( @@ -1683,27 +1688,27 @@ public void batchCheck() throws Exception { mockHttpClient .onPost(postUrl) .withBody(is(expectedBody)) - .withHeader(CLIENT_METHOD_HEADER, "BatchCheck") + .withHeader(CLIENT_METHOD_HEADER, "ClientBatchCheck") .withHeader(CLIENT_BULK_REQUEST_ID_HEADER, anyValidUUID()) .doReturn(200, "{\"allowed\":true}"); ClientCheckRequest request = new ClientCheckRequest() ._object(DEFAULT_OBJECT) .relation(DEFAULT_RELATION) .user(DEFAULT_USER); - ClientBatchCheckOptions options = new ClientBatchCheckOptions() + ClientBatchCheckClientOptions options = new ClientBatchCheckClientOptions() .authorizationModelId(DEFAULT_AUTH_MODEL_ID) .consistency(ConsistencyPreference.MINIMIZE_LATENCY); // When - List response = - fga.batchCheck(List.of(request), options).get(); + List response = + fga.clientBatchCheck(List.of(request), options).get(); // Then mockHttpClient .verify() .post(postUrl) .withBody(is(expectedBody)) - .withHeader(CLIENT_METHOD_HEADER, "BatchCheck") + .withHeader(CLIENT_METHOD_HEADER, "ClientBatchCheck") .withHeader(CLIENT_BULK_REQUEST_ID_HEADER, anyValidUUID()) .called(1); assertEquals(Boolean.TRUE, response.get(0).getAllowed()); @@ -1732,12 +1737,12 @@ public void shouldShutdownExecutorAfterBatchCheck() throws Exception { ._object(DEFAULT_OBJECT) .relation(DEFAULT_RELATION) .user(DEFAULT_USER); - ClientBatchCheckOptions options = new ClientBatchCheckOptions() + ClientBatchCheckClientOptions options = new ClientBatchCheckClientOptions() .authorizationModelId(DEFAULT_AUTH_MODEL_ID) .consistency(ConsistencyPreference.MINIMIZE_LATENCY); // When - fga.batchCheck(List.of(request), options).get(); + fga.clientBatchCheck(List.of(request), options).get(); // Then verify(mockExecutor).shutdown(); @@ -1745,7 +1750,7 @@ public void shouldShutdownExecutorAfterBatchCheck() throws Exception { } @Test - public void batchCheck_twentyTimes() throws Exception { + public void clientBatchCheck_twentyTimes() throws Exception { // Given String postUrl = String.format("https://api.fga.example/stores/%s/check", DEFAULT_STORE_ID); String expectedBody = String.format( @@ -1754,7 +1759,7 @@ public void batchCheck_twentyTimes() throws Exception { mockHttpClient .onPost(postUrl) .withBody(is(expectedBody)) - .withHeader(CLIENT_METHOD_HEADER, "BatchCheck") + .withHeader(CLIENT_METHOD_HEADER, "ClientBatchCheck") .withHeader(CLIENT_BULK_REQUEST_ID_HEADER, anyValidUUID()) .doReturn(200, "{\"allowed\":true}"); List requests = IntStream.range(0, 20) @@ -1763,29 +1768,30 @@ public void batchCheck_twentyTimes() throws Exception { .relation(DEFAULT_RELATION) .user(DEFAULT_USER)) .collect(Collectors.toList()); - ClientBatchCheckOptions options = new ClientBatchCheckOptions().authorizationModelId(DEFAULT_AUTH_MODEL_ID); + ClientBatchCheckClientOptions options = + new ClientBatchCheckClientOptions().authorizationModelId(DEFAULT_AUTH_MODEL_ID); // When - fga.batchCheck(requests, options).get(); + fga.clientBatchCheck(requests, options).get(); // Then mockHttpClient .verify() .post(postUrl) .withBody(is(expectedBody)) - .withHeader(CLIENT_METHOD_HEADER, "BatchCheck") + .withHeader(CLIENT_METHOD_HEADER, "ClientBatchCheck") .withHeader(CLIENT_BULK_REQUEST_ID_HEADER, anyValidUUID()) .called(20); } @Test - public void batchCheck_storeIdRequired() { + public void clientBatchCheck_storeIdRequired() { // Given clientConfiguration.storeId(null); // When - var exception = assertThrows(FgaInvalidParameterException.class, () -> fga.batchCheck( - List.of(new ClientCheckRequest()), new ClientBatchCheckOptions()) + var exception = assertThrows(FgaInvalidParameterException.class, () -> fga.clientBatchCheck( + List.of(new ClientCheckRequest()), new ClientBatchCheckClientOptions()) .get()); // Then @@ -1794,7 +1800,7 @@ public void batchCheck_storeIdRequired() { } @Test - public void batchCheck_400() throws Exception { + public void clientBatchCheck_400() throws Exception { // Given String postUrl = String.format("https://api.fga.example/stores/%s/check", DEFAULT_STORE_ID); mockHttpClient @@ -1802,8 +1808,8 @@ public void batchCheck_400() throws Exception { .doReturn(400, "{\"code\":\"validation_error\",\"message\":\"Generic validation error\"}"); // When - List response = fga.batchCheck( - List.of(new ClientCheckRequest()), new ClientBatchCheckOptions()) + List response = fga.clientBatchCheck( + List.of(new ClientCheckRequest()), new ClientBatchCheckClientOptions()) .join(); // Then @@ -1820,7 +1826,7 @@ public void batchCheck_400() throws Exception { } @Test - public void batchCheck_404() throws Exception { + public void clientBatchCheck_404() throws Exception { // Given String postUrl = String.format("https://api.fga.example/stores/%s/check", DEFAULT_STORE_ID); mockHttpClient @@ -1828,8 +1834,8 @@ public void batchCheck_404() throws Exception { .doReturn(404, "{\"code\":\"undefined_endpoint\",\"message\":\"Endpoint not enabled\"}"); // When - List response = fga.batchCheck( - List.of(new ClientCheckRequest()), new ClientBatchCheckOptions()) + List response = fga.clientBatchCheck( + List.of(new ClientCheckRequest()), new ClientBatchCheckClientOptions()) .join(); // Then @@ -1845,7 +1851,7 @@ public void batchCheck_404() throws Exception { } @Test - public void batchCheck_500() throws Exception { + public void clientBatchCheck_500() throws Exception { // Given String postUrl = String.format("https://api.fga.example/stores/%s/check", DEFAULT_STORE_ID); mockHttpClient @@ -1853,8 +1859,8 @@ public void batchCheck_500() throws Exception { .doReturn(500, "{\"code\":\"internal_error\",\"message\":\"Internal Server Error\"}"); // When - List response = fga.batchCheck( - List.of(new ClientCheckRequest()), new ClientBatchCheckOptions()) + List response = fga.clientBatchCheck( + List.of(new ClientCheckRequest()), new ClientBatchCheckClientOptions()) .join(); // Then @@ -1869,6 +1875,171 @@ public void batchCheck_500() throws Exception { "{\"code\":\"internal_error\",\"message\":\"Internal Server Error\"}", exception.getResponseData()); } + @Test + public void shouldThrowExceptionWhenCorrelationIdsAreDuplicated() { + // Given + ClientBatchCheckItem item1 = new ClientBatchCheckItem() + .user("user:81684243-9356-4421-8fbf-a4f8d36aa31b") + .relation("viewer") + ._object("workspace:1") + .correlationId("cor-id"); + ClientBatchCheckItem item2 = new ClientBatchCheckItem() + .user("user:91284243-9356-4421-8fbf-a4f8d36aa31b") + .relation("viewer") + ._object("workspace:2") + .correlationId("cor-id"); + ClientBatchCheckRequest request = new ClientBatchCheckRequest().checks(List.of(item1, item2)); + + // When + FgaValidationError error = assertThrows( + FgaValidationError.class, () -> fga.batchCheck(request).join()); + + // Then + assertEquals("correlationId", error.getField()); + assertEquals("When calling batchCheck, correlation IDs must be unique", error.getMessage()); + } + + @Test + public void shouldReturnEmptyResultsWhenEmptyChecksAreSpecified() throws Exception { + // Given + ClientBatchCheckRequest request = new ClientBatchCheckRequest().checks(List.of()); + + // When + ClientBatchCheckResponse response = fga.batchCheck(request).join(); + + // Then + assertEquals(0, response.getResult().size()); + } + + @Test + public void shouldHandleSingleBatchSuccessfully() throws Exception { + // Given + String postUrl = String.format("https://api.fga.example/stores/%s/batch-check", DEFAULT_STORE_ID); + mockHttpClient + .onPost(postUrl) + .doReturn( + 200, + "{\"result\": {\"cor-1\": {\"allowed\": true, \"error\": null}, \"cor-2\": {\"allowed\": false, \"error\": null}}}"); + + ClientBatchCheckItem item1 = new ClientBatchCheckItem() + .user("user:81684243-9356-4421-8fbf-a4f8d36aa31b") + .relation("can_read") + ._object("document") + .contextualTuples(List.of( + new ClientTupleKey() + .user("user:81684243-9356-4421-8fbf-a4f8d36aa31b") + .relation("editor") + ._object("folder:product"), + new ClientTupleKey() + .user("folder:product") + .relation("parent") + ._object("document:0192ab2a-d83f-756d-9397-c5ed9f3cb69a"))) + .correlationId("cor-1"); + ClientBatchCheckItem item2 = new ClientBatchCheckItem() + .user("folder:product") + .relation("parent") + ._object("document:0192ab2a-d83f-756d-9397-c5ed9f3cb69a") + .correlationId("cor-2"); + ClientBatchCheckRequest request = new ClientBatchCheckRequest().checks(List.of(item1, item2)); + + // When + ClientBatchCheckResponse response = fga.batchCheck(request).join(); + + // Then + mockHttpClient.verify().post(postUrl).called(1); + + assertNotNull(response); + assertEquals(2, response.getResult().size()); + assertTrue(response.getResult().get(0).isAllowed()); + assertFalse(response.getResult().get(1).isAllowed()); + } + + @Test + public void shouldSplitBatchesSuccessfully(WireMockRuntimeInfo wireMockRuntimeInfo) throws Exception { + // Given + String httpBaseUrl = wireMockRuntimeInfo.getHttpBaseUrl(); + var fga = new OpenFgaClient(clientConfiguration.apiUrl(httpBaseUrl), new ApiClient()); + String postUrl = String.format("/stores/%s/batch-check", DEFAULT_STORE_ID); + + WireMock.stubFor( + WireMock.post(postUrl) + .withRequestBody(matchingJsonPath("$.checks[0].correlation_id", WireMock.equalTo("cor-1"))) + .willReturn( + WireMock.aResponse() + .withStatus(200) + .withBody( + "{\"result\": {\"cor-1\": {\"allowed\": true, \"error\": null}, \"cor-2\": {\"allowed\": false, \"error\": null}}}"))); + + WireMock.stubFor( + WireMock.post(postUrl) + .withRequestBody(matchingJsonPath("$.checks[0].correlation_id", WireMock.equalTo("cor-3"))) + .willReturn( + WireMock.aResponse() + .withStatus(200) + .withBody( + "{\"result\": {\"cor-3\": {\"allowed\": false, \"error\": {\"input_error\": \"relation_not_found\", \"message\": \"relation not found\"}}}}}"))); + + ClientBatchCheckItem item1 = new ClientBatchCheckItem() + .user("user:81684243-9356-4421-8fbf-a4f8d36aa31b") + .relation("can_read") + ._object("document") + .contextualTuples(List.of( + new ClientTupleKey() + .user("user:81684243-9356-4421-8fbf-a4f8d36aa31b") + .relation("editor") + ._object("folder:product"), + new ClientTupleKey() + .user("folder:product") + .relation("parent") + ._object("document:0192ab2a-d83f-756d-9397-c5ed9f3cb69a"))) + .correlationId("cor-1"); + ClientBatchCheckItem item2 = new ClientBatchCheckItem() + .user("folder:product") + .relation("parent") + ._object("document:0192ab2a-d83f-756d-9397-c5ed9f3cb69a") + .correlationId("cor-2"); + ClientBatchCheckItem item3 = new ClientBatchCheckItem() + .user("folder:product") + .relation("parent") + ._object("document:9992ab2a-d83f-756d-9397-c5ed9f3cj8a4") + .correlationId("cor-3"); + ClientBatchCheckRequest request = new ClientBatchCheckRequest().checks(List.of(item1, item2, item3)); + + ClientBatchCheckOptions options = new ClientBatchCheckOptions().maxBatchSize(2); + + // When + ClientBatchCheckResponse response = fga.batchCheck(request, options).join(); + + // Then + ClientBatchCheckSingleResponse response1 = response.getResult().stream() + .filter(r -> r.getCorrelationId().equals("cor-1")) + .findFirst() + .orElse(null); + assertNotNull(response1); + assertTrue(response1.isAllowed()); + assertEquals( + "user:81684243-9356-4421-8fbf-a4f8d36aa31b", + response1.getRequest().getUser()); + + ClientBatchCheckSingleResponse response2 = response.getResult().stream() + .filter(r -> r.getCorrelationId().equals("cor-2")) + .findFirst() + .orElse(null); + assertNotNull(response2); + assertFalse(response2.isAllowed()); + assertEquals("folder:product", response2.getRequest().getUser()); + + ClientBatchCheckSingleResponse response3 = response.getResult().stream() + .filter(r -> r.getCorrelationId().equals("cor-3")) + .findFirst() + .orElse(null); + assertNotNull(response3); + assertFalse(response3.isAllowed()); + assertEquals("folder:product", response3.getRequest().getUser()); + assertEquals(ErrorCode.RELATION_NOT_FOUND, response3.getError().getInputError()); + assertEquals("relation not found", response3.getError().getMessage()); + } + /** * Expand all relationships in userset tree format, and following userset rewrite rules. Useful to reason * about and debug a certain relationship. @@ -2130,7 +2301,7 @@ public void listRelations() throws Exception { mockHttpClient .onPost(postUrl) .withBody(is(expectedBody)) - .withHeader(CLIENT_METHOD_HEADER, "BatchCheck") + .withHeader(CLIENT_METHOD_HEADER, "ClientBatchCheck") .withHeader(CLIENT_BULK_REQUEST_ID_HEADER, anyValidUUID()) .doReturn(200, "{\"allowed\":true}"); ClientListRelationsRequest request = new ClientListRelationsRequest() @@ -2150,7 +2321,7 @@ public void listRelations() throws Exception { .verify() .post(postUrl) .withBody(is(expectedBody)) - .withHeader(CLIENT_METHOD_HEADER, "BatchCheck") + .withHeader(CLIENT_METHOD_HEADER, "ClientBatchCheck") .withHeader(CLIENT_BULK_REQUEST_ID_HEADER, anyValidUUID()) .called(1); assertNotNull(response); @@ -2169,7 +2340,7 @@ public void listRelations_deny() throws Exception { mockHttpClient .onPost(postUrl) .withBody(is(expectedBody)) - .withHeader(CLIENT_METHOD_HEADER, "BatchCheck") + .withHeader(CLIENT_METHOD_HEADER, "ClientBatchCheck") .withHeader(CLIENT_BULK_REQUEST_ID_HEADER, anyValidUUID()) .doReturn(200, "{\"allowed\":false}"); ClientListRelationsRequest request = new ClientListRelationsRequest() @@ -2188,7 +2359,7 @@ public void listRelations_deny() throws Exception { .verify() .post(postUrl) .withBody(is(expectedBody)) - .withHeader(CLIENT_METHOD_HEADER, "BatchCheck") + .withHeader(CLIENT_METHOD_HEADER, "ClientBatchCheck") .withHeader(CLIENT_BULK_REQUEST_ID_HEADER, anyValidUUID()) .called(1); assertNotNull(response); @@ -2356,7 +2527,7 @@ public void listRelations_contextAndContextualTuples() throws Exception { mockHttpClient .onPost(postUrl) .withBody(is(expectedBody)) - .withHeader(CLIENT_METHOD_HEADER, "BatchCheck") + .withHeader(CLIENT_METHOD_HEADER, "ClientBatchCheck") .withHeader(CLIENT_BULK_REQUEST_ID_HEADER, anyValidUUID()) .doReturn(200, "{\"allowed\":false}"); ClientListRelationsRequest request = new ClientListRelationsRequest() @@ -2380,7 +2551,7 @@ public void listRelations_contextAndContextualTuples() throws Exception { .verify() .post(postUrl) .withBody(is(expectedBody)) - .withHeader(CLIENT_METHOD_HEADER, "BatchCheck") + .withHeader(CLIENT_METHOD_HEADER, "ClientBatchCheck") .withHeader(CLIENT_BULK_REQUEST_ID_HEADER, anyValidUUID()) .called(1); assertNotNull(response);