Skip to content

Commit

Permalink
test(client): add tests on listRelations, flesh out logic
Browse files Browse the repository at this point in the history
  • Loading branch information
booniepepper committed Nov 14, 2023
1 parent 4b404f2 commit 30e1124
Show file tree
Hide file tree
Showing 5 changed files with 288 additions and 11 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,10 @@ public String getRawResponse() {
return rawResponse;
}

public String getRelation() {
return request == null ? null : request.getRelation();
}

public static BiFunction<ClientCheckResponse, Throwable, ClientBatchCheckResponse> asyncHandler(
ClientCheckRequest request) {
return (response, throwable) -> new ClientBatchCheckResponse(request, response, throwable);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,20 @@ public List<String> getRelations() {
return relations;
}

public static ClientListRelationsResponse fromBatchCheckResponses(List<ClientBatchCheckResponse> responses) {
return new ClientListRelationsResponse(responses.stream()
public static ClientListRelationsResponse fromBatchCheckResponses(List<ClientBatchCheckResponse> responses)
throws Throwable {
// If any response ultimately failed (with retries) we throw the first exception encountered.
var failedResponse = responses.stream()
.filter(response -> response.getThrowable() != null)
.findFirst();
if (failedResponse.isPresent()) {
throw failedResponse.get().getThrowable();
}

var relations = responses.stream()
.filter(ClientBatchCheckResponse::getAllowed)
.map(batchCheckResponse -> batchCheckResponse.getRequest().getRelation())
.collect(Collectors.toList()));
.map(ClientBatchCheckResponse::getRelation)
.collect(Collectors.toList());
return new ClientListRelationsResponse(relations);
}
}
37 changes: 30 additions & 7 deletions src/main/java/dev/openfga/sdk/api/client/OpenFgaClient.java
Original file line number Diff line number Diff line change
Expand Up @@ -495,8 +495,9 @@ public CompletableFuture<ClientListObjectsResponse> listObjects(
* ListRelations - List allowed relations a user has with an object (evaluates)
*/
public CompletableFuture<ClientListRelationsResponse> listRelations(
ClientListRelationsRequest request, ClientBatchCheckOptions options) throws FgaInvalidParameterException {
if (request.getRelations().isEmpty()) {
ClientListRelationsRequest request, ClientListRelationsOptions options)
throws FgaInvalidParameterException {
if (request.getRelations() == null || request.getRelations().isEmpty()) {
throw new FgaInvalidParameterException(
"At least 1 relation to check has to be provided when calling ListRelations");
}
Expand All @@ -508,7 +509,8 @@ public CompletableFuture<ClientListRelationsResponse> listRelations(
._object(request.getObject()))
.collect(Collectors.toList());

return batchCheck(batchCheckRequests, options).thenApply(ClientListRelationsResponse::fromBatchCheckResponses);
return batchCheck(batchCheckRequests, options.asClientBatchCheckOptions())
.thenCompose(responses -> call(() -> ClientListRelationsResponse.fromBatchCheckResponses(responses)));
}

/* ************
Expand Down Expand Up @@ -586,17 +588,38 @@ public CompletableFuture<ClientWriteAssertionsResponse> writeAssertions(
* @param <R> The type of API response
*/
@FunctionalInterface
private interface CheckedAsyncInvocation<R> {
CompletableFuture<R> call() throws Throwable;
}

private <T> CompletableFuture<T> call(CheckedAsyncInvocation<T> action) {
try {
return action.call();
} catch (CompletionException completionException) {
return CompletableFuture.failedFuture(completionException.getCause());
} catch (Throwable throwable) {
return CompletableFuture.failedFuture(throwable);
}
}

/**
* A {@link FunctionalInterface} for calling any function that could throw an exception.
* It wraps exceptions encountered with {@link CompletableFuture#failedFuture(Throwable)}
*
* @param <R> The return type
*/
@FunctionalInterface
private interface CheckedInvocation<R> {
CompletableFuture<R> call() throws FgaInvalidParameterException, ApiException;
R call() throws Throwable;
}

private <T> CompletableFuture<T> call(CheckedInvocation<T> action) {
try {
return action.call();
return CompletableFuture.completedFuture(action.call());
} catch (CompletionException completionException) {
return CompletableFuture.failedFuture(completionException.getCause());
} catch (Exception exception) {
return CompletableFuture.failedFuture(exception);
} catch (Throwable throwable) {
return CompletableFuture.failedFuture(throwable);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
/*
* OpenFGA
* A high performance and flexible authorization/permission engine built for developers and inspired by Google Zanzibar.
*
* The version of the OpenAPI document: 0.1
* Contact: [email protected]
*
* 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.configuration;

public class ClientListRelationsOptions {
private Integer maxParallelRequests;
private String authorizationModelId;

public ClientListRelationsOptions maxParallelRequests(Integer maxParallelRequests) {
this.maxParallelRequests = maxParallelRequests;
return this;
}

public Integer getMaxParallelRequests() {
return maxParallelRequests;
}

public ClientListRelationsOptions authorizationModelId(String authorizationModelId) {
this.authorizationModelId = authorizationModelId;
return this;
}

public String getAuthorizationModelId() {
return authorizationModelId;
}

public ClientBatchCheckOptions asClientBatchCheckOptions() {
return new ClientBatchCheckOptions()
.authorizationModelId(authorizationModelId)
.maxParallelRequests(maxParallelRequests);
}
}
198 changes: 198 additions & 0 deletions src/test/java/dev/openfga/sdk/api/client/OpenFgaClientTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -1671,6 +1671,204 @@ public void listObjects_500() throws Exception {
assertEquals(
"{\"code\":\"internal_error\",\"message\":\"Internal Server Error\"}", exception.getResponseData());
}
/**
* Check whether a user is authorized to access an object.
*/
@Test
public void listRelations() throws Exception {
// Given
String postUrl = String.format("https://localhost/stores/%s/check", DEFAULT_STORE_ID);
String expectedBody = String.format(
"{\"tuple_key\":{\"object\":\"%s\",\"relation\":\"%s\",\"user\":\"%s\"},\"contextual_tuples\":null,\"authorization_model_id\":\"01G5JAVJ41T49E9TT3SKVS7X1J\",\"trace\":null}",
DEFAULT_OBJECT, DEFAULT_RELATION, DEFAULT_USER);
mockHttpClient.onPost(postUrl).withBody(is(expectedBody)).doReturn(200, "{\"allowed\":true}");
ClientListRelationsRequest request = new ClientListRelationsRequest()
.relations(List.of(DEFAULT_RELATION))
.user(DEFAULT_USER)
._object(DEFAULT_OBJECT);
ClientListRelationsOptions options =
new ClientListRelationsOptions().authorizationModelId(DEFAULT_AUTH_MODEL_ID);

// When
ClientListRelationsResponse response =
fga.listRelations(request, options).get();

// Then
mockHttpClient.verify().post(postUrl).withBody(is(expectedBody)).called(1);
assertNotNull(response);
assertNotNull(response.getRelations());
assertEquals(1, response.getRelations().size());
assertEquals(DEFAULT_RELATION, response.getRelations().get(0));
}

@Test
public void listRelations_deny() throws Exception {
// Given
String postUrl = String.format("https://localhost/stores/%s/check", DEFAULT_STORE_ID);
String expectedBody = String.format(
"{\"tuple_key\":{\"object\":\"%s\",\"relation\":\"%s\",\"user\":\"%s\"},\"contextual_tuples\":null,\"authorization_model_id\":\"01G5JAVJ41T49E9TT3SKVS7X1J\",\"trace\":null}",
DEFAULT_OBJECT, "owner", DEFAULT_USER);
mockHttpClient.onPost(postUrl).withBody(is(expectedBody)).doReturn(200, "{\"allowed\":false}");
ClientListRelationsRequest request = new ClientListRelationsRequest()
.relations(List.of("owner"))
._object(DEFAULT_OBJECT)
.user(DEFAULT_USER);
ClientListRelationsOptions options =
new ClientListRelationsOptions().authorizationModelId(DEFAULT_AUTH_MODEL_ID);

// When
ClientListRelationsResponse response =
fga.listRelations(request, options).get();

// Then
mockHttpClient.verify().post(postUrl).withBody(is(expectedBody)).called(1);
assertNotNull(response);
assertNotNull(response.getRelations());
assertTrue(response.getRelations().isEmpty());
}

@Test
public void listRelations_storeIdRequired() {
// Given
clientConfiguration.storeId(null);
ClientListRelationsRequest request = new ClientListRelationsRequest()
.user(DEFAULT_USER)
.relations(List.of(DEFAULT_RELATION))
._object(DEFAULT_OBJECT);

// When
var exception = assertThrows(
FgaInvalidParameterException.class, () -> fga.listRelations(request, new ClientListRelationsOptions())
.get());

// Then
assertEquals(
"Required parameter storeId was invalid when calling ClientConfiguration.", exception.getMessage());
}

@Test
public void listRelations_nonNullRelationsRequired() {
// Given
ClientListRelationsRequest request = new ClientListRelationsRequest()
.user(DEFAULT_USER)
.relations(null) // Should fail
._object(DEFAULT_OBJECT);

// When
var exception = assertThrows(
FgaInvalidParameterException.class, () -> fga.listRelations(request, new ClientListRelationsOptions())
.get());

// Then
assertEquals(
"At least 1 relation to check has to be provided when calling ListRelations", exception.getMessage());
}

@Test
public void listRelations_atLeastOneRelationRequired() {
// Given
ClientListRelationsRequest request = new ClientListRelationsRequest()
.user(DEFAULT_USER)
.relations(List.of()) // Should fail
._object(DEFAULT_OBJECT);

// When
var exception = assertThrows(
FgaInvalidParameterException.class, () -> fga.listRelations(request, new ClientListRelationsOptions())
.get());

// Then
assertEquals(
"At least 1 relation to check has to be provided when calling ListRelations", exception.getMessage());
}

@Test
public void listRelations_400() throws Exception {
// Given
String postUrl = String.format("https://localhost/stores/%s/check", DEFAULT_STORE_ID);
String expectedBody = String.format(
"{\"tuple_key\":{\"object\":\"%s\",\"relation\":\"%s\",\"user\":\"%s\"},\"contextual_tuples\":null,\"authorization_model_id\":\"01G5JAVJ41T49E9TT3SKVS7X1J\",\"trace\":null}",
DEFAULT_OBJECT, DEFAULT_RELATION, DEFAULT_USER);
mockHttpClient
.onPost(postUrl)
.withBody(is(expectedBody))
.doReturn(400, "{\"code\":\"validation_error\",\"message\":\"Generic validation error\"}");
ClientListRelationsRequest request = new ClientListRelationsRequest()
.user(DEFAULT_USER)
.relations(List.of(DEFAULT_RELATION))
._object(DEFAULT_OBJECT);

// When
var execException = assertThrows(
ExecutionException.class, () -> fga.listRelations(request, new ClientListRelationsOptions())
.get());

// Then
mockHttpClient.verify().post(postUrl).withBody(is(expectedBody)).called(1);
var exception = assertInstanceOf(FgaApiValidationError.class, execException.getCause());
assertEquals(400, exception.getStatusCode());
assertEquals(
"{\"code\":\"validation_error\",\"message\":\"Generic validation error\"}",
exception.getResponseData());
}

@Test
public void listRelations_404() throws Exception {
// Given
String postUrl = String.format("https://localhost/stores/%s/check", DEFAULT_STORE_ID);
String expectedBody = String.format(
"{\"tuple_key\":{\"object\":\"%s\",\"relation\":\"%s\",\"user\":\"%s\"},\"contextual_tuples\":null,\"authorization_model_id\":\"01G5JAVJ41T49E9TT3SKVS7X1J\",\"trace\":null}",
DEFAULT_OBJECT, DEFAULT_RELATION, DEFAULT_USER);
mockHttpClient
.onPost(postUrl)
.withBody(is(expectedBody))
.doReturn(404, "{\"code\":\"undefined_endpoint\",\"message\":\"Endpoint not enabled\"}");
ClientListRelationsRequest request = new ClientListRelationsRequest()
.user(DEFAULT_USER)
.relations(List.of(DEFAULT_RELATION))
._object(DEFAULT_OBJECT);

// When
var execException = assertThrows(
ExecutionException.class, () -> fga.listRelations(request, new ClientListRelationsOptions())
.get());

// Then
mockHttpClient.verify().post(postUrl).withBody(is(expectedBody)).called(1);
var exception = assertInstanceOf(FgaApiNotFoundError.class, execException.getCause());
assertEquals(404, exception.getStatusCode());
assertEquals(
"{\"code\":\"undefined_endpoint\",\"message\":\"Endpoint not enabled\"}", exception.getResponseData());
}

@Test
public void listRelations_500() throws Exception {
// Given
String postUrl = String.format("https://localhost/stores/%s/check", DEFAULT_STORE_ID);
String expectedBody = String.format(
"{\"tuple_key\":{\"object\":\"%s\",\"relation\":\"%s\",\"user\":\"%s\"},\"contextual_tuples\":null,\"authorization_model_id\":\"01G5JAVJ41T49E9TT3SKVS7X1J\",\"trace\":null}",
DEFAULT_OBJECT, DEFAULT_RELATION, DEFAULT_USER);
mockHttpClient
.onPost(postUrl)
.withBody(is(expectedBody))
.doReturn(500, "{\"code\":\"internal_error\",\"message\":\"Internal Server Error\"}");
ClientListRelationsRequest request = new ClientListRelationsRequest()
.user(DEFAULT_USER)
.relations(List.of(DEFAULT_RELATION))
._object(DEFAULT_OBJECT);

// When
var execException = assertThrows(
ExecutionException.class, () -> fga.listRelations(request, new ClientListRelationsOptions())
.get());

// Then
mockHttpClient.verify().post(postUrl).withBody(is(expectedBody)).called(1 + DEFAULT_MAX_RETRIES);
var exception = assertInstanceOf(FgaApiInternalError.class, execException.getCause());
assertEquals(500, exception.getStatusCode());
assertEquals(
"{\"code\":\"internal_error\",\"message\":\"Internal Server Error\"}", exception.getResponseData());
}

/**
* Read assertions for an authorization model ID.
Expand Down

0 comments on commit 30e1124

Please sign in to comment.