diff --git a/.openapi-generator/FILES b/.openapi-generator/FILES index 4a6526e..0308880 100644 --- a/.openapi-generator/FILES +++ b/.openapi-generator/FILES @@ -82,13 +82,32 @@ src/main/java/dev/openfga/sdk/api/auth/CredentialsFlowResponse.java src/main/java/dev/openfga/sdk/api/auth/OAuth2Client.java src/main/java/dev/openfga/sdk/api/client/ApiClient.java 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/ClientCheckRequest.java +src/main/java/dev/openfga/sdk/api/client/ClientExpandRequest.java +src/main/java/dev/openfga/sdk/api/client/ClientListObjectsRequest.java +src/main/java/dev/openfga/sdk/api/client/ClientListRelationsRequest.java +src/main/java/dev/openfga/sdk/api/client/ClientReadRequest.java +src/main/java/dev/openfga/sdk/api/client/ClientTupleKey.java +src/main/java/dev/openfga/sdk/api/client/ClientWriteRequest.java +src/main/java/dev/openfga/sdk/api/client/OpenFgaClient.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/ClientCheckOptions.java +src/main/java/dev/openfga/sdk/api/configuration/ClientConfiguration.java src/main/java/dev/openfga/sdk/api/configuration/ClientCredentials.java +src/main/java/dev/openfga/sdk/api/configuration/ClientExpandOptions.java +src/main/java/dev/openfga/sdk/api/configuration/ClientListObjectsOptions.java +src/main/java/dev/openfga/sdk/api/configuration/ClientReadOptions.java +src/main/java/dev/openfga/sdk/api/configuration/ClientWriteOptions.java src/main/java/dev/openfga/sdk/api/configuration/Configuration.java src/main/java/dev/openfga/sdk/api/configuration/ConfigurationOverride.java src/main/java/dev/openfga/sdk/api/configuration/Credentials.java src/main/java/dev/openfga/sdk/api/configuration/CredentialsMethod.java +src/main/java/dev/openfga/sdk/api/configuration/ListStoresOptions.java +src/main/java/dev/openfga/sdk/api/configuration/ReadAuthorizationModelOptions.java +src/main/java/dev/openfga/sdk/api/configuration/ReadAuthorizationModelsOptions.java +src/main/java/dev/openfga/sdk/api/configuration/ReadChangesOptions.java src/main/java/dev/openfga/sdk/api/model/AbstractOpenApiSchema.java src/main/java/dev/openfga/sdk/api/model/Any.java src/main/java/dev/openfga/sdk/api/model/Assertion.java @@ -149,9 +168,12 @@ src/main/java/dev/openfga/sdk/errors/FgaInvalidParameterException.java src/main/java/dev/openfga/sdk/util/Pair.java src/main/java/dev/openfga/sdk/util/StringUtil.java src/test-integration/java/dev/openfga/sdk/api/OpenFgaApiIntegrationTest.java +src/test-integration/java/dev/openfga/sdk/api/client/OpenFgaClientIntegrationTest.java +src/test-integration/java/package-info.java src/test/java/dev/openfga/sdk/api/OpenFgaApiTest.java src/test/java/dev/openfga/sdk/api/OpenFgaApiTest.java src/test/java/dev/openfga/sdk/api/auth/OAuth2ClientTest.java +src/test/java/dev/openfga/sdk/api/client/OpenFgaClientTest.java src/test/java/dev/openfga/sdk/api/configuration/ClientCredentialsTest.java src/test/java/dev/openfga/sdk/api/configuration/ConfigurationTest.java src/test/java/dev/openfga/sdk/api/model/AnyTest.java diff --git a/CHANGELOG.md b/CHANGELOG.md index 4a06299..f38be19 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ # Changelog +## v0.0.5 + +### [0.0.5](https://github.com/openfga/java-sdk/compare/v0.0.4...v0.0.5) (2023-09-27) + +- feat(client): add `OpenFgaClient` wrapping `OpenFgaApi` and exposing a simplified interface. + See [docs](https://github.com/openfga/java-sdk?tab=readme-ov-file#initializing-the-api-client) +- chore(docs): update the README with installation and usage instructions. + ## v0.0.3, v0.0.4 ### [0.0.4](https://github.com/openfga/java-sdk/compare/v0.0.2...v0.0.4) (2023-09-21) diff --git a/README.md b/README.md index f2213ba..0e32cc1 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,7 @@ # Java SDK for OpenFGA +[![Maven Central](https://img.shields.io/maven-central/v/dev.openfga/openfga-sdk.svg?label=Maven%20Central)](https://search.maven.org/search?q=g:%22dev.openfga%22%20AND%20a:%22openfga-sdk%22) +[![Javadoc](https://javadoc.io/badge2/dev.openfga/openfga-sdk/javadoc.svg)](https://javadoc.io/doc/dev.openfga/openfga-sdk) [![Release](https://img.shields.io/github/v/release/openfga/java-sdk?sort=semver&color=green)](https://github.com/openfga/java-sdk/releases) [![License](https://img.shields.io/badge/License-Apache_2.0-blue.svg)](./LICENSE) [![FOSSA Status](https://app.fossa.com/api/projects/git%2Bgithub.com%2Fopenfga%2Fjava-sdk.svg?type=shield)](https://app.fossa.com/projects/git%2Bgithub.com%2Fopenfga%2Fjava-sdk?ref=badge_shield) @@ -65,6 +67,49 @@ OpenFGA is designed to make it easy for application builders to model their perm ## Installation +The OpenFGA Java SDK is available on [Maven Central](https://central.sonatype.com/). + +It can be used with the following: + +* Gradle (Groovy) + +```groovy +implementation 'dev.openfga:openfga-sdk:0.0.5' +``` + +* Gradle (Kotlin) + +```kotlin +implementation("dev.openfga:openfga-sdk:0.0.5") +``` + +* Apache Maven + +```xml + + dev.openfga + openfga-sdk + 0.0.5 + +``` + +* Ivy + +```xml + +``` + +* SBT + +```scala +libraryDependencies += "dev.openfga" % "openfga-sdk" % "0.0.5" +``` + +* Leiningen + +```edn +[dev.openfga/openfga-sdk "0.0.5"] +``` ## Getting Started @@ -75,17 +120,85 @@ OpenFGA is designed to make it easy for application builders to model their perm #### No Credentials -```cjava +```java +import com.fasterxml.jackson.databind.ObjectMapper; +import dev.openfga.sdk.api.client.ApiClient; +import dev.openfga.sdk.api.client.OpenFgaClient; +import dev.openfga.sdk.api.configuration.ClientConfiguration; +import java.net.http.HttpClient; + +public class Example { + public static void main(String[] args) throws Exception { + var clientConfig = new ClientConfiguration() + .apiUrl(System.getenv("OPENFGA_API_URL")) // If not specified, will default to "https://localhost:8080" + .storeId(System.getenv("OPENFGA_STORE_ID")) // Not required when calling createStore() or listStores() + .authorizationModelId(System.getenv("OPENFGA_AUTHORIZATION_MODEL_ID")); // Optional, can be overridden per request + var apiClient = new ApiClient(HttpClient.newBuilder(), new ObjectMapper()); + + var fgaClient = new OpenFgaClient(apiClient, clientConfig); + var response = fgaClient.readAuthorizationModels().get(); + } +} ``` #### API Token ```java +import com.fasterxml.jackson.databind.ObjectMapper; +import dev.openfga.sdk.api.client.ApiClient; +import dev.openfga.sdk.api.client.OpenFgaClient; +import dev.openfga.sdk.api.configuration.ApiToken; +import dev.openfga.sdk.api.configuration.ClientConfiguration; +import dev.openfga.sdk.api.configuration.Credentials; +import java.net.http.HttpClient; + +public class Example { + public static void main(String[] args) throws Exception { + var clientConfig = new ClientConfiguration() + .apiUrl(System.getenv("OPENFGA_API_URL")) // If not specified, will default to "https://localhost:8080" + .storeId(System.getenv("OPENFGA_STORE_ID")) // Not required when calling createStore() or listStores() + .authorizationModelId(System.getenv("OPENFGA_AUTHORIZATION_MODEL_ID")) // Optional, can be overridden per request + .credentials(new Credentials( + new ApiToken(System.getenv("OPENFGA_API_TOKEN")) // will be passed as the "Authorization: Bearer ${ApiToken}" request header + )); + var apiClient = new ApiClient(HttpClient.newBuilder(), new ObjectMapper()); + + var fgaClient = new OpenFgaClient(apiClient, clientConfig); + var response = fgaClient.readAuthorizationModels().get(); + } +} ``` #### Client Credentials ```java +import com.fasterxml.jackson.databind.ObjectMapper; +import dev.openfga.sdk.api.client.ApiClient; +import dev.openfga.sdk.api.client.OpenFgaClient; +import dev.openfga.sdk.api.configuration.ClientConfiguration; +import dev.openfga.sdk.api.configuration.ClientCredentials; +import dev.openfga.sdk.api.configuration.Credentials; +import java.net.http.HttpClient; + +public class Example { + public static void main(String[] args) throws Exception { + var clientConfig = new ClientConfiguration() + .apiUrl(System.getenv("OPENFGA_API_URL")) // If not specified, will default to "https://localhost:8080" + .storeId(System.getenv("OPENFGA_STORE_ID")) // Not required when calling createStore() or listStores() + .authorizationModelId(System.getenv("OPENFGA_AUTHORIZATION_MODEL_ID")) // Optional, can be overridden per request + .credentials(new Credentials( + new ClientCredentials() + .apiTokenIssuer(System.getenv("OPENFGA_API_TOKEN_ISSUER")) + .apiAudience(System.getenv("OPENFGA_API_AUDIENCE")) + .clientId(System.getenv("OPENFGA_CLIENT_ID")) + .clientSecret(System.getenv("OPENFGA_CLIENT_SECRET")) + )); + var apiClient = new ApiClient(HttpClient.newBuilder(), new ObjectMapper()); + + var fgaClient = new OpenFgaClient(apiClient, clientConfig); + var response = fgaClient.readAuthorizationModels().get(); + } +} ``` @@ -106,6 +219,12 @@ Get a paginated list of stores. [API Documentation](https://openfga.dev/api/service/docs/api#/Stores/ListStores) ```java +var options = new ListStoresOptions() + .pageSize(10) + .continuationToken("..."); +var stores = fgaClient.listStores(options); + +// stores = [{ "id": "01FQH7V8BEG3GPQW93KTRFR8JB", "name": "FGA Demo Store", "created_at": "2022-01-01T00:00:00.000Z", "updated_at": "2022-01-01T00:00:00.000Z" }] ``` ##### Create Store @@ -115,6 +234,17 @@ Initialize a store. [API Documentation](https://openfga.dev/api/service/docs/api#/Stores/CreateStore) ```java +var request = new CreateStoreRequest().name("FGA Demo"); +var store = fgaClient.createStore(request).get(); + +// store.getId() = "01FQH7V8BEG3GPQW93KTRFR8JB" + +// store the store.getId() in database + +// update the storeId of the client instance +fgaClient.setStoreId(store.getId()); + +// continue calling the API normally ``` ##### Get Store @@ -126,6 +256,9 @@ Get information about the current store. > Requires a client initialized with a storeId ```java +var store = fgaClient.getStore().get(); + +// store = { "id": "01FQH7V8BEG3GPQW93KTRFR8JB", "name": "FGA Demo Store", "created_at": "2022-01-01T00:00:00.000Z", "updated_at": "2022-01-01T00:00:00.000Z" } ``` ##### Delete Store @@ -137,6 +270,7 @@ Delete a store. > Requires a client initialized with a storeId ```java +var store = fgaClient.deleteStore().get(); ``` #### Authorization Models @@ -148,6 +282,14 @@ Read all authorization models in the store. [API Documentation](https://openfga.dev/api/service#/Authorization%20Models/ReadAuthorizationModels) ```java +var options = new ReadAuthorizationModelsOptions() + .pageSize(10) + .continuationToken("..."); +var response = fgaClient.readAuthorizationModels(options).get(); + +// response.getAuthorizationModels() = [ +// { id: "01GXSA8YR785C4FYS3C0RTG7B1", schemaVersion: "1.1", typeDefinitions: [...] }, +// { id: "01GXSBM5PVYHCJNRNKXMB4QZTW", schemaVersion: "1.1", typeDefinitions: [...] }]; ``` ##### Write Authorization Model @@ -163,6 +305,37 @@ Create a new authorization model. > You can use the OpenFGA [CLI](https://github.com/openfga/cli) or [Syntax Transformer](https://github.com/openfga/syntax-transformer) to convert between the OpenFGA DSL and the JSON authorization model. ```java + +var request = new WriteAuthorizationModelRequest() + .schemaVersion("1.1") + .typeDefinitions(List.of( + new TypeDefinition().type("user").relations(Map.of()), + new TypeDefinition() + .type("document") + .relations(Map.of( + "writer", new Userset(), + "viewer", new Userset().union(new Usersets() + .child(List.of( + new Userset(), + new Userset().computedUserset(new ObjectRelation().relation("writer")) + )) + ) + )) + .metadata(new Metadata() + .relations(Map.of( + "writer", new RelationMetadata().directlyRelatedUserTypes( + List.of(new RelationReference().type("user")) + ), + "viewer", new RelationMetadata().directlyRelatedUserTypes( + List.of(new RelationReference().type("user")) + ) + )) + ) + )); + +var response = fgaClient.writeAuthorizationModel(request).get(); + +// response.getAuthorizationModelId() = "01GXSA8YR785C4FYS3C0RTG7B1" ``` #### Read a Single Authorization Model @@ -172,6 +345,15 @@ Read a particular authorization model. [API Documentation](https://openfga.dev/api/service#/Authorization%20Models/ReadAuthorizationModel) ```java +var options = new ReadAuthorizationModelOptions() + // You can rely on the model id set in the configuration or override it for this specific request + .authorizationModelId("01GXSA8YR785C4FYS3C0RTG7B1"); + +var response = fgaClient.readAuthorizationModel(options).get(); + +// response.getAuthorizationModel().getId() = "01GXSA8YR785C4FYS3C0RTG7B1" +// response.getAuthorizationModel().getSchemaVersion() = "1.1" +// response.getAuthorizationModel().getTypeDefinitions() = [{ "type": "document", "relations": { ... } }, { "type": "user", "relations": { ... }}] ``` ##### Read the Latest Authorization Model @@ -181,6 +363,11 @@ Reads the latest authorization model (note: this ignores the model id in configu [API Documentation](https://openfga.dev/api/service#/Authorization%20Models/ReadAuthorizationModel) ```java +var response = fgaClient.readLatestAuthorizationModel().get(); + +// response.getAuthorizationModel().getId() = "01GXSA8YR785C4FYS3C0RTG7B1" +// response.getAuthorizationModel().SchemaVersion() = "1.1" +// response.getAuthorizationModel().TypeDefinitions() = [{ "type": "document", "relations": { ... } }, { "type": "user", "relations": { ... }}] ``` #### Relationship Tuples @@ -192,6 +379,18 @@ Reads the list of historical relationship tuple writes and deletes. [API Documentation](https://openfga.dev/api/service#/Relationship%20Tuples/ReadChanges) ```java +var options = new ClientReadChangesOptions() + .type("document") + .pageSize(10) + .continuationToken("..."); + +var response = fgaClient.readChanges(options).get(); + +// response.getContinuationToken() = ... +// response.getChanges() = [ +// { tupleKey: { user, relation, object }, operation: TupleOperation.WRITE, timestamp: ... }, +// { tupleKey: { user, relation, object }, operation: TupleOperation.DELETE, timestamp: ... } +// ] ``` ##### Read Relationship Tuples @@ -201,6 +400,38 @@ Reads the relationship tuples stored in the database. It does not evaluate nor e [API Documentation](https://openfga.dev/api/service#/Relationship%20Tuples/Read) ```java +// Find if a relationship tuple stating that a certain user is a viewer of a certain document +var request = new ClientReadRequest() + .user("user:81684243-9356-4421-8fbf-a4f8d36aa31b") + .relation("viewer") + ._object("document:roadmap"); + +// Find all relationship tuples where a certain user has a relationship as any relation to a certain document +var request = new ClientReadRequest() + .user("user:81684243-9356-4421-8fbf-a4f8d36aa31b") + ._object("document:roadmap"); + +// Find all relationship tuples where a certain user is a viewer of any document +var request = new ClientReadRequest() + .user("user:81684243-9356-4421-8fbf-a4f8d36aa31b") + .relation("viewer") + ._object("document:"); + +// Find all relationship tuples where any user has a relationship as any relation with a particular document +var request = new ClientReadRequest() + ._object("document:roadmap"); + +// Read all stored relationship tuples +var request = new ClientReadRequest(); + +var options = new ClientReadOptions() + .pageSize(10) + .continuationToken("..."); + +var response = fgaClient.read(request, options).get(); + +// In all the above situations, the response will be of the form: +// response = { tuples: [{ key: { user, relation, object }, timestamp }, ...]} ``` ##### Write (Create and Delete) Relationship Tuples @@ -214,6 +445,29 @@ Create and/or delete relationship tuples to update the system state. By default, write runs in a transaction mode where any invalid operation (deleting a non-existing tuple, creating an existing tuple, one of the tuples was invalid) or a server error will fail the entire operation. ```java +var request = new ClientWriteRequest() + .writes(List.of( + new TupleKey() + .user("user:81684243-9356-4421-8fbf-a4f8d36aa31b") + .relation("viewer") + ._object("document:roadmap"), + new TupleKey() + .user("user:81684243-9356-4421-8fbf-a4f8d36aa31b") + .relation("viewer") + ._object("document:budget") + )) + .deletes(List.of( + new TupleKey() + .user("user:81684243-9356-4421-8fbf-a4f8d36aa31b") + .relation("writer") + ._object("document:roadmap") + )); + +// You can rely on the model id set in the configuration or override it for this specific request +var options = new ClientWriteOptions() + .authorizationModelId("01GXSA8YR785C4FYS3C0RTG7B1"); + +var response = fgaClient.write(request, options).get(); ``` Convenience `WriteTuples` and `DeleteTuples` methods are also available. @@ -223,6 +477,7 @@ Convenience `WriteTuples` and `DeleteTuples` methods are also available. The SDK will split the writes into separate requests and send them sequentially to avoid violating rate limits. ```java +// Coming soon ``` #### Relationship Queries @@ -234,6 +489,16 @@ Check if a user has a particular relation with an object. [API Documentation](https://openfga.dev/api/service#/Relationship%20Queries/Check) ```java +var request = new ClientCheckRequest() + .user("user:81684243-9356-4421-8fbf-a4f8d36aa31b") + .relation("writer") + ._object("document:roadmap"); +var options = new ClientCheckOptions() + // You can rely on the model id set in the configuration or override it for this specific request + .authorizationModelId("01GXSA8YR785C4FYS3C0RTG7B1"); + +var response = fgaClient.check(request, options).get(); +// response.getAllowed() = true ``` ##### Batch Check @@ -242,6 +507,7 @@ Run a set of [checks](#check). Batch Check will return `allowed: false` if it en If 429s or 5xxs are encountered, the underlying check will retry up to 15 times before giving up. ```java +// Coming soon ``` ##### Expand @@ -251,6 +517,16 @@ Expands the relationships in userset tree format. [API Documentation](https://openfga.dev/api/service#/Relationship%20Queries/Expand) ```java +var request = new ClientExpandRequest() + .relation("viewer") + ._object("document:roadmap"); +var options = new ClientCheckOptions() + // You can rely on the model id set in the configuration or override it for this specific request + .authorizationModelId("01GXSA8YR785C4FYS3C0RTG7B1"); + +var response = fgaClient.expand(request, options).get(); + +// response.getTree().getRoot() = {"name":"document:roadmap#viewer","leaf":{"users":{"users":["user:81684243-9356-4421-8fbf-a4f8d36aa31b","user:f52a4f7a-054d-47ff-bb6e-3ac81269988f"]}}} ``` ##### List Objects @@ -260,6 +536,23 @@ List the objects of a particular type a user has access to. [API Documentation](https://openfga.dev/api/service#/Relationship%20Queries/ListObjects) ```java +var request = new ClientListObjectsRequest() + .user("user:81684243-9356-4421-8fbf-a4f8d36aa31b") + .relation("viewer") + .type("document") + .contextualTuples(List.of( + new ClientTupleKey() + .user("user:81684243-9356-4421-8fbf-a4f8d36aa31b") + .relation("writer") + ._object("document:budget") + )); +var options = new ClientListObjectsOptions() + // You can rely on the model id set in the configuration or override it for this specific request + .authorizationModelId("01GXSA8YR785C4FYS3C0RTG7B1"); + +var response = fgaClient.listObjects(request, options).get(); + +// response.getObjects() = ["document:roadmap"] ``` ##### List Relations @@ -267,6 +560,7 @@ List the objects of a particular type a user has access to. List the relations a user has on an object. ```java +// Coming soon. ``` #### Assertions @@ -278,6 +572,7 @@ Read assertions for a particular authorization model. [API Documentation](https://openfga.dev/api/service#/Assertions/Read%20Assertions) ```java +var response = fgaClient.readAssertions().get(); ``` ##### Write Assertions @@ -287,11 +582,37 @@ Update the assertions for a particular authorization model. [API Documentation](https://openfga.dev/api/service#/Assertions/Write%20Assertions) ```java +var assertions = List.of( + new ClientAssertion() + .user("user:81684243-9356-4421-8fbf-a4f8d36aa31b") + .relation("viewer") + ._object("document:roadmap") + .expectation(true) +); +fgaClient.writeAssertions(assertions).get(); ``` ### API Endpoints +| Method | HTTP request | Description | +| ------------- | ------------- | ------------- | +| [**check**](docs/OpenFgaApi.md#check) | **POST** /stores/{store_id}/check | Check whether a user is authorized to access an object | +| [**createStore**](docs/OpenFgaApi.md#createstore) | **POST** /stores | Create a store | +| [**deleteStore**](docs/OpenFgaApi.md#deletestore) | **DELETE** /stores/{store_id} | Delete a store | +| [**expand**](docs/OpenFgaApi.md#expand) | **POST** /stores/{store_id}/expand | Expand all relationships in userset tree format, and following userset rewrite rules. Useful to reason about and debug a certain relationship | +| [**getStore**](docs/OpenFgaApi.md#getstore) | **GET** /stores/{store_id} | Get a store | +| [**listObjects**](docs/OpenFgaApi.md#listobjects) | **POST** /stores/{store_id}/list-objects | List all objects of the given type that the user has a relation with | +| [**listStores**](docs/OpenFgaApi.md#liststores) | **GET** /stores | List all stores | +| [**read**](docs/OpenFgaApi.md#read) | **POST** /stores/{store_id}/read | Get tuples from the store that matches a query, without following userset rewrite rules | +| [**readAssertions**](docs/OpenFgaApi.md#readassertions) | **GET** /stores/{store_id}/assertions/{authorization_model_id} | Read assertions for an authorization model ID | +| [**readAuthorizationModel**](docs/OpenFgaApi.md#readauthorizationmodel) | **GET** /stores/{store_id}/authorization-models/{id} | Return a particular version of an authorization model | +| [**readAuthorizationModels**](docs/OpenFgaApi.md#readauthorizationmodels) | **GET** /stores/{store_id}/authorization-models | Return all the authorization models for a particular store | +| [**readChanges**](docs/OpenFgaApi.md#readchanges) | **GET** /stores/{store_id}/changes | Return a list of all the tuple changes | +| [**write**](docs/OpenFgaApi.md#write) | **POST** /stores/{store_id}/write | Add or delete tuples from the store | +| [**writeAssertions**](docs/OpenFgaApi.md#writeassertions) | **PUT** /stores/{store_id}/assertions/{authorization_model_id} | Upsert assertions for an authorization model ID | +| [**writeAuthorizationModel**](docs/OpenFgaApi.md#writeauthorizationmodel) | **POST** /stores/{store_id}/authorization-models | Create a new authorization model | + ### Models diff --git a/build.gradle b/build.gradle index 156bc95..291c259 100644 --- a/build.gradle +++ b/build.gradle @@ -2,6 +2,7 @@ plugins { id 'java' // Quality + id 'jacoco' id 'jvm-test-suite' id 'com.diffplug.spotless' version '6.20.0' @@ -17,7 +18,7 @@ plugins { apply from: 'publish.gradle' group = 'dev.openfga' -version = '0.0.4' +version = '0.0.5' repositories { mavenCentral() @@ -36,6 +37,16 @@ javadoc { options.addStringOption('Xdoclint:none', '-quiet') } +test { + // JaCoCo coverage report is always generated after tests run. + finalizedBy jacocoTestReport +} + +jacocoTestReport { + // tests are required to run before generating a JaCoCo coverage report. + dependsOn test +} + ext { jackson_version = "2.14.1" junit_version = "5.7.1" diff --git a/publish.gradle b/publish.gradle index b070679..927ab8c 100644 --- a/publish.gradle +++ b/publish.gradle @@ -6,7 +6,7 @@ publishing { pom { group = 'dev.openfga' name = 'openfga-sdk' - version = '0.0.4' + version = '0.0.5' description = 'This is an autogenerated Java SDK for OpenFGA. It provides a wrapper around the [OpenFGA API definition](https://openfga.dev/api).' url = 'https://openfga.dev' licenses { diff --git a/src/main/java/dev/openfga/sdk/api/OpenFgaApi.java b/src/main/java/dev/openfga/sdk/api/OpenFgaApi.java index 1b27e63..c4f2e25 100644 --- a/src/main/java/dev/openfga/sdk/api/OpenFgaApi.java +++ b/src/main/java/dev/openfga/sdk/api/OpenFgaApi.java @@ -52,6 +52,11 @@ import java.util.concurrent.CompletableFuture; import java.util.function.Consumer; +/** + * A low-level API representation of an OpenFGA server. + *

+ * Most code should favor the simpler and higher-level {@link OpenFgaClient} when calling an OpenFGA server. + */ public class OpenFgaApi { private final HttpClient memberVarHttpClient; private final ObjectMapper memberVarObjectMapper; @@ -61,8 +66,6 @@ public class OpenFgaApi { private final Consumer> memberVarResponseInterceptor; private final Consumer> memberVarAsyncResponseInterceptor; - // TODO: In every request, get access token, (Assuming plain access token, or OAuth2 CredentialsMethod) - public OpenFgaApi(ApiClient apiClient, Configuration configuration) throws FgaInvalidParameterException { memberVarHttpClient = apiClient.getHttpClient(); memberVarObjectMapper = apiClient.getObjectMapper(); diff --git a/src/main/java/dev/openfga/sdk/api/client/ClientAssertion.java b/src/main/java/dev/openfga/sdk/api/client/ClientAssertion.java new file mode 100644 index 0000000..d1722a9 --- /dev/null +++ b/src/main/java/dev/openfga/sdk/api/client/ClientAssertion.java @@ -0,0 +1,86 @@ +/* + * 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: 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; + +import dev.openfga.sdk.api.model.Assertion; +import dev.openfga.sdk.api.model.TupleKey; +import java.util.List; +import java.util.stream.Collectors; + +public class ClientAssertion { + private String user; + private String relation; + private String _object; + private boolean expectation; + + public ClientAssertion user(String user) { + this.user = user; + return this; + } + + /** + * Get user + * @return user + **/ + public String getUser() { + return user; + } + + public ClientAssertion relation(String relation) { + this.relation = relation; + return this; + } + + /** + * Get relation + * @return relation + **/ + public String getRelation() { + return relation; + } + + public ClientAssertion _object(String _object) { + this._object = _object; + return this; + } + + /** + * Get _object + * @return _object + **/ + public String getObject() { + return _object; + } + + public ClientAssertion expectation(boolean expectation) { + this.expectation = expectation; + return this; + } + + public boolean getExpectation() { + return expectation; + } + + public Assertion asAssertion() { + TupleKey tupleKey = new TupleKey().user(user).relation(relation)._object(_object); + return new Assertion().tupleKey(tupleKey).expectation(expectation); + } + + public static List asAssertions(List assertions) { + if (assertions == null || assertions.isEmpty()) { + return List.of(); + } + + return assertions.stream().map(ClientAssertion::asAssertion).collect(Collectors.toList()); + } +} diff --git a/src/main/java/dev/openfga/sdk/api/client/ClientCheckRequest.java b/src/main/java/dev/openfga/sdk/api/client/ClientCheckRequest.java new file mode 100644 index 0000000..a8d4005 --- /dev/null +++ b/src/main/java/dev/openfga/sdk/api/client/ClientCheckRequest.java @@ -0,0 +1,58 @@ +/* + * 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: 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; + +public class ClientCheckRequest { + private String user; + private String relation; + private String _object; + + public ClientCheckRequest _object(String _object) { + this._object = _object; + return this; + } + + /** + * Get _object + * @return _object + **/ + public String getObject() { + return _object; + } + + public ClientCheckRequest relation(String relation) { + this.relation = relation; + return this; + } + + /** + * Get relation + * @return relation + **/ + public String getRelation() { + return relation; + } + + public ClientCheckRequest user(String user) { + this.user = user; + return this; + } + + /** + * Get user + * @return user + **/ + public String getUser() { + return user; + } +} diff --git a/src/main/java/dev/openfga/sdk/api/client/ClientExpandRequest.java b/src/main/java/dev/openfga/sdk/api/client/ClientExpandRequest.java new file mode 100644 index 0000000..9394753 --- /dev/null +++ b/src/main/java/dev/openfga/sdk/api/client/ClientExpandRequest.java @@ -0,0 +1,58 @@ +/* + * 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: 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; + +public class ClientExpandRequest { + private String user; + private String relation; + private String _object; + + public ClientExpandRequest _object(String _object) { + this._object = _object; + return this; + } + + /** + * Get _object + * @return _object + **/ + public String getObject() { + return _object; + } + + public ClientExpandRequest relation(String relation) { + this.relation = relation; + return this; + } + + /** + * Get relation + * @return relation + **/ + public String getRelation() { + return relation; + } + + public ClientExpandRequest user(String user) { + this.user = user; + return this; + } + + /** + * Get user + * @return user + **/ + public String getUser() { + return user; + } +} diff --git a/src/main/java/dev/openfga/sdk/api/client/ClientListObjectsRequest.java b/src/main/java/dev/openfga/sdk/api/client/ClientListObjectsRequest.java new file mode 100644 index 0000000..9f25b34 --- /dev/null +++ b/src/main/java/dev/openfga/sdk/api/client/ClientListObjectsRequest.java @@ -0,0 +1,66 @@ +/* + * 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: 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; + +import java.util.List; + +public class ClientListObjectsRequest { + private String user; + private String relation; + private String type; + private List contextualTupleKeys; + + public ClientListObjectsRequest user(String user) { + this.user = user; + return this; + } + + /** + * Get user + * @return user + **/ + public String getUser() { + return user; + } + + public ClientListObjectsRequest relation(String relation) { + this.relation = relation; + return this; + } + + /** + * Get relation + * @return relation + **/ + public String getRelation() { + return relation; + } + + public ClientListObjectsRequest type(String type) { + this.type = type; + return this; + } + + public String getType() { + return type; + } + + public ClientListObjectsRequest contextualTupleKeys(List contextualTupleKeys) { + this.contextualTupleKeys = contextualTupleKeys; + return this; + } + + public List getContextualTupleKeys() { + return contextualTupleKeys; + } +} diff --git a/src/main/java/dev/openfga/sdk/api/client/ClientListRelationsRequest.java b/src/main/java/dev/openfga/sdk/api/client/ClientListRelationsRequest.java new file mode 100644 index 0000000..1f122f5 --- /dev/null +++ b/src/main/java/dev/openfga/sdk/api/client/ClientListRelationsRequest.java @@ -0,0 +1,66 @@ +/* + * 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: 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; + +import java.util.List; + +public class ClientListRelationsRequest { + private String user; + private String _object; + private List relations; + private List contextualTupleKeys; + + public ClientListRelationsRequest user(String user) { + this.user = user; + return this; + } + + /** + * Get user + * @return user + **/ + public String getUser() { + return user; + } + + public ClientListRelationsRequest _object(String _object) { + this._object = _object; + return this; + } + + public String getObject() { + return _object; + } + + public ClientListRelationsRequest relations(List relations) { + this.relations = relations; + return this; + } + + /** + * Get relations + * @return relations + **/ + public List getRelations() { + return relations; + } + + public ClientListRelationsRequest contextualTupleKeys(List contextualTupleKeys) { + this.contextualTupleKeys = contextualTupleKeys; + return this; + } + + public List getContextualTupleKeys() { + return contextualTupleKeys; + } +} diff --git a/src/main/java/dev/openfga/sdk/api/client/ClientReadRequest.java b/src/main/java/dev/openfga/sdk/api/client/ClientReadRequest.java new file mode 100644 index 0000000..1b9df66 --- /dev/null +++ b/src/main/java/dev/openfga/sdk/api/client/ClientReadRequest.java @@ -0,0 +1,58 @@ +/* + * 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: 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; + +public class ClientReadRequest { + private String user; + private String relation; + private String _object; + + public ClientReadRequest _object(String _object) { + this._object = _object; + return this; + } + + /** + * Get _object + * @return _object + **/ + public String getObject() { + return _object; + } + + public ClientReadRequest relation(String relation) { + this.relation = relation; + return this; + } + + /** + * Get relation + * @return relation + **/ + public String getRelation() { + return relation; + } + + public ClientReadRequest user(String user) { + this.user = user; + return this; + } + + /** + * Get user + * @return user + **/ + public String getUser() { + return user; + } +} diff --git a/src/main/java/dev/openfga/sdk/api/client/ClientTupleKey.java b/src/main/java/dev/openfga/sdk/api/client/ClientTupleKey.java new file mode 100644 index 0000000..7993519 --- /dev/null +++ b/src/main/java/dev/openfga/sdk/api/client/ClientTupleKey.java @@ -0,0 +1,88 @@ +/* + * 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: 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; + +import dev.openfga.sdk.api.model.ContextualTupleKeys; +import dev.openfga.sdk.api.model.TupleKey; +import dev.openfga.sdk.api.model.TupleKeys; +import java.util.List; +import java.util.stream.Collectors; + +public class ClientTupleKey { + private String user; + private String relation; + private String _object; + + public ClientTupleKey _object(String _object) { + this._object = _object; + return this; + } + + /** + * Get _object + * @return _object + **/ + public String getObject() { + return _object; + } + + public ClientTupleKey relation(String relation) { + this.relation = relation; + return this; + } + + /** + * Get relation + * @return relation + **/ + public String getRelation() { + return relation; + } + + public ClientTupleKey user(String user) { + this.user = user; + return this; + } + + /** + * Get user + * @return user + **/ + public String getUser() { + return user; + } + + public TupleKey asTupleKey() { + return new TupleKey().user(user).relation(relation)._object(_object); + } + + public static TupleKeys asTupleKeys(List clientTupleKeys) { + if (clientTupleKeys == null || clientTupleKeys.size() == 0) { + return new TupleKeys(); + } + + return new TupleKeys().tupleKeys(asListOfTupleKey(clientTupleKeys)); + } + + public static ContextualTupleKeys asContextualTupleKeys(List clientTupleKeys) { + if (clientTupleKeys == null || clientTupleKeys.size() == 0) { + return new ContextualTupleKeys(); + } + + return new ContextualTupleKeys().tupleKeys(asListOfTupleKey(clientTupleKeys)); + } + + private static List asListOfTupleKey(List clientTupleKeys) { + return clientTupleKeys.stream().map(ClientTupleKey::asTupleKey).collect(Collectors.toList()); + } +} diff --git a/src/main/java/dev/openfga/sdk/api/client/ClientWriteRequest.java b/src/main/java/dev/openfga/sdk/api/client/ClientWriteRequest.java new file mode 100644 index 0000000..ecbebbc --- /dev/null +++ b/src/main/java/dev/openfga/sdk/api/client/ClientWriteRequest.java @@ -0,0 +1,38 @@ +/* + * 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: 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; + +import java.util.List; + +public class ClientWriteRequest { + private List writes; + private List deletes; + + public ClientWriteRequest writes(List writes) { + this.writes = writes; + return this; + } + + public List getWrites() { + return writes; + } + + public ClientWriteRequest deletes(List deletes) { + this.deletes = deletes; + return this; + } + + public List getDeletes() { + return deletes; + } +} diff --git a/src/main/java/dev/openfga/sdk/api/client/OpenFgaClient.java b/src/main/java/dev/openfga/sdk/api/client/OpenFgaClient.java new file mode 100644 index 0000000..34517d6 --- /dev/null +++ b/src/main/java/dev/openfga/sdk/api/client/OpenFgaClient.java @@ -0,0 +1,509 @@ +/* + * 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: 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; + +import static dev.openfga.sdk.util.StringUtil.isNullOrWhitespace; + +import dev.openfga.sdk.api.*; +import dev.openfga.sdk.api.configuration.*; +import dev.openfga.sdk.api.model.*; +import dev.openfga.sdk.errors.*; +import java.util.List; +import java.util.concurrent.CompletableFuture; + +public class OpenFgaClient { + private ApiClient apiClient; + private ClientConfiguration configuration; + private OpenFgaApi api; + + 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; + + public OpenFgaClient(ApiClient apiClient, ClientConfiguration configuration) throws FgaInvalidParameterException { + this.apiClient = apiClient; + this.configuration = configuration; + this.api = new OpenFgaApi(apiClient, configuration); + } + + /* *********** + * Utilities * + *************/ + + public void setStoreId(String storeId) { + configuration.storeId(storeId); + } + + public void setAuthorizationModelId(String authorizationModelId) { + configuration.authorizationModelId(authorizationModelId); + } + + public void setConfiguration(ClientConfiguration configuration) throws FgaInvalidParameterException { + this.configuration = configuration; + this.api = new OpenFgaApi(apiClient, configuration); + } + + /* ******** + * Stores * + **********/ + + /** + * ListStores - Get a paginated list of stores. + */ + public CompletableFuture listStores() throws FgaInvalidParameterException { + configuration.assertValid(); + return call(() -> api.listStores(null, null)); + } + + public CompletableFuture listStores(ListStoresOptions options) + throws FgaInvalidParameterException { + configuration.assertValid(); + return call(() -> api.listStores(options.getPageSize(), options.getContinuationToken())); + } + + /** + * CreateStore - Initialize a store + */ + public CompletableFuture createStore(CreateStoreRequest request) + throws FgaInvalidParameterException { + configuration.assertValid(); + return call(() -> api.createStore(request)); + } + + /** + * GetStore - Get information about the current store. + * @throws FgaInvalidParameterException When the Store ID is null, empty, or whitespace + */ + public CompletableFuture getStore() throws FgaInvalidParameterException { + configuration.assertValid(); + String storeId = configuration.getStoreIdChecked(); + return call(() -> api.getStore(storeId)); + } + + /** + * DeleteStore - Delete a store + * + * @throws FgaInvalidParameterException When the Store ID is null, empty, or whitespace + */ + public CompletableFuture deleteStore() throws FgaInvalidParameterException { + configuration.assertValid(); + String storeId = configuration.getStoreIdChecked(); + return call(() -> api.deleteStore(storeId)); + } + + /* ********************** + * Authorization Models * + ************************/ + + /** + * ReadAuthorizationModels - Read all authorization models + * + * @throws FgaInvalidParameterException When the Store ID is null, empty, or whitespace + */ + public CompletableFuture readAuthorizationModels() + throws FgaInvalidParameterException { + return readAuthorizationModels(null); + } + + /** + * ReadAuthorizationModels - Read all authorization models + * + * @throws FgaInvalidParameterException When the Store ID is null, empty, or whitespace + */ + public CompletableFuture readAuthorizationModels( + ReadAuthorizationModelsOptions options) throws FgaInvalidParameterException { + configuration.assertValid(); + String storeId = configuration.getStoreIdChecked(); + + Integer pageSize; + String continuationToken; + + if (options != null) { + pageSize = options.getPageSize(); + continuationToken = options.getContinuationToken(); + } else { + // null are valid for these values + continuationToken = null; + pageSize = null; + } + + return call(() -> api.readAuthorizationModels(storeId, pageSize, continuationToken)); + } + + /** + * WriteAuthorizationModel - Create a new version of the authorization model + * + * @throws FgaInvalidParameterException When the Store ID is null, empty, or whitespace + */ + public CompletableFuture writeAuthorizationModel( + WriteAuthorizationModelRequest request) throws FgaInvalidParameterException { + configuration.assertValid(); + String storeId = configuration.getStoreIdChecked(); + return call(() -> api.writeAuthorizationModel(storeId, request)); + } + + /** + * ReadAuthorizationModel - Read the current authorization model + * + * @throws FgaInvalidParameterException When either the Store ID or Authorization Model ID are null, empty, or whitespace + */ + public CompletableFuture readAuthorizationModel() + throws FgaInvalidParameterException { + configuration.assertValid(); + String storeId = configuration.getStoreIdChecked(); + String authorizationModelId = configuration.getAuthorizationModelIdChecked(); + return call(() -> api.readAuthorizationModel(storeId, authorizationModelId)); + } + + /** + * ReadAuthorizationModel - Read the current authorization model + * + * @throws FgaInvalidParameterException When either the Store ID or Authorization Model ID are null, empty, or whitespace + */ + public CompletableFuture readAuthorizationModel( + ReadAuthorizationModelOptions options) throws FgaInvalidParameterException { + configuration.assertValid(); + String storeId = configuration.getStoreIdChecked(); + String authorizationModelId = options.getAuthorizationModelIdChecked(); + return call(() -> api.readAuthorizationModel(storeId, authorizationModelId)); + } + + /** + * ReadLatestAuthorizationModel - Read the latest authorization model for the current store + * + * @throws FgaInvalidParameterException When the Store ID is null, empty, or whitespace + */ + public CompletableFuture readLatestAuthorizationModel() + throws FgaInvalidParameterException { + configuration.assertValid(); + String storeId = configuration.getStoreIdChecked(); + return call(() -> api.readAuthorizationModels(storeId, 1, null)) + .thenApply(response -> new ReadAuthorizationModelResponse() + .authorizationModel(response.getAuthorizationModels().get(0))); + } + + /* ********************* + * Relationship Tuples * + ***********************/ + + /** + * Read Changes - Read the list of historical relationship tuple writes and deletes + * + * @throws FgaInvalidParameterException When the Store ID is null, empty, or whitespace + */ + public CompletableFuture readChanges(ReadChangesOptions options) + throws FgaInvalidParameterException { + configuration.assertValid(); + String storeId = configuration.getStoreIdChecked(); + return call(() -> + api.readChanges(storeId, options.getType(), options.getPageSize(), options.getContinuationToken())); + } + + /** + * Read - Read tuples previously written to the store (does not evaluate) + * + * @throws FgaInvalidParameterException When the Store ID is null, empty, or whitespace + */ + public CompletableFuture read(ClientReadRequest request) throws FgaInvalidParameterException { + return read(request, null); + } + + /** + * Read - Read tuples previously written to the store (does not evaluate) + * + * @throws FgaInvalidParameterException When the Store ID is null, empty, or whitespace + */ + public CompletableFuture read(ClientReadRequest request, ClientReadOptions options) + throws FgaInvalidParameterException { + configuration.assertValid(); + String storeId = configuration.getStoreIdChecked(); + + ReadRequest body = new ReadRequest(); + TupleKey tupleKey = new TupleKey(); + + if (request != null) { + tupleKey.user(request.getUser()).relation(request.getRelation())._object(request.getObject()); + } + + if (options != null) { + body.pageSize(options.getPageSize()).continuationToken(options.getContinuationToken()); + } + + body.tupleKey(tupleKey); + + return call(() -> api.read(storeId, body)); + } + + /** + * Write - Create or delete relationship tuples + * + * @throws FgaInvalidParameterException When the Store ID is null, empty, or whitespace + */ + public CompletableFuture write(ClientWriteRequest request) throws FgaInvalidParameterException { + return write(request, null); + } + + /** + * Write - Create or delete relationship tuples + * + * @throws FgaInvalidParameterException When the Store ID is null, empty, or whitespace + */ + public CompletableFuture write(ClientWriteRequest request, ClientWriteOptions options) + throws FgaInvalidParameterException { + configuration.assertValid(); + String storeId = configuration.getStoreIdChecked(); + + WriteRequest body = new WriteRequest(); + + if (request != null) { + TupleKeys writes = ClientTupleKey.asTupleKeys(request.getWrites()); + if (!writes.getTupleKeys().isEmpty()) { + body.writes(writes); + } + + TupleKeys deletes = ClientTupleKey.asTupleKeys(request.getDeletes()); + if (!deletes.getTupleKeys().isEmpty()) { + body.deletes(deletes); + } + } + + if (options != null && !isNullOrWhitespace(options.getAuthorizationModelId())) { + body.authorizationModelId(options.getAuthorizationModelId()); + } else { + String authorizationModelId = configuration.getAuthorizationModelId(); + body.authorizationModelId(authorizationModelId); + } + + return call(() -> api.write(storeId, body)); + } + + /** + * WriteTuples - Utility method to write tuples, wraps Write + * + * @throws FgaInvalidParameterException When the Store ID is null, empty, or whitespace + */ + public CompletableFuture writeTuples(List tupleKeys) throws FgaInvalidParameterException { + configuration.assertValid(); + String storeId = configuration.getStoreIdChecked(); + + var request = new WriteRequest().writes(ClientTupleKey.asTupleKeys(tupleKeys)); + String authorizationModelId = configuration.getAuthorizationModelId(); + if (!isNullOrWhitespace(authorizationModelId)) { + request.authorizationModelId(authorizationModelId); + } + + return call(() -> api.write(storeId, request)); + } + + /** + * DeleteTuples - Utility method to delete tuples, wraps Write + * + * @throws FgaInvalidParameterException When the Store ID is null, empty, or whitespace + */ + public CompletableFuture deleteTuples(List tupleKeys) throws FgaInvalidParameterException { + configuration.assertValid(); + String storeId = configuration.getStoreIdChecked(); + + var request = new WriteRequest().deletes(ClientTupleKey.asTupleKeys(tupleKeys)); + String authorizationModelId = configuration.getAuthorizationModelId(); + if (!isNullOrWhitespace(authorizationModelId)) { + request.authorizationModelId(authorizationModelId); + } + + return call(() -> api.write(storeId, request)); + } + + /* ********************** + * Relationship Queries * + ***********************/ + + /** + * Check - Check if a user has a particular relation with an object (evaluates) + * + * @throws FgaInvalidParameterException When the Store ID is null, empty, or whitespace + */ + public CompletableFuture check(ClientCheckRequest request) throws FgaInvalidParameterException { + return check(request, null); + } + + /** + * Check - Check if a user has a particular relation with an object (evaluates) + * + * @throws FgaInvalidParameterException When the Store ID is null, empty, or whitespace + */ + public CompletableFuture check(ClientCheckRequest request, ClientCheckOptions options) + throws FgaInvalidParameterException { + configuration.assertValid(); + String storeId = configuration.getStoreIdChecked(); + + CheckRequest body = new CheckRequest(); + + if (request != null) { + body.tupleKey(new TupleKey() + .user(request.getUser()) + .relation(request.getRelation()) + ._object(request.getObject())); + } + + if (options != null && !isNullOrWhitespace(options.getAuthorizationModelId())) { + body.authorizationModelId(options.getAuthorizationModelId()); + } else { + String authorizationModelId = configuration.getAuthorizationModelId(); + body.authorizationModelId(authorizationModelId); + } + + return call(() -> api.check(storeId, body)); + } + + /** + * BatchCheck - Run a set of checks (evaluates) + * + * @throws FgaInvalidParameterException When the Store ID is null, empty, or whitespace + */ + // TODO + + /** + * Expand - Expands the relationships in userset tree format (evaluates) + * + * @throws FgaInvalidParameterException When the Store ID is null, empty, or whitespace + */ + public CompletableFuture expand(ClientExpandRequest request) throws FgaInvalidParameterException { + return expand(request, null); + } + + /** + * Expand - Expands the relationships in userset tree format (evaluates) + * + * @throws FgaInvalidParameterException When the Store ID is null, empty, or whitespace + */ + public CompletableFuture expand(ClientExpandRequest request, ClientExpandOptions options) + throws FgaInvalidParameterException { + configuration.assertValid(); + String storeId = configuration.getStoreIdChecked(); + + ExpandRequest body = new ExpandRequest(); + + if (request != null) { + body.tupleKey(new TupleKey() + .user(request.getUser()) + .relation(request.getRelation()) + ._object(request.getObject())); + } + + if (options != null && !isNullOrWhitespace(options.getAuthorizationModelId())) { + body.authorizationModelId(options.getAuthorizationModelId()); + } else { + String authorizationModelId = configuration.getAuthorizationModelId(); + body.authorizationModelId(authorizationModelId); + } + + return call(() -> api.expand(storeId, body)); + } + + /** + * ListObjects - List the objects of a particular type that the user has a certain relation to (evaluates) + * + * @throws FgaInvalidParameterException When the Store ID is null, empty, or whitespace + */ + public CompletableFuture listObjects(ClientListObjectsRequest request) + throws FgaInvalidParameterException { + return listObjects(request, null); + } + + /** + * ListObjects - List the objects of a particular type that the user has a certain relation to (evaluates) + * + * @throws FgaInvalidParameterException When the Store ID is null, empty, or whitespace + */ + public CompletableFuture listObjects( + ClientListObjectsRequest request, ClientListObjectsOptions options) throws FgaInvalidParameterException { + configuration.assertValid(); + String storeId = configuration.getStoreIdChecked(); + + ListObjectsRequest body = new ListObjectsRequest(); + + if (request != null) { + body.user(request.getUser()) + .relation(request.getRelation()) + .type(request.getType()) + .contextualTuples(ClientTupleKey.asContextualTupleKeys(request.getContextualTupleKeys())); + } + + if (options != null && !isNullOrWhitespace(options.getAuthorizationModelId())) { + body.authorizationModelId(options.getAuthorizationModelId()); + } else { + String authorizationModelId = configuration.getAuthorizationModelId(); + body.authorizationModelId(authorizationModelId); + } + + return call(() -> api.listObjects(storeId, body)); + } + + /* + * ListRelations - List all the relations a user has with an object (evaluates) + */ + // TODO + + /* ************ + * Assertions * + **************/ + + /** + * ReadAssertions - Read assertions for a particular authorization model + * + * @throws FgaInvalidParameterException When either the Store ID or Authorization Model ID is null, empty, or whitespace + */ + public CompletableFuture readAssertions() throws FgaInvalidParameterException { + // TODO: Add version of this function that accepts ClientReadAssertionsOptions + configuration.assertValid(); + String storeId = configuration.getStoreIdChecked(); + String authorizationModelId = configuration.getAuthorizationModelIdChecked(); + return call(() -> api.readAssertions(storeId, authorizationModelId)); + } + + /** + * WriteAssertions - Updates assertions for a particular authorization model + * + * @throws FgaInvalidParameterException When either the Store ID or Authorization Model ID is null, empty, or whitespace + */ + public CompletableFuture writeAssertions(List assertions) + throws FgaInvalidParameterException { + // TODO: Add version of this function that accepts ClientWriteAssertionsOptions + configuration.assertValid(); + String storeId = configuration.getStoreIdChecked(); + String authorizationModelId = configuration.getAuthorizationModelIdChecked(); + + WriteAssertionsRequest body = new WriteAssertionsRequest().assertions(ClientAssertion.asAssertions(assertions)); + + return call(() -> api.writeAssertions(storeId, authorizationModelId, body)); + } + + /** + * A {@link FunctionalInterface} for calling a low-level API from {@link OpenFgaApi}. It wraps exceptions + * encountered with {@link CompletableFuture#failedFuture(Throwable)} + * + * @param The type of API response + */ + @FunctionalInterface + private interface CheckedInvocation { + CompletableFuture call() throws FgaInvalidParameterException, ApiException; + } + + private CompletableFuture call(CheckedInvocation action) { + try { + return action.call(); + } catch (FgaInvalidParameterException | ApiException exception) { + return CompletableFuture.failedFuture(exception); + } + } +} diff --git a/src/main/java/dev/openfga/sdk/api/configuration/ClientCheckOptions.java b/src/main/java/dev/openfga/sdk/api/configuration/ClientCheckOptions.java new file mode 100644 index 0000000..b69a4cf --- /dev/null +++ b/src/main/java/dev/openfga/sdk/api/configuration/ClientCheckOptions.java @@ -0,0 +1,26 @@ +/* + * 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: 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.configuration; + +public class ClientCheckOptions { + private String authorizationModelId; + + public ClientCheckOptions authorizationModelId(String authorizationModelId) { + this.authorizationModelId = authorizationModelId; + return this; + } + + public String getAuthorizationModelId() { + return authorizationModelId; + } +} diff --git a/src/main/java/dev/openfga/sdk/api/configuration/ClientConfiguration.java b/src/main/java/dev/openfga/sdk/api/configuration/ClientConfiguration.java new file mode 100644 index 0000000..5cd9bb5 --- /dev/null +++ b/src/main/java/dev/openfga/sdk/api/configuration/ClientConfiguration.java @@ -0,0 +1,135 @@ +/* + * 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: 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.configuration; + +import static dev.openfga.sdk.util.StringUtil.isNullOrWhitespace; + +import dev.openfga.sdk.errors.FgaInvalidParameterException; +import java.time.Duration; + +public class ClientConfiguration extends Configuration { + private String storeId; + private String authorizationModelId; + + public void assertValidStoreId() throws FgaInvalidParameterException { + if (isNullOrWhitespace(storeId)) { + throw new FgaInvalidParameterException("storeId", "ClientConfiguration"); + } + } + + public void assertValidAuthorizationModelId() throws FgaInvalidParameterException { + if (isNullOrWhitespace(authorizationModelId)) { + throw new FgaInvalidParameterException("authorizationModelId", "ClientConfiguration"); + } + } + + /** + * Set the Store ID. + * + * @param storeId The URL. + * @return This object. + */ + public ClientConfiguration storeId(String storeId) { + this.storeId = storeId; + return this; + } + + /** + * Get the Authorization Model ID. + * + * @return The Authorization Model ID. + */ + public String getStoreId() { + return storeId; + } + + /** + * Get the Store ID. + * + * @return The Store ID. + * @throws FgaInvalidParameterException when the Store ID is null, empty, or whitespace + */ + public String getStoreIdChecked() throws FgaInvalidParameterException { + assertValidStoreId(); + return storeId; + } + + /** + * Set the Authorization Model ID. + * + * @param authorizationModelId The URL. + * @return This object. + */ + public ClientConfiguration authorizationModelId(String authorizationModelId) { + this.authorizationModelId = authorizationModelId; + return this; + } + + /** + * Get the Authorization Model ID. + * + * @return The Authorization Model ID. + */ + public String getAuthorizationModelId() { + return authorizationModelId; + } + + /** + * Get the Authorization Model ID. + * + * @return The Authorization Model ID. + * @throws FgaInvalidParameterException when the Authorization Model ID is null, empty, or whitespace + */ + public String getAuthorizationModelIdChecked() throws FgaInvalidParameterException { + assertValidAuthorizationModelId(); + return authorizationModelId; + } + + /* Overrides beyond this point required for typing. */ + + @Override + public ClientConfiguration override(ConfigurationOverride configurationOverride) { + super.override(configurationOverride); + return this; + } + + @Override + public ClientConfiguration apiUrl(String apiUrl) { + super.apiUrl(apiUrl); + return this; + } + + @Override + public ClientConfiguration credentials(Credentials credentials) { + super.credentials(credentials); + return this; + } + + @Override + public ClientConfiguration userAgent(String userAgent) { + super.userAgent(userAgent); + return this; + } + + @Override + public ClientConfiguration readTimeout(Duration readTimeout) { + super.readTimeout(readTimeout); + return this; + } + + @Override + public ClientConfiguration connectTimeout(Duration connectTimeout) { + super.connectTimeout(connectTimeout); + return this; + } +} diff --git a/src/main/java/dev/openfga/sdk/api/configuration/ClientExpandOptions.java b/src/main/java/dev/openfga/sdk/api/configuration/ClientExpandOptions.java new file mode 100644 index 0000000..66dc5b5 --- /dev/null +++ b/src/main/java/dev/openfga/sdk/api/configuration/ClientExpandOptions.java @@ -0,0 +1,26 @@ +/* + * 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: 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.configuration; + +public class ClientExpandOptions { + private String authorizationModelId; + + public ClientExpandOptions authorizationModelId(String authorizationModelId) { + this.authorizationModelId = authorizationModelId; + return this; + } + + public String getAuthorizationModelId() { + return authorizationModelId; + } +} diff --git a/src/main/java/dev/openfga/sdk/api/configuration/ClientListObjectsOptions.java b/src/main/java/dev/openfga/sdk/api/configuration/ClientListObjectsOptions.java new file mode 100644 index 0000000..67ba5f4 --- /dev/null +++ b/src/main/java/dev/openfga/sdk/api/configuration/ClientListObjectsOptions.java @@ -0,0 +1,26 @@ +/* + * 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: 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.configuration; + +public class ClientListObjectsOptions { + private String authorizationModelId; + + public ClientListObjectsOptions authorizationModelId(String authorizationModelId) { + this.authorizationModelId = authorizationModelId; + return this; + } + + public String getAuthorizationModelId() { + return authorizationModelId; + } +} diff --git a/src/main/java/dev/openfga/sdk/api/configuration/ClientReadOptions.java b/src/main/java/dev/openfga/sdk/api/configuration/ClientReadOptions.java new file mode 100644 index 0000000..c3a44b2 --- /dev/null +++ b/src/main/java/dev/openfga/sdk/api/configuration/ClientReadOptions.java @@ -0,0 +1,36 @@ +/* + * 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: 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.configuration; + +public class ClientReadOptions { + private Integer pageSize; + private String continuationToken; + + public ClientReadOptions pageSize(Integer pageSize) { + this.pageSize = pageSize; + return this; + } + + public Integer getPageSize() { + return pageSize; + } + + public ClientReadOptions continuationToken(String continuationToken) { + this.continuationToken = continuationToken; + return this; + } + + public String getContinuationToken() { + return continuationToken; + } +} diff --git a/src/main/java/dev/openfga/sdk/api/configuration/ClientWriteOptions.java b/src/main/java/dev/openfga/sdk/api/configuration/ClientWriteOptions.java new file mode 100644 index 0000000..cd60f92 --- /dev/null +++ b/src/main/java/dev/openfga/sdk/api/configuration/ClientWriteOptions.java @@ -0,0 +1,26 @@ +/* + * 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: 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.configuration; + +public class ClientWriteOptions { + private String authorizationModelId; + + public ClientWriteOptions authorizationModelId(String authorizationModelId) { + this.authorizationModelId = authorizationModelId; + return this; + } + + public String getAuthorizationModelId() { + return authorizationModelId; + } +} diff --git a/src/main/java/dev/openfga/sdk/api/configuration/Configuration.java b/src/main/java/dev/openfga/sdk/api/configuration/Configuration.java index feeb8a9..8738c9d 100644 --- a/src/main/java/dev/openfga/sdk/api/configuration/Configuration.java +++ b/src/main/java/dev/openfga/sdk/api/configuration/Configuration.java @@ -27,10 +27,10 @@ * Configurations for an api client. */ public class Configuration implements BaseConfiguration { - public static final String VERSION = "0.0.4"; + public static final String VERSION = "0.0.5"; private static final String DEFAULT_API_URL = "http://localhost:8080"; - private static final String DEFAULT_USER_AGENT = "openfga-sdk java/0.0.4"; + private static final String DEFAULT_USER_AGENT = "openfga-sdk java/0.0.5"; private static final Duration DEFAULT_READ_TIMEOUT = Duration.ofSeconds(10); private static final Duration DEFAULT_CONNECT_TIMEOUT = Duration.ofSeconds(10); @@ -47,21 +47,6 @@ public Configuration() { this.connectTimeout = DEFAULT_CONNECT_TIMEOUT; } - public Configuration(String apiUrl) { - this.apiUrl = apiUrl; - this.userAgent = DEFAULT_USER_AGENT; - this.readTimeout = DEFAULT_READ_TIMEOUT; - this.connectTimeout = DEFAULT_CONNECT_TIMEOUT; - } - - public Configuration(String apiUrl, Credentials credentials) { - this.apiUrl = apiUrl; - this.credentials = credentials; - this.userAgent = DEFAULT_USER_AGENT; - this.readTimeout = DEFAULT_READ_TIMEOUT; - this.connectTimeout = DEFAULT_CONNECT_TIMEOUT; - } - /** * Assert that the configuration is valid. */ diff --git a/src/main/java/dev/openfga/sdk/api/configuration/ListStoresOptions.java b/src/main/java/dev/openfga/sdk/api/configuration/ListStoresOptions.java new file mode 100644 index 0000000..57f06c2 --- /dev/null +++ b/src/main/java/dev/openfga/sdk/api/configuration/ListStoresOptions.java @@ -0,0 +1,36 @@ +/* + * 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: 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.configuration; + +public class ListStoresOptions { + private Integer pageSize; + private String continuationToken; + + public ListStoresOptions pageSize(Integer pageSize) { + this.pageSize = pageSize; + return this; + } + + public Integer getPageSize() { + return pageSize; + } + + public ListStoresOptions continuationToken(String continuationToken) { + this.continuationToken = continuationToken; + return this; + } + + public String getContinuationToken() { + return continuationToken; + } +} diff --git a/src/main/java/dev/openfga/sdk/api/configuration/ReadAuthorizationModelOptions.java b/src/main/java/dev/openfga/sdk/api/configuration/ReadAuthorizationModelOptions.java new file mode 100644 index 0000000..adffdf6 --- /dev/null +++ b/src/main/java/dev/openfga/sdk/api/configuration/ReadAuthorizationModelOptions.java @@ -0,0 +1,41 @@ +/* + * 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: 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.configuration; + +import static dev.openfga.sdk.util.StringUtil.isNullOrWhitespace; + +import dev.openfga.sdk.errors.FgaInvalidParameterException; + +public class ReadAuthorizationModelOptions { + private String authorizationModelId; + + public void assertValidAuthorizationModelId() throws FgaInvalidParameterException { + if (isNullOrWhitespace(authorizationModelId)) { + throw new FgaInvalidParameterException("authorizationModelId", "ClientConfiguration"); + } + } + + public ReadAuthorizationModelOptions authorizationModelId(String authorizationModelId) { + this.authorizationModelId = authorizationModelId; + return this; + } + + public String getAuthorizationModelId() { + return authorizationModelId; + } + + public String getAuthorizationModelIdChecked() throws FgaInvalidParameterException { + assertValidAuthorizationModelId(); + return authorizationModelId; + } +} diff --git a/src/main/java/dev/openfga/sdk/api/configuration/ReadAuthorizationModelsOptions.java b/src/main/java/dev/openfga/sdk/api/configuration/ReadAuthorizationModelsOptions.java new file mode 100644 index 0000000..edc9982 --- /dev/null +++ b/src/main/java/dev/openfga/sdk/api/configuration/ReadAuthorizationModelsOptions.java @@ -0,0 +1,36 @@ +/* + * 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: 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.configuration; + +public class ReadAuthorizationModelsOptions { + private Integer pageSize; + private String continuationToken; + + public ReadAuthorizationModelsOptions pageSize(Integer pageSize) { + this.pageSize = pageSize; + return this; + } + + public Integer getPageSize() { + return pageSize; + } + + public ReadAuthorizationModelsOptions continuationToken(String continuationToken) { + this.continuationToken = continuationToken; + return this; + } + + public String getContinuationToken() { + return continuationToken; + } +} diff --git a/src/main/java/dev/openfga/sdk/api/configuration/ReadChangesOptions.java b/src/main/java/dev/openfga/sdk/api/configuration/ReadChangesOptions.java new file mode 100644 index 0000000..0b032b2 --- /dev/null +++ b/src/main/java/dev/openfga/sdk/api/configuration/ReadChangesOptions.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: 0.1 + * 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.configuration; + +public class ReadChangesOptions { + private String type; + private Integer pageSize; + private String continuationToken; + + public ReadChangesOptions type(String type) { + this.type = type; + return this; + } + + public String getType() { + return type; + } + + public ReadChangesOptions pageSize(Integer pageSize) { + this.pageSize = pageSize; + return this; + } + + public Integer getPageSize() { + return pageSize; + } + + public ReadChangesOptions continuationToken(String continuationToken) { + this.continuationToken = continuationToken; + return this; + } + + public String getContinuationToken() { + return continuationToken; + } +} diff --git a/src/test-integration/java/dev/openfga/sdk/api/OpenFgaApiIntegrationTest.java b/src/test-integration/java/dev/openfga/sdk/api/OpenFgaApiIntegrationTest.java index 3cc1196..53911eb 100644 --- a/src/test-integration/java/dev/openfga/sdk/api/OpenFgaApiIntegrationTest.java +++ b/src/test-integration/java/dev/openfga/sdk/api/OpenFgaApiIntegrationTest.java @@ -38,7 +38,7 @@ public class OpenFgaApiIntegrationTest { @BeforeEach public void initializeApi() throws Exception { - Configuration apiConfig = new Configuration("http://localhost:8080"); + Configuration apiConfig = new Configuration().apiUrl("http://localhost:8080"); ApiClient apiClient = new ApiClient(HttpClient.newBuilder(), mapper); api = new OpenFgaApi(apiClient, apiConfig); } diff --git a/src/test-integration/java/dev/openfga/sdk/api/client/OpenFgaClientIntegrationTest.java b/src/test-integration/java/dev/openfga/sdk/api/client/OpenFgaClientIntegrationTest.java new file mode 100644 index 0000000..219f320 --- /dev/null +++ b/src/test-integration/java/dev/openfga/sdk/api/client/OpenFgaClientIntegrationTest.java @@ -0,0 +1,340 @@ +/* + * 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: 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; + +import static org.junit.jupiter.api.Assertions.*; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import dev.openfga.sdk.api.configuration.*; +import dev.openfga.sdk.api.model.*; +import java.net.http.HttpClient; +import java.util.List; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +public class OpenFgaClientIntegrationTest { + private static final ObjectMapper mapper = new ObjectMapper().findAndRegisterModules(); + private static final String DEFAULT_AUTH_MODEL = + "{\"schema_version\":\"1.1\",\"type_definitions\":[{\"type\":\"user\"},{\"type\":\"document\",\"relations\":{\"reader\":{\"this\":{}},\"writer\":{\"this\":{}},\"owner\":{\"this\":{}}},\"metadata\":{\"relations\":{\"reader\":{\"directly_related_user_types\":[{\"type\":\"user\"}]},\"writer\":{\"directly_related_user_types\":[{\"type\":\"user\"}]},\"owner\":{\"directly_related_user_types\":[{\"type\":\"user\"}]}}}}]}"; + private static final String DEFAULT_USER = "user:81684243-9356-4421-8fbf-a4f8d36aa31b"; + private static final String DEFAULT_DOC = "document:2021-budget"; + public static final ClientTupleKey DEFAULT_TUPLE_KEY = + new ClientTupleKey().user(DEFAULT_USER).relation("reader")._object(DEFAULT_DOC); + public static final List DEFAULT_TUPLE_KEYS = List.of(DEFAULT_TUPLE_KEY); + public static final ClientAssertion DEFAULT_ASSERTION = new ClientAssertion() + .user(DEFAULT_USER) + .relation("reader") + ._object(DEFAULT_DOC) + .expectation(true); + + private OpenFgaClient fga; + + @BeforeEach + public void initializeApi() throws Exception { + ClientConfiguration apiConfig = new ClientConfiguration().apiUrl("http://localhost:8080"); + ApiClient apiClient = new ApiClient(HttpClient.newBuilder(), mapper); + fga = new OpenFgaClient(apiClient, apiConfig); + } + + @Test + public void createStore() throws Exception { + // Given + String storeName = thisTestName(); + CreateStoreRequest createStoreRequest = new CreateStoreRequest().name(storeName); + + // When + CreateStoreResponse response = fga.createStore(createStoreRequest).get(); + + // Then + assertEquals("OpenFgaClientIntegrationTest.createStore", response.getName()); + } + + @Test + public void deleteStore() throws Exception { + // Given + String storeName = thisTestName(); + String storeId = createStore(storeName); + fga.setStoreId(storeId); + + // When + fga.deleteStore().get(); + + // Then + ListStoresResponse response = fga.listStores().get(); + assertNotNull(response.getStores()); + boolean itWasDeleted = response.getStores().stream().map(Store::getId).noneMatch(storeId::equals); + assertTrue(itWasDeleted, String.format("No stores should remain with the id %s.", storeId)); + } + + @Test + public void getStore() throws Exception { + // Given + String storeName = thisTestName(); + String storeId = createStore(storeName); + fga.setStoreId(storeId); + + // When + GetStoreResponse response = fga.getStore().get(); + + // Then + assertEquals(storeName, response.getName()); + } + + @Test + public void listStores() throws Exception { + // Given + String testName = thisTestName(); + String store1 = testName + "-store1"; + String store2 = testName + "-store2"; + String store3 = testName + "-store3"; + List stores = List.of(store1, store2, store3); + for (String store : stores) { + createStore(store); + } + + // When + ListStoresResponse response = fga.listStores().get(); + + // Then + for (String store : stores) { + assertNotNull(response.getStores()); + boolean exists = response.getStores().stream().map(Store::getName).anyMatch(store::equals); + assertTrue(exists, String.format("Store %s should be in listStores response", store)); + } + } + + @Test + public void readAuthModel() throws Exception { + // Given + String storeName = thisTestName(); + String storeId = createStore(storeName); + fga.setStoreId(storeId); + String authModelId = writeAuthModel(storeId); + fga.setAuthorizationModelId(authModelId); + + // When + ReadAuthorizationModelResponse response = fga.readAuthorizationModel().get(); + + // Then + AuthorizationModel authModel = response.getAuthorizationModel(); + assertNotNull(authModel); + assertEquals(authModelId, response.getAuthorizationModel().getId()); + String typeDefsJson = mapper.writeValueAsString(authModel.getTypeDefinitions()); + assertEquals( + "[{\"type\":\"user\",\"relations\":{},\"metadata\":null},{\"type\":\"document\",\"relations\":{\"owner\":{\"this\":{},\"computedUserset\":null,\"tupleToUserset\":null,\"union\":null,\"intersection\":null,\"difference\":null},\"reader\":{\"this\":{},\"computedUserset\":null,\"tupleToUserset\":null,\"union\":null,\"intersection\":null,\"difference\":null},\"writer\":{\"this\":{},\"computedUserset\":null,\"tupleToUserset\":null,\"union\":null,\"intersection\":null,\"difference\":null}},\"metadata\":{\"relations\":{\"owner\":{\"directly_related_user_types\":[{\"type\":\"user\",\"relation\":null,\"wildcard\":null}]},\"reader\":{\"directly_related_user_types\":[{\"type\":\"user\",\"relation\":null,\"wildcard\":null}]},\"writer\":{\"directly_related_user_types\":[{\"type\":\"user\",\"relation\":null,\"wildcard\":null}]}}}}]", + typeDefsJson); + } + + @Test + public void readAuthModels() throws Exception { + // Given + String storeName = thisTestName(); + String storeId = createStore(storeName); + fga.setStoreId(storeId); + String authModelId = writeAuthModel(storeId); + fga.setAuthorizationModelId(authModelId); + var options = new ReadAuthorizationModelsOptions(); + + // When + ReadAuthorizationModelsResponse response = + fga.readAuthorizationModels(options).get(); + + // Then + assertNotNull(response.getAuthorizationModels()); + response.getAuthorizationModels().stream() + .filter(authModel -> authModelId.equals(authModel.getId())) + .forEach(authModel -> { + assertEquals(authModelId, authModel.getId()); + try { + String typeDefsJson = mapper.writeValueAsString(authModel.getTypeDefinitions()); + + assertEquals( + "[{\"type\":\"user\",\"relations\":{},\"metadata\":null},{\"type\":\"document\",\"relations\":{\"owner\":{\"this\":{},\"computedUserset\":null,\"tupleToUserset\":null,\"union\":null,\"intersection\":null,\"difference\":null},\"reader\":{\"this\":{},\"computedUserset\":null,\"tupleToUserset\":null,\"union\":null,\"intersection\":null,\"difference\":null},\"writer\":{\"this\":{},\"computedUserset\":null,\"tupleToUserset\":null,\"union\":null,\"intersection\":null,\"difference\":null}},\"metadata\":{\"relations\":{\"owner\":{\"directly_related_user_types\":[{\"type\":\"user\",\"relation\":null,\"wildcard\":null}]},\"reader\":{\"directly_related_user_types\":[{\"type\":\"user\",\"relation\":null,\"wildcard\":null}]},\"writer\":{\"directly_related_user_types\":[{\"type\":\"user\",\"relation\":null,\"wildcard\":null}]}}}}]", + typeDefsJson); + } catch (JsonProcessingException ex) { + assertNull(ex); + } + }); + } + + @Test + public void writeAuthModel() throws Exception { + // Given + String storeName = thisTestName(); + String storeId = createStore(storeName); + fga.setStoreId(storeId); + WriteAuthorizationModelRequest request = + mapper.readValue(DEFAULT_AUTH_MODEL, WriteAuthorizationModelRequest.class); + + // When + WriteAuthorizationModelResponse response = + fga.writeAuthorizationModel(request).get(); + + // Then + assertNotNull(response); + assertNotNull(response.getAuthorizationModelId()); + assertNotEquals("", response.getAuthorizationModelId()); + } + + @Test + public void write_and_read() throws Exception { + // Given + String storeName = thisTestName(); + String storeId = createStore(storeName); + fga.setStoreId(storeId); + String authModelId = writeAuthModel(storeId); + fga.setAuthorizationModelId(authModelId); + + ClientWriteRequest writeRequest = new ClientWriteRequest().writes(List.of(DEFAULT_TUPLE_KEY)); + ClientReadRequest readRequest = + new ClientReadRequest().user(DEFAULT_USER)._object(DEFAULT_DOC); + + // When + fga.write(writeRequest).get(); + ReadResponse response = fga.read(readRequest).get(); + + // Then + assertNotNull(response.getTuples()); + TupleKey key = response.getTuples().get(0).getKey(); + assertNotNull(key); + assertEquals(DEFAULT_USER, key.getUser()); + assertEquals("reader", key.getRelation()); + assertEquals(DEFAULT_DOC, key.getObject()); + } + + @Test + public void write_and_check() throws Exception { + // Given + String storeName = thisTestName(); + String storeId = createStore(storeName); + fga.setStoreId(storeId); + String authModelId = writeAuthModel(storeId); + fga.setAuthorizationModelId(authModelId); + ClientWriteRequest writeRequest = new ClientWriteRequest().writes(List.of(DEFAULT_TUPLE_KEY)); + ClientCheckRequest checkRequest = + new ClientCheckRequest().user(DEFAULT_USER).relation("reader")._object(DEFAULT_DOC); + + // When + fga.write(writeRequest).get(); + CheckResponse response = fga.check(checkRequest).get(); + + // Then + assertNotNull(response.getAllowed()); + assertTrue(response.getAllowed()); + } + + @Test + public void write_and_expand() throws Exception { + // Given + String storeName = thisTestName(); + String storeId = createStore(storeName); + fga.setStoreId(storeId); + String authModelId = writeAuthModel(storeId); + fga.setAuthorizationModelId(authModelId); + ClientWriteRequest writeRequest = new ClientWriteRequest().writes(List.of(DEFAULT_TUPLE_KEY)); + ClientExpandRequest expandRequest = + new ClientExpandRequest()._object(DEFAULT_DOC).relation("reader"); + + // When + fga.write(writeRequest).get(); + ExpandResponse response = fga.expand(expandRequest).get(); + + // Then + assertNotNull(response.getTree()); + String responseJson = mapper.writeValueAsString(response); + assertEquals( + "{\"tree\":{\"root\":{\"name\":\"document:2021-budget#reader\",\"leaf\":{\"users\":{\"users\":[\"user:81684243-9356-4421-8fbf-a4f8d36aa31b\"]},\"computed\":null,\"tupleToUserset\":null},\"difference\":null,\"union\":null,\"intersection\":null}}}", + responseJson); + } + + @Test + public void write_and_listObjects() throws Exception { + // Given + String storeName = thisTestName(); + String storeId = createStore(storeName); + fga.setStoreId(storeId); + String authModelId = writeAuthModel(storeId); + fga.setAuthorizationModelId(authModelId); + ClientWriteRequest writeRequest = new ClientWriteRequest().writes(List.of(DEFAULT_TUPLE_KEY)); + ClientListObjectsRequest listObjectsRequest = new ClientListObjectsRequest() + .user(DEFAULT_USER) + .relation("reader") + .type("document"); + + // When + fga.write(writeRequest).get(); + ListObjectsResponse response = fga.listObjects(listObjectsRequest).get(); + + // Then + assertNotNull(response.getObjects()); + assertEquals(1, response.getObjects().size()); + assertEquals(DEFAULT_DOC, response.getObjects().get(0)); + } + + @Test + public void write_readAssertions() throws Exception { + // Given + String storeName = thisTestName(); + String storeId = createStore(storeName); + fga.setStoreId(storeId); + String authModelId = writeAuthModel(storeId); + fga.setAuthorizationModelId(authModelId); + List assertions = List.of(DEFAULT_ASSERTION); + + // When + fga.writeAssertions(assertions).get(); + ReadAssertionsResponse response = fga.readAssertions().get(); + + // Then + String responseJson = mapper.writeValueAsString(response.getAssertions()); + assertEquals( + "[{\"tuple_key\":{\"object\":\"document:2021-budget\",\"relation\":\"reader\",\"user\":\"user:81684243-9356-4421-8fbf-a4f8d36aa31b\"},\"expectation\":true}]", + responseJson); + } + + /** + * Create a store for a given name. If tests fail here, troubleshoot with the no-arguments + * test method createStore(). + * @return The created Store ID + */ + private String createStore(String storeName) throws Exception { + CreateStoreResponse response = + fga.createStore(new CreateStoreRequest().name(storeName)).get(); + return response.getId(); + } + + /** + * Add a default authorization model to a store. If tests fail here, troubleshoot with the + * no-arguments @Test writeAuthModel() method. + * @return The created Authorization Model ID + */ + private String writeAuthModel(String storeId) throws Exception { + fga.setStoreId(storeId); + WriteAuthorizationModelRequest request = + mapper.readValue(DEFAULT_AUTH_MODEL, WriteAuthorizationModelRequest.class); + WriteAuthorizationModelResponse response = + fga.writeAuthorizationModel(request).get(); + return response.getAuthorizationModelId(); + } + + /** Get the name of the test that invokes this function. Returned in the form: "$class.$fn" */ + private String thisTestName() { + // Tracing the stack gives an array of: + // 0: getStackTrace(), 1: getThisFunctionName(), 2: , 3: ... + StackTraceElement callingFn = Thread.currentThread().getStackTrace()[2]; + String callingClass = callingFn.getClassName().replace("dev.openfga.sdk.api.client.", ""); + + return String.format("%s.%s", callingClass, callingFn.getMethodName()); + } +} diff --git a/src/test-integration/java/package-info.java b/src/test-integration/java/package-info.java new file mode 100644 index 0000000..3cca732 --- /dev/null +++ b/src/test-integration/java/package-info.java @@ -0,0 +1,9 @@ +/** + * This is an autogenerated Java SDK for OpenFGA. + * It provides a wrapper around the OpenFGA API definition. + *

+ * Most interaction should be centered around the high-level {@link dev.openfga.sdk.api.client.OpenFgaClient}. + * + * @see OpenFGA Docs + * @see Source + */ diff --git a/src/test/java/dev/openfga/sdk/api/auth/OAuth2ClientTest.java b/src/test/java/dev/openfga/sdk/api/auth/OAuth2ClientTest.java index a311150..8819c66 100644 --- a/src/test/java/dev/openfga/sdk/api/auth/OAuth2ClientTest.java +++ b/src/test/java/dev/openfga/sdk/api/auth/OAuth2ClientTest.java @@ -22,20 +22,21 @@ class OAuth2ClientTest { private final ObjectMapper mapper = new ObjectMapper(); private HttpClientMock mockHttpClient; - private Credentials credentials; private OAuth2Client oAuth2; @BeforeEach public void setup() throws FgaInvalidParameterException { mockHttpClient = new HttpClientMock(); - credentials = new Credentials(new ClientCredentials() + var credentials = new Credentials(new ClientCredentials() .clientId(CLIENT_ID) .clientSecret(CLIENT_SECRET) .apiAudience(AUDIENCE) .apiTokenIssuer(API_TOKEN_ISSUER)); - oAuth2 = new OAuth2Client(new Configuration("", credentials), mockHttpClient, mapper); + var configuration = new Configuration().apiUrl("").credentials(credentials); + + oAuth2 = new OAuth2Client(configuration, mockHttpClient, mapper); } @Test diff --git a/src/test/java/dev/openfga/sdk/api/client/OpenFgaClientTest.java b/src/test/java/dev/openfga/sdk/api/client/OpenFgaClientTest.java new file mode 100644 index 0000000..296ffc2 --- /dev/null +++ b/src/test/java/dev/openfga/sdk/api/client/OpenFgaClientTest.java @@ -0,0 +1,1794 @@ +/* + * 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: 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; + +import static org.hamcrest.Matchers.*; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.pgssoft.httpclient.HttpClientMock; +import dev.openfga.sdk.api.configuration.*; +import dev.openfga.sdk.api.model.*; +import dev.openfga.sdk.errors.*; +import java.time.Duration; +import java.util.List; +import java.util.concurrent.ExecutionException; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +/** + * API tests for OpenFgaClient + */ +public class OpenFgaClientTest { + private static final String DEFAULT_STORE_ID = "01YCP46JKYM8FJCQ37NMBYHE5X"; + private static final String DEFAULT_STORE_NAME = "test_store"; + private static final String DEFAULT_AUTH_MODEL_ID = "01G5JAVJ41T49E9TT3SKVS7X1J"; + private static final String DEFAULT_USER = "user:81684243-9356-4421-8fbf-a4f8d36aa31b"; + private static final String DEFAULT_RELATION = "reader"; + private static final String DEFAULT_TYPE = "document"; + private static final String DEFAULT_OBJECT = "document:budget"; + private static final String DEFAULT_SCHEMA_VERSION = "1.1"; + public static final String EMPTY_RESPONSE_BODY = "{}"; + + private OpenFgaClient fga; + private ClientConfiguration clientConfiguration; + private HttpClientMock mockHttpClient; + + @BeforeEach + public void beforeEachTest() throws Exception { + mockHttpClient = new HttpClientMock(); + + clientConfiguration = new ClientConfiguration() + .storeId(DEFAULT_STORE_ID) + .authorizationModelId(DEFAULT_AUTH_MODEL_ID) + .apiUrl("https://localhost") + .credentials(new Credentials()) + .readTimeout(Duration.ofMillis(250)); + + ApiClient mockApiClient = mock(ApiClient.class); + when(mockApiClient.getHttpClient()).thenReturn(mockHttpClient); + when(mockApiClient.getObjectMapper()).thenReturn(new ObjectMapper()); + + fga = new OpenFgaClient(mockApiClient, clientConfiguration); + } + + /* ****************** + * Credential tests * + ********************/ + @Test + public void createStore_withApiToken() throws Exception { + // Given + String apiToken = "some-static-token"; + clientConfiguration.credentials(new Credentials(new ApiToken(apiToken))); + fga.setConfiguration(clientConfiguration); + String expectedBody = String.format("{\"name\":\"%s\"}", DEFAULT_STORE_NAME); + String requestBody = String.format("{\"id\":\"%s\",\"name\":\"%s\"}", DEFAULT_STORE_ID, DEFAULT_STORE_NAME); + mockHttpClient + .onPost("https://localhost/stores") + .withBody(is(expectedBody)) + .withHeader("Authorization", String.format("Bearer %s", apiToken)) + .doReturn(201, requestBody); + CreateStoreRequest request = new CreateStoreRequest().name(DEFAULT_STORE_NAME); + + // When + CreateStoreResponse response = fga.createStore(request).get(); + + // Then + mockHttpClient + .verify() + .post("https://localhost/stores") + .withBody(is(expectedBody)) + .withHeader("Authorization", String.format("Bearer %s", apiToken)) + .called(1); + assertEquals(DEFAULT_STORE_ID, response.getId()); + assertEquals(DEFAULT_STORE_NAME, response.getName()); + } + + @Test + public void createStore_withClientCredentials() throws Exception { + // Given + String apiTokenIssuer = "oauth2.server"; + String clientId = "some-client-id"; + String clientSecret = "some-client-secret"; + String apiToken = "some-generated-token"; + String apiAudience = "some-audience"; + clientConfiguration.credentials(new Credentials(new ClientCredentials() + .clientId(clientId) + .clientSecret(clientSecret) + .apiTokenIssuer(apiTokenIssuer) + .apiAudience(apiAudience))); + fga.setConfiguration(clientConfiguration); + + String expectedOAuth2Body = String.format( + "{\"client_id\":\"%s\",\"client_secret\":\"%s\",\"audience\":\"%s\",\"grant_type\":\"client_credentials\"}", + clientId, clientSecret, apiAudience); + String expectedBody = String.format("{\"name\":\"%s\"}", DEFAULT_STORE_NAME); + String requestBody = String.format("{\"id\":\"%s\",\"name\":\"%s\"}", DEFAULT_STORE_ID, DEFAULT_STORE_NAME); + mockHttpClient + .onPost(String.format("https://%s/oauth/token", apiTokenIssuer)) + .withBody(is(expectedOAuth2Body)) + .doReturn(200, String.format("{\"access_token\":\"%s\"}", apiToken)); + mockHttpClient + .onPost("https://localhost/stores") + .withBody(is(expectedBody)) + .withHeader("Authorization", String.format("Bearer %s", apiToken)) + .doReturn(201, requestBody); + CreateStoreRequest request = new CreateStoreRequest().name(DEFAULT_STORE_NAME); + + // When + // We call two times to ensure the token is cached after the first request. + CreateStoreResponse response1 = fga.createStore(request).get(); + CreateStoreResponse response2 = fga.createStore(request).get(); + + // Then + // OAuth2 server should be called 1 time. + mockHttpClient + .verify() + .post(String.format("https://%s/oauth/token", apiTokenIssuer)) + .called(1); + // OpenFGA server should be called 2 times. + mockHttpClient + .verify() + .post("https://localhost/stores") + .withBody(is(expectedBody)) + .withHeader("Authorization", String.format("Bearer %s", apiToken)) + .called(2); + assertEquals(DEFAULT_STORE_ID, response1.getId()); + assertEquals(DEFAULT_STORE_NAME, response1.getName()); + assertEquals(DEFAULT_STORE_ID, response2.getId()); + assertEquals(DEFAULT_STORE_NAME, response2.getName()); + } + + /** + * List all stores. + */ + @Test + public void listStoresTest() throws Exception { + // Given + String responseBody = + String.format("{\"stores\":[{\"id\":\"%s\",\"name\":\"%s\"}]}", DEFAULT_STORE_ID, DEFAULT_STORE_NAME); + mockHttpClient.onGet("https://localhost/stores").doReturn(200, responseBody); + + // When + ListStoresResponse response = fga.listStores().get(); + + // Then + mockHttpClient.verify().get("https://localhost/stores").called(1); + assertNotNull(response.getStores()); + assertEquals(1, response.getStores().size()); + assertEquals(DEFAULT_STORE_ID, response.getStores().get(0).getId()); + assertEquals(DEFAULT_STORE_NAME, response.getStores().get(0).getName()); + } + + @Test + public void listStoresTest_withOptions() throws Exception { + // Given + String responseBody = + String.format("{\"stores\":[{\"id\":\"%s\",\"name\":\"%s\"}]}", DEFAULT_STORE_ID, DEFAULT_STORE_NAME); + int pageSize = 10; + String continuationToken = "continuationToken"; + String getUrl = String.format( + "https://localhost/stores?page_size=%d&continuation_token=%s", pageSize, continuationToken); + mockHttpClient.onGet(getUrl).doReturn(200, responseBody); + ListStoresOptions options = new ListStoresOptions().pageSize(pageSize).continuationToken(continuationToken); + + // When + ListStoresResponse response = fga.listStores(options).get(); + + // Then + mockHttpClient.verify().get(getUrl).called(1); + assertNotNull(response.getStores()); + assertEquals(1, response.getStores().size()); + assertEquals(DEFAULT_STORE_ID, response.getStores().get(0).getId()); + assertEquals(DEFAULT_STORE_NAME, response.getStores().get(0).getName()); + } + + /** + * Create a store. + */ + @Test + public void createStoreTest() throws Exception { + // Given + String expectedBody = String.format("{\"name\":\"%s\"}", DEFAULT_STORE_NAME); + String requestBody = String.format("{\"id\":\"%s\",\"name\":\"%s\"}", DEFAULT_STORE_ID, DEFAULT_STORE_NAME); + mockHttpClient + .onPost("https://localhost/stores") + .withBody(is(expectedBody)) + .doReturn(201, requestBody); + CreateStoreRequest request = new CreateStoreRequest().name(DEFAULT_STORE_NAME); + + // When + CreateStoreResponse response = fga.createStore(request).get(); + + // Then + mockHttpClient + .verify() + .post("https://localhost/stores") + .withBody(is(expectedBody)) + .called(1); + assertEquals(DEFAULT_STORE_ID, response.getId()); + assertEquals(DEFAULT_STORE_NAME, response.getName()); + } + + @Test + public void createStore_bodyRequired() { + // When + ExecutionException execException = assertThrows( + ExecutionException.class, () -> fga.createStore(null).get()); + + // Then + ApiException exception = assertInstanceOf(ApiException.class, execException.getCause()); + assertEquals("Missing the required parameter 'body' when calling createStore", exception.getMessage()); + } + + @Test + public void createStore_400() throws Exception { + // Given + mockHttpClient + .onPost("https://localhost/stores") + .doReturn(400, "{\"code\":\"validation_error\",\"message\":\"Generic validation error\"}"); + + // When + ExecutionException execException = + assertThrows(ExecutionException.class, () -> fga.createStore(new CreateStoreRequest()) + .get()); + + // Then + mockHttpClient.verify().post("https://localhost/stores").called(1); + ApiException exception = assertInstanceOf(ApiException.class, execException.getCause()); + assertEquals(400, exception.getCode()); + assertEquals( + "{\"code\":\"validation_error\",\"message\":\"Generic validation error\"}", + exception.getResponseBody()); + } + + @Test + public void createStore_404() throws Exception { + // Given + mockHttpClient + .onPost("https://localhost/stores") + .doReturn(404, "{\"code\":\"undefined_endpoint\",\"message\":\"Endpoint not enabled\"}"); + + // When + ExecutionException execException = + assertThrows(ExecutionException.class, () -> fga.createStore(new CreateStoreRequest()) + .get()); + + // Then + mockHttpClient.verify().post("https://localhost/stores").called(1); + ApiException exception = assertInstanceOf(ApiException.class, execException.getCause()); + assertEquals(404, exception.getCode()); + assertEquals( + "{\"code\":\"undefined_endpoint\",\"message\":\"Endpoint not enabled\"}", exception.getResponseBody()); + } + + @Test + public void createStore_500() throws Exception { + // Given + mockHttpClient + .onPost("https://localhost/stores") + .doReturn(500, "{\"code\":\"internal_error\",\"message\":\"Internal Server Error\"}"); + + // When + ExecutionException execException = + assertThrows(ExecutionException.class, () -> fga.createStore(new CreateStoreRequest()) + .get()); + + // Then + mockHttpClient.verify().post("https://localhost/stores").called(1); + ApiException exception = assertInstanceOf(ApiException.class, execException.getCause()); + assertEquals(500, exception.getCode()); + assertEquals( + "{\"code\":\"internal_error\",\"message\":\"Internal Server Error\"}", exception.getResponseBody()); + } + + /** + * Get a store. + */ + @Test + public void getStoreTest() throws Exception { + // Given + String getUrl = String.format("https://localhost/stores/%s", DEFAULT_STORE_ID); + String responseBody = String.format("{\"id\":\"%s\",\"name\":\"%s\"}", DEFAULT_STORE_ID, DEFAULT_STORE_NAME); + mockHttpClient.onGet(getUrl).doReturn(200, responseBody); + + // When + GetStoreResponse response = fga.getStore().get(); + + // Then + mockHttpClient.verify().get(getUrl).called(1); + assertEquals(DEFAULT_STORE_ID, response.getId()); + assertEquals(DEFAULT_STORE_NAME, response.getName()); + } + + @Test + public void getStore_storeIdRequired() { + // Given + clientConfiguration.storeId(null); + + // When + var exception = assertThrows( + FgaInvalidParameterException.class, () -> fga.getStore().get()); + + // Then + assertEquals( + "Required parameter storeId was invalid when calling ClientConfiguration.", exception.getMessage()); + } + + @Test + public void getStore_400() throws Exception { + // Given + String getUrl = String.format("https://localhost/stores/%s", DEFAULT_STORE_ID); + mockHttpClient + .onGet(getUrl) + .doReturn(400, "{\"code\":\"validation_error\",\"message\":\"Generic validation error\"}"); + + // When + ExecutionException execException = + assertThrows(ExecutionException.class, () -> fga.getStore().get()); + + // Then + mockHttpClient.verify().get(getUrl).called(1); + ApiException exception = assertInstanceOf(ApiException.class, execException.getCause()); + assertEquals(400, exception.getCode()); + assertEquals( + "{\"code\":\"validation_error\",\"message\":\"Generic validation error\"}", + exception.getResponseBody()); + } + + @Test + public void getStore_404() throws Exception { + // Given + String getUrl = String.format("https://localhost/stores/%s", DEFAULT_STORE_ID); + mockHttpClient + .onGet(getUrl) + .doReturn(404, "{\"code\":\"undefined_endpoint\",\"message\":\"Endpoint not enabled\"}"); + + // When + ExecutionException execException = + assertThrows(ExecutionException.class, () -> fga.getStore().get()); + + // Then + mockHttpClient.verify().get(getUrl).called(1); + ApiException exception = assertInstanceOf(ApiException.class, execException.getCause()); + assertEquals(404, exception.getCode()); + assertEquals( + "{\"code\":\"undefined_endpoint\",\"message\":\"Endpoint not enabled\"}", exception.getResponseBody()); + } + + @Test + public void getStore_500() throws Exception { + // Given + String getUrl = String.format("https://localhost/stores/%s", DEFAULT_STORE_ID); + mockHttpClient + .onGet(getUrl) + .doReturn(500, "{\"code\":\"internal_error\",\"message\":\"Internal Server Error\"}"); + + // When + ExecutionException execException = + assertThrows(ExecutionException.class, () -> fga.getStore().get()); + + // Then + mockHttpClient.verify().get(getUrl).called(1); + ApiException exception = assertInstanceOf(ApiException.class, execException.getCause()); + assertEquals(500, exception.getCode()); + assertEquals( + "{\"code\":\"internal_error\",\"message\":\"Internal Server Error\"}", exception.getResponseBody()); + } + + /** + * Delete a store. + */ + @Test + public void deleteStoreTest() throws Exception { + // Given + String deleteUrl = String.format("https://localhost/stores/%s", DEFAULT_STORE_ID); + mockHttpClient.onDelete(deleteUrl).doReturn(204, EMPTY_RESPONSE_BODY); + + // When + fga.deleteStore().get(); + + // Then + mockHttpClient.verify().delete(deleteUrl).called(1); + } + + @Test + public void deleteStore_storeIdRequired() { + // Given + clientConfiguration.storeId(null); + + // When + var exception = assertThrows( + FgaInvalidParameterException.class, () -> fga.deleteStore().get()); + + // Then + assertEquals( + "Required parameter storeId was invalid when calling ClientConfiguration.", exception.getMessage()); + } + + @Test + public void deleteStore_400() { + // Given + String deleteUrl = String.format("https://localhost/stores/%s", DEFAULT_STORE_ID); + mockHttpClient + .onDelete(deleteUrl) + .doReturn(400, "{\"code\":\"validation_error\",\"message\":\"Generic validation error\"}"); + + // When + ExecutionException execException = + assertThrows(ExecutionException.class, () -> fga.deleteStore().get()); + + // Then + mockHttpClient.verify().delete(deleteUrl).called(1); + ApiException exception = assertInstanceOf(ApiException.class, execException.getCause()); + assertEquals(400, exception.getCode()); + assertEquals( + "{\"code\":\"validation_error\",\"message\":\"Generic validation error\"}", + exception.getResponseBody()); + } + + @Test + public void deleteStore_404() { + // Given + String deleteUrl = String.format("https://localhost/stores/%s", DEFAULT_STORE_ID); + mockHttpClient + .onDelete(deleteUrl) + .doReturn(404, "{\"code\":\"undefined_endpoint\",\"message\":\"Endpoint not enabled\"}"); + + // When + ExecutionException execException = + assertThrows(ExecutionException.class, () -> fga.deleteStore().get()); + + // Then + mockHttpClient.verify().delete(deleteUrl).called(1); + ApiException exception = assertInstanceOf(ApiException.class, execException.getCause()); + assertEquals(404, exception.getCode()); + assertEquals( + "{\"code\":\"undefined_endpoint\",\"message\":\"Endpoint not enabled\"}", exception.getResponseBody()); + } + + @Test + public void deleteStore_500() { + // Given + String deleteUrl = String.format("https://localhost/stores/%s", DEFAULT_STORE_ID); + mockHttpClient + .onDelete(deleteUrl) + .doReturn(500, "{\"code\":\"internal_error\",\"message\":\"Internal Server Error\"}"); + + // When + ExecutionException execException = + assertThrows(ExecutionException.class, () -> fga.deleteStore().get()); + + // Then + mockHttpClient.verify().delete(deleteUrl).called(1); + ApiException exception = assertInstanceOf(ApiException.class, execException.getCause()); + assertEquals(500, exception.getCode()); + assertEquals( + "{\"code\":\"internal_error\",\"message\":\"Internal Server Error\"}", exception.getResponseBody()); + } + + /** + * Return all the authorization models for a particular store. + */ + @Test + public void readAuthorizationModelsTest() throws Exception { + // Given + String getUrl = String.format("https://localhost/stores/%s/authorization-models", DEFAULT_STORE_ID); + var options = new ReadAuthorizationModelsOptions(); + String responseBody = String.format( + "{\"authorization_models\":[{\"id\":\"%s\",\"schema_version\":\"%s\"}]}", + DEFAULT_AUTH_MODEL_ID, DEFAULT_SCHEMA_VERSION); + mockHttpClient.onGet(getUrl).doReturn(200, responseBody); + + // When + ReadAuthorizationModelsResponse response = + fga.readAuthorizationModels(options).get(); + + // Then + mockHttpClient.verify().get(getUrl).called(1); + assertNotNull(response.getAuthorizationModels()); + assertEquals(1, response.getAuthorizationModels().size()); + AuthorizationModel authModel = response.getAuthorizationModels().get(0); + assertEquals(DEFAULT_AUTH_MODEL_ID, authModel.getId()); + assertEquals(DEFAULT_SCHEMA_VERSION, authModel.getSchemaVersion()); + } + + @Test + public void readAuthorizationModels_storeIdRequired() { + // Given + clientConfiguration.storeId(null); + var options = new ReadAuthorizationModelsOptions(); + + // When + var exception = assertThrows(FgaInvalidParameterException.class, () -> fga.readAuthorizationModels(options) + .get()); + + // Then + assertEquals( + "Required parameter storeId was invalid when calling ClientConfiguration.", exception.getMessage()); + } + + @Test + public void readAuthorizationModels_400() { + // Given + String getUrl = String.format("https://localhost/stores/%s/authorization-models", DEFAULT_STORE_ID); + var options = new ReadAuthorizationModelsOptions(); + mockHttpClient + .onGet(getUrl) + .doReturn(400, "{\"code\":\"validation_error\",\"message\":\"Generic validation error\"}"); + + // When + ExecutionException execException = + assertThrows(ExecutionException.class, () -> fga.readAuthorizationModels(options) + .get()); + + // Then + mockHttpClient.verify().get(getUrl).called(1); + ApiException exception = assertInstanceOf(ApiException.class, execException.getCause()); + assertEquals(400, exception.getCode()); + assertEquals( + "{\"code\":\"validation_error\",\"message\":\"Generic validation error\"}", + exception.getResponseBody()); + } + + @Test + public void readAuthorizationModels_404() { + // Given + String getUrl = String.format("https://localhost/stores/%s/authorization-models", DEFAULT_STORE_ID); + var options = new ReadAuthorizationModelsOptions(); + mockHttpClient + .onGet(getUrl) + .doReturn(404, "{\"code\":\"undefined_endpoint\",\"message\":\"Endpoint not enabled\"}"); + + // When + ExecutionException execException = + assertThrows(ExecutionException.class, () -> fga.readAuthorizationModels(options) + .get()); + + // Then + mockHttpClient.verify().get(getUrl).called(1); + ApiException exception = assertInstanceOf(ApiException.class, execException.getCause()); + assertEquals(404, exception.getCode()); + assertEquals( + "{\"code\":\"undefined_endpoint\",\"message\":\"Endpoint not enabled\"}", exception.getResponseBody()); + } + + @Test + public void readAuthorizationModels_500() throws Exception { + // Given + String getUrl = String.format("https://localhost/stores/%s/authorization-models", DEFAULT_STORE_ID); + var options = new ReadAuthorizationModelsOptions(); + mockHttpClient + .onGet(getUrl) + .doReturn(500, "{\"code\":\"internal_error\",\"message\":\"Internal Server Error\"}"); + + // When + ExecutionException execException = + assertThrows(ExecutionException.class, () -> fga.readAuthorizationModels(options) + .get()); + + // Then + mockHttpClient.verify().get(getUrl).called(1); + ApiException exception = assertInstanceOf(ApiException.class, execException.getCause()); + assertEquals(500, exception.getCode()); + assertEquals( + "{\"code\":\"internal_error\",\"message\":\"Internal Server Error\"}", exception.getResponseBody()); + } + + @Test + public void readLatestAuthorizationModelTest() throws Exception { + // Given + String getUrl = String.format("https://localhost/stores/%s/authorization-models?page_size=1", DEFAULT_STORE_ID); + String responseBody = String.format( + "{\"authorization_models\":[{\"id\":\"%s\",\"schema_version\":\"%s\"}]}", + DEFAULT_AUTH_MODEL_ID, DEFAULT_SCHEMA_VERSION); + mockHttpClient.onGet(getUrl).doReturn(200, responseBody); + + // When + ReadAuthorizationModelResponse response = + fga.readLatestAuthorizationModel().get(); + + // Then + mockHttpClient.verify().get(getUrl).called(1); + assertNotNull(response.getAuthorizationModel()); + AuthorizationModel authModel = response.getAuthorizationModel(); + assertEquals(DEFAULT_AUTH_MODEL_ID, authModel.getId()); + assertEquals(DEFAULT_SCHEMA_VERSION, authModel.getSchemaVersion()); + } + + @Test + public void readChanges() throws Exception { + // Given + String changeType = "repo"; + String user = "user:81684243-9356-4421-8fbf-a4f8d36aa31b"; + String relation = "viewer"; + String object = "document:roadmap"; + String continuationToken = + "eyJwayI6IkxBVEVTVF9OU0NPTkZJR19hdXRoMHN0b3JlIiwic2siOiIxem1qbXF3MWZLZExTcUoyN01MdTdqTjh0cWgifQ"; + + ReadChangesOptions options = new ReadChangesOptions().type(changeType); + String getUrl = String.format("https://localhost/stores/%s/changes?type=%s", DEFAULT_STORE_ID, changeType); + String responseBody = String.format( + "{\"changes\":[{\"tuple_key\":{\"user\":\"%s\",\"relation\":\"%s\",\"object\":\"%s\"},\"operation\":\"TUPLE_OPERATION_WRITE\"}],\"continuation_token\":\"%s\"}", + user, relation, object, continuationToken); + mockHttpClient.onGet(getUrl).doReturn(200, responseBody); + + // When + ReadChangesResponse response = fga.readChanges(options).get(); + + // Then + mockHttpClient.verify().get(getUrl).called(1); + assertEquals(continuationToken, response.getContinuationToken()); + assertNotNull(response.getChanges()); + assertEquals(1, response.getChanges().size()); + TupleChange change = response.getChanges().get(0); + assertEquals(TupleOperation.WRITE, change.getOperation()); + TupleKey tupleKey = change.getTupleKey(); + assertNotNull(tupleKey); + assertEquals(user, tupleKey.getUser()); + assertEquals(relation, tupleKey.getRelation()); + assertEquals(object, tupleKey.getObject()); + } + + /** + * Create a new authorization model. + */ + @Test + public void writeAuthorizationModelTest() throws Exception { + // Given + String postUrl = String.format("https://localhost/stores/%s/authorization-models", DEFAULT_STORE_ID); + String expectedBody = + "{\"type_definitions\":[{\"type\":\"document\",\"relations\":{},\"metadata\":null}],\"schema_version\":\"1.1\"}"; + String responseBody = String.format("{\"authorization_model_id\":\"%s\"}", DEFAULT_AUTH_MODEL_ID); + mockHttpClient.onPost(postUrl).withBody(is(expectedBody)).doReturn(201, responseBody); + WriteAuthorizationModelRequest request = new WriteAuthorizationModelRequest() + .schemaVersion(DEFAULT_SCHEMA_VERSION) + .typeDefinitions(List.of(new TypeDefinition().type(DEFAULT_TYPE))); + + // When + WriteAuthorizationModelResponse response = + fga.writeAuthorizationModel(request).get(); + + // Then + mockHttpClient.verify().post(postUrl).withBody(is(expectedBody)).called(1); + assertEquals(DEFAULT_AUTH_MODEL_ID, response.getAuthorizationModelId()); + } + + @Test + public void writeAuthorizationModel_storeIdRequired() { + // Given + clientConfiguration.storeId(null); + + // When + var exception = assertThrows(FgaInvalidParameterException.class, () -> fga.writeAuthorizationModel( + new WriteAuthorizationModelRequest()) + .get()); + + // Then + assertEquals( + "Required parameter storeId was invalid when calling ClientConfiguration.", exception.getMessage()); + } + + @Test + public void writeAuthorizationModel_bodyRequired() { + // When + ExecutionException execException = + assertThrows(ExecutionException.class, () -> fga.writeAuthorizationModel(null) + .get()); + + // Then + ApiException exception = assertInstanceOf(ApiException.class, execException.getCause()); + assertEquals( + "Missing the required parameter 'body' when calling writeAuthorizationModel", exception.getMessage()); + } + + @Test + public void writeAuthorizationModel_400() throws Exception { + // Given + String postUrl = String.format("https://localhost/stores/%s/authorization-models", DEFAULT_STORE_ID); + mockHttpClient + .onPost(postUrl) + .doReturn(400, "{\"code\":\"validation_error\",\"message\":\"Generic validation error\"}"); + + // When + ExecutionException execException = assertThrows( + ExecutionException.class, () -> fga.writeAuthorizationModel(new WriteAuthorizationModelRequest()) + .get()); + + // Then + mockHttpClient.verify().post(postUrl).called(1); + ApiException exception = assertInstanceOf(ApiException.class, execException.getCause()); + assertEquals(400, exception.getCode()); + assertEquals( + "{\"code\":\"validation_error\",\"message\":\"Generic validation error\"}", + exception.getResponseBody()); + } + + @Test + public void writeAuthorizationModel_404() { + // Given + String postUrl = String.format("https://localhost/stores/%s/authorization-models", DEFAULT_STORE_ID); + mockHttpClient + .onPost(postUrl) + .doReturn(404, "{\"code\":\"undefined_endpoint\",\"message\":\"Endpoint not enabled\"}"); + + // When + ExecutionException execException = assertThrows( + ExecutionException.class, () -> fga.writeAuthorizationModel(new WriteAuthorizationModelRequest()) + .get()); + + // Then + mockHttpClient.verify().post(postUrl).called(1); + ApiException exception = assertInstanceOf(ApiException.class, execException.getCause()); + assertEquals(404, exception.getCode()); + assertEquals( + "{\"code\":\"undefined_endpoint\",\"message\":\"Endpoint not enabled\"}", exception.getResponseBody()); + } + + @Test + public void writeAuthorizationModel_500() { + // Given + String postUrl = String.format("https://localhost/stores/%s/authorization-models", DEFAULT_STORE_ID); + mockHttpClient + .onPost(postUrl) + .doReturn(500, "{\"code\":\"internal_error\",\"message\":\"Internal Server Error\"}"); + + // When + ExecutionException execException = assertThrows( + ExecutionException.class, () -> fga.writeAuthorizationModel(new WriteAuthorizationModelRequest()) + .get()); + + // Then + mockHttpClient.verify().post(postUrl).called(1); + ApiException exception = assertInstanceOf(ApiException.class, execException.getCause()); + assertEquals(500, exception.getCode()); + assertEquals( + "{\"code\":\"internal_error\",\"message\":\"Internal Server Error\"}", exception.getResponseBody()); + } + + /** + * Return a particular version of an authorization model. + */ + @Test + public void readAuthorizationModelTest() throws Exception { + // Given + String getUrl = String.format( + "https://localhost/stores/%s/authorization-models/%s", DEFAULT_STORE_ID, DEFAULT_AUTH_MODEL_ID); + String getResponse = String.format( + "{\"authorization_model\":{\"id\":\"%s\",\"schema_version\":\"%s\"}}", + DEFAULT_AUTH_MODEL_ID, DEFAULT_SCHEMA_VERSION); + mockHttpClient.onGet(getUrl).doReturn(200, getResponse); + + // When + ReadAuthorizationModelResponse response = fga.readAuthorizationModel().get(); + + // Then + mockHttpClient.verify().get(getUrl).called(1); + assertNotNull(response.getAuthorizationModel()); + assertEquals(DEFAULT_AUTH_MODEL_ID, response.getAuthorizationModel().getId()); + assertEquals(DEFAULT_SCHEMA_VERSION, response.getAuthorizationModel().getSchemaVersion()); + } + + @Test + public void readAuthorizationModelTest_withOptions() throws Exception { + // Given + String authorizationModelId = "alternateAuthorizationModelId"; + ReadAuthorizationModelOptions options = + new ReadAuthorizationModelOptions().authorizationModelId(authorizationModelId); + String getUrl = String.format( + "https://localhost/stores/%s/authorization-models/%s", DEFAULT_STORE_ID, authorizationModelId); + String getResponse = String.format( + "{\"authorization_model\":{\"id\":\"%s\",\"schema_version\":\"%s\"}}", + authorizationModelId, DEFAULT_SCHEMA_VERSION); + mockHttpClient.onGet(getUrl).doReturn(200, getResponse); + + // When + ReadAuthorizationModelResponse response = + fga.readAuthorizationModel(options).get(); + + // Then + mockHttpClient.verify().get(getUrl).called(1); + assertNotNull(response.getAuthorizationModel()); + assertEquals(authorizationModelId, response.getAuthorizationModel().getId()); + assertEquals(DEFAULT_SCHEMA_VERSION, response.getAuthorizationModel().getSchemaVersion()); + } + + @Test + public void readAuthorizationModel_storeIdRequired() { + // Given + clientConfiguration.storeId(null); + + // When + var exception = assertThrows(FgaInvalidParameterException.class, () -> fga.readAuthorizationModel() + .get()); + + // Then + assertEquals( + "Required parameter storeId was invalid when calling ClientConfiguration.", exception.getMessage()); + } + + @Test + public void readAuthorizationModel_idRequired() { + // Given + clientConfiguration.authorizationModelId(null); + + // When + var exception = assertThrows(FgaInvalidParameterException.class, () -> fga.readAuthorizationModel() + .get()); + + // Then + assertEquals( + "Required parameter authorizationModelId was invalid when calling ClientConfiguration.", + exception.getMessage()); + } + + @Test + public void readAuthorizationModel_400() { + // Given + String getUrl = String.format( + "https://localhost/stores/%s/authorization-models/%s", DEFAULT_STORE_ID, DEFAULT_AUTH_MODEL_ID); + mockHttpClient + .onGet(getUrl) + .doReturn(400, "{\"code\":\"validation_error\",\"message\":\"Generic validation error\"}"); + + // When + ExecutionException execException = assertThrows( + ExecutionException.class, () -> fga.readAuthorizationModel().get()); + + // Then + mockHttpClient.verify().get(getUrl).called(1); + ApiException exception = assertInstanceOf(ApiException.class, execException.getCause()); + assertEquals(400, exception.getCode()); + assertEquals( + "{\"code\":\"validation_error\",\"message\":\"Generic validation error\"}", + exception.getResponseBody()); + } + + @Test + public void readAuthorizationModel_404() throws Exception { + // Given + String getUrl = String.format( + "https://localhost/stores/%s/authorization-models/%s", DEFAULT_STORE_ID, DEFAULT_AUTH_MODEL_ID); + mockHttpClient + .onGet(getUrl) + .doReturn(404, "{\"code\":\"undefined_endpoint\",\"message\":\"Endpoint not enabled\"}"); + + // When + ExecutionException execException = assertThrows( + ExecutionException.class, () -> fga.readAuthorizationModel().get()); + + // Then + mockHttpClient.verify().get(getUrl).called(1); + ApiException exception = assertInstanceOf(ApiException.class, execException.getCause()); + assertEquals(404, exception.getCode()); + assertEquals( + "{\"code\":\"undefined_endpoint\",\"message\":\"Endpoint not enabled\"}", exception.getResponseBody()); + } + + @Test + public void readAuthorizationModel_500() throws Exception { + // Given + String getUrl = String.format( + "https://localhost/stores/%s/authorization-models/%s", DEFAULT_STORE_ID, DEFAULT_AUTH_MODEL_ID); + mockHttpClient + .onGet(getUrl) + .doReturn(500, "{\"code\":\"internal_error\",\"message\":\"Internal Server Error\"}"); + + // When + ExecutionException execException = assertThrows( + ExecutionException.class, () -> fga.readAuthorizationModel().get()); + + // Then + mockHttpClient.verify().get(getUrl).called(1); + ApiException exception = assertInstanceOf(ApiException.class, execException.getCause()); + assertEquals(500, exception.getCode()); + assertEquals( + "{\"code\":\"internal_error\",\"message\":\"Internal Server Error\"}", exception.getResponseBody()); + } + + /** + * Get tuples from the store that matches a query, without following userset rewrite rules. + */ + @Test + public void readTest() throws Exception { + // Given + String postUrl = String.format("https://localhost/stores/%s/read", DEFAULT_STORE_ID); + String expectedBody = String.format( + "{\"tuple_key\":{\"object\":\"%s\",\"relation\":\"%s\",\"user\":\"%s\"},\"page_size\":null,\"continuation_token\":null}", + DEFAULT_OBJECT, DEFAULT_RELATION, DEFAULT_USER); + String responseBody = String.format( + "{\"tuples\":[{\"key\":{\"user\":\"%s\",\"relation\":\"%s\",\"object\":\"%s\"}}]}", + DEFAULT_USER, DEFAULT_RELATION, DEFAULT_OBJECT); + mockHttpClient.onPost(postUrl).withBody(is(expectedBody)).doReturn(200, responseBody); + ClientReadRequest request = new ClientReadRequest() + .user(DEFAULT_USER) + .relation(DEFAULT_RELATION) + ._object(DEFAULT_OBJECT); + + // When + ReadResponse response = fga.read(request).get(); + + // Then + mockHttpClient.verify().post(postUrl).withBody(is(expectedBody)).called(1); + assertNotNull(response.getTuples()); + assertEquals(1, response.getTuples().size()); + TupleKey key = response.getTuples().get(0).getKey(); + assertNotNull(key); + assertEquals(DEFAULT_USER, key.getUser()); + assertEquals(DEFAULT_RELATION, key.getRelation()); + assertEquals(DEFAULT_OBJECT, key.getObject()); + } + + @Test + public void read_storeIdRequired() { + // Given + clientConfiguration.storeId(null); + + // When + var exception = assertThrows(FgaInvalidParameterException.class, () -> fga.read(new ClientReadRequest()) + .get()); + + // Then + assertEquals( + "Required parameter storeId was invalid when calling ClientConfiguration.", exception.getMessage()); + } + + @Test + public void read_400() { + // Given + String postUrl = String.format("https://localhost/stores/%s/read", DEFAULT_STORE_ID); + mockHttpClient + .onPost(postUrl) + .doReturn(400, "{\"code\":\"validation_error\",\"message\":\"Generic validation error\"}"); + + // When + ExecutionException execException = + assertThrows(ExecutionException.class, () -> fga.read(new ClientReadRequest()) + .get()); + + // Then + mockHttpClient.verify().post(postUrl).called(1); + ApiException exception = assertInstanceOf(ApiException.class, execException.getCause()); + assertEquals(400, exception.getCode()); + assertEquals( + "{\"code\":\"validation_error\",\"message\":\"Generic validation error\"}", + exception.getResponseBody()); + } + + @Test + public void read_404() { + // Given + String postUrl = String.format("https://localhost/stores/%s/read", DEFAULT_STORE_ID); + mockHttpClient + .onPost(postUrl) + .doReturn(404, "{\"code\":\"undefined_endpoint\",\"message\":\"Endpoint not enabled\"}"); + + // When + ExecutionException execException = + assertThrows(ExecutionException.class, () -> fga.read(new ClientReadRequest()) + .get()); + + // Then + mockHttpClient.verify().post(postUrl).called(1); + ApiException exception = assertInstanceOf(ApiException.class, execException.getCause()); + assertEquals(404, exception.getCode()); + assertEquals( + "{\"code\":\"undefined_endpoint\",\"message\":\"Endpoint not enabled\"}", exception.getResponseBody()); + } + + @Test + public void read_500() { + // Given + String postUrl = String.format("https://localhost/stores/%s/read", DEFAULT_STORE_ID); + mockHttpClient + .onPost(postUrl) + .doReturn(500, "{\"code\":\"internal_error\",\"message\":\"Internal Server Error\"}"); + + // When + ExecutionException execException = + assertThrows(ExecutionException.class, () -> fga.read(new ClientReadRequest()) + .get()); + + // Then + mockHttpClient.verify().post(postUrl).called(1); + ApiException exception = assertInstanceOf(ApiException.class, execException.getCause()); + assertEquals(500, exception.getCode()); + assertEquals( + "{\"code\":\"internal_error\",\"message\":\"Internal Server Error\"}", exception.getResponseBody()); + } + + /** + * Add or delete tuples from the store. + */ + @Test + public void writeTest_writes() throws Exception { + // Given + String postPath = "https://localhost/stores/01YCP46JKYM8FJCQ37NMBYHE5X/write"; + String expectedBody = String.format( + "{\"writes\":{\"tuple_keys\":[{\"object\":\"%s\",\"relation\":\"%s\",\"user\":\"%s\"}]},\"deletes\":null,\"authorization_model_id\":\"%s\"}", + DEFAULT_OBJECT, DEFAULT_RELATION, DEFAULT_USER, DEFAULT_AUTH_MODEL_ID); + mockHttpClient.onPost(postPath).withBody(is(expectedBody)).doReturn(200, EMPTY_RESPONSE_BODY); + ClientWriteRequest request = new ClientWriteRequest() + .writes(List.of(new ClientTupleKey() + ._object(DEFAULT_OBJECT) + .relation(DEFAULT_RELATION) + .user(DEFAULT_USER))); + + // When + fga.write(request); + + // Then + mockHttpClient.verify().post(postPath).withBody(is(expectedBody)).called(1); + } + + /** + * Add or delete tuples from the store. + */ + @Test + public void writeTest_deletes() throws Exception { + // Given + String postPath = "https://localhost/stores/01YCP46JKYM8FJCQ37NMBYHE5X/write"; + String expectedBody = String.format( + "{\"writes\":null,\"deletes\":{\"tuple_keys\":[{\"object\":\"%s\",\"relation\":\"%s\",\"user\":\"%s\"}]},\"authorization_model_id\":\"%s\"}", + DEFAULT_OBJECT, DEFAULT_RELATION, DEFAULT_USER, DEFAULT_AUTH_MODEL_ID); + mockHttpClient.onPost(postPath).withBody(is(expectedBody)).doReturn(200, EMPTY_RESPONSE_BODY); + ClientWriteRequest request = new ClientWriteRequest() + .deletes(List.of(new ClientTupleKey() + ._object(DEFAULT_OBJECT) + .relation(DEFAULT_RELATION) + .user(DEFAULT_USER))); + + // When + fga.write(request); + + // Then + mockHttpClient.verify().post(postPath).withBody(is(expectedBody)).called(1); + } + + @Test + public void writeTuplesTest() throws Exception { + // Given + String postPath = String.format("https://localhost/stores/%s/write", DEFAULT_STORE_ID); + String expectedBody = String.format( + "{\"writes\":{\"tuple_keys\":[{\"object\":\"%s\",\"relation\":\"%s\",\"user\":\"%s\"}]},\"deletes\":null,\"authorization_model_id\":\"%s\"}", + DEFAULT_OBJECT, DEFAULT_RELATION, DEFAULT_USER, DEFAULT_AUTH_MODEL_ID); + mockHttpClient.onPost(postPath).withBody(is(expectedBody)).doReturn(200, EMPTY_RESPONSE_BODY); + List tuples = List.of(new ClientTupleKey() + ._object(DEFAULT_OBJECT) + .relation(DEFAULT_RELATION) + .user(DEFAULT_USER)); + + // When + fga.writeTuples(tuples); + + // Then + mockHttpClient.verify().post(postPath).withBody(is(expectedBody)).called(1); + } + + @Test + public void deleteTuplesTest() throws Exception { + // Given + String postPath = String.format("https://localhost/stores/%s/write", DEFAULT_STORE_ID); + String expectedBody = String.format( + "{\"writes\":null,\"deletes\":{\"tuple_keys\":[{\"object\":\"%s\",\"relation\":\"%s\",\"user\":\"%s\"}]},\"authorization_model_id\":\"%s\"}", + DEFAULT_OBJECT, DEFAULT_RELATION, DEFAULT_USER, DEFAULT_AUTH_MODEL_ID); + mockHttpClient.onPost(postPath).withBody(is(expectedBody)).doReturn(200, EMPTY_RESPONSE_BODY); + List tuples = List.of(new ClientTupleKey() + ._object(DEFAULT_OBJECT) + .relation(DEFAULT_RELATION) + .user(DEFAULT_USER)); + + // When + fga.deleteTuples(tuples); + + // Then + mockHttpClient.verify().post(postPath).withBody(is(expectedBody)).called(1); + } + + @Test + public void write_storeIdRequired() { + // Given + clientConfiguration.storeId(null); + + // When + var exception = assertThrows(FgaInvalidParameterException.class, () -> fga.write(new ClientWriteRequest()) + .get()); + + // Then + assertEquals( + "Required parameter storeId was invalid when calling ClientConfiguration.", exception.getMessage()); + } + + @Test + public void write_400() throws Exception { + // Given + String postUrl = "https://localhost/stores/01YCP46JKYM8FJCQ37NMBYHE5X/write"; + mockHttpClient + .onPost(postUrl) + .doReturn(400, "{\"code\":\"validation_error\",\"message\":\"Generic validation error\"}"); + + // When + ExecutionException execException = + assertThrows(ExecutionException.class, () -> fga.write(new ClientWriteRequest()) + .get()); + + // Then + mockHttpClient.verify().post(postUrl).called(1); + ApiException exception = assertInstanceOf(ApiException.class, execException.getCause()); + assertEquals(400, exception.getCode()); + assertEquals( + "{\"code\":\"validation_error\",\"message\":\"Generic validation error\"}", + exception.getResponseBody()); + } + + @Test + public void write_404() throws Exception { + // Given + String postUrl = "https://localhost/stores/01YCP46JKYM8FJCQ37NMBYHE5X/write"; + mockHttpClient + .onPost(postUrl) + .doReturn(404, "{\"code\":\"undefined_endpoint\",\"message\":\"Endpoint not enabled\"}"); + + // When + ExecutionException execException = + assertThrows(ExecutionException.class, () -> fga.write(new ClientWriteRequest()) + .get()); + + // Then + mockHttpClient.verify().post(postUrl).called(1); + ApiException exception = assertInstanceOf(ApiException.class, execException.getCause()); + assertEquals(404, exception.getCode()); + assertEquals( + "{\"code\":\"undefined_endpoint\",\"message\":\"Endpoint not enabled\"}", exception.getResponseBody()); + } + + @Test + public void write_500() throws Exception { + // Given + String postUrl = "https://localhost/stores/01YCP46JKYM8FJCQ37NMBYHE5X/write"; + mockHttpClient + .onPost(postUrl) + .doReturn(500, "{\"code\":\"internal_error\",\"message\":\"Internal Server Error\"}"); + + // When + ExecutionException execException = + assertThrows(ExecutionException.class, () -> fga.write(new ClientWriteRequest()) + .get()); + + // Then + mockHttpClient.verify().post(postUrl).called(1); + ApiException exception = assertInstanceOf(ApiException.class, execException.getCause()); + assertEquals(500, exception.getCode()); + assertEquals( + "{\"code\":\"internal_error\",\"message\":\"Internal Server Error\"}", exception.getResponseBody()); + } + + /** + * Check whether a user is authorized to access an object. + */ + @Test + public void check() 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}"); + ClientCheckRequest request = new ClientCheckRequest() + ._object(DEFAULT_OBJECT) + .relation(DEFAULT_RELATION) + .user(DEFAULT_USER); + ClientCheckOptions options = new ClientCheckOptions().authorizationModelId(DEFAULT_AUTH_MODEL_ID); + + // When + CheckResponse response = fga.check(request, options).get(); + + // Then + mockHttpClient.verify().post(postUrl).withBody(is(expectedBody)).called(1); + assertEquals(Boolean.TRUE, response.getAllowed()); + } + + @Test + public void check_storeIdRequired() { + // Given + clientConfiguration.storeId(null); + + // When + var exception = assertThrows(FgaInvalidParameterException.class, () -> fga.check(new ClientCheckRequest()) + .get()); + + // Then + assertEquals( + "Required parameter storeId was invalid when calling ClientConfiguration.", exception.getMessage()); + } + + @Test + public void check_400() throws Exception { + // Given + String postUrl = String.format("https://localhost/stores/%s/check", DEFAULT_STORE_ID); + mockHttpClient + .onPost(postUrl) + .doReturn(400, "{\"code\":\"validation_error\",\"message\":\"Generic validation error\"}"); + + // When + ExecutionException execException = + assertThrows(ExecutionException.class, () -> fga.check(new ClientCheckRequest()) + .get()); + + // Then + mockHttpClient.verify().post(postUrl).called(1); + ApiException exception = assertInstanceOf(ApiException.class, execException.getCause()); + assertEquals(400, exception.getCode()); + assertEquals( + "{\"code\":\"validation_error\",\"message\":\"Generic validation error\"}", + exception.getResponseBody()); + } + + @Test + public void check_404() throws Exception { + // Given + String postUrl = String.format("https://localhost/stores/%s/check", DEFAULT_STORE_ID); + mockHttpClient + .onPost(postUrl) + .doReturn(404, "{\"code\":\"undefined_endpoint\",\"message\":\"Endpoint not enabled\"}"); + + // When + ExecutionException execException = + assertThrows(ExecutionException.class, () -> fga.check(new ClientCheckRequest()) + .get()); + + // Then + mockHttpClient.verify().post(postUrl).called(1); + ApiException exception = assertInstanceOf(ApiException.class, execException.getCause()); + assertEquals(404, exception.getCode()); + assertEquals( + "{\"code\":\"undefined_endpoint\",\"message\":\"Endpoint not enabled\"}", exception.getResponseBody()); + } + + @Test + public void check_500() throws Exception { + // Given + String postUrl = String.format("https://localhost/stores/%s/check", DEFAULT_STORE_ID); + mockHttpClient + .onPost(postUrl) + .doReturn(500, "{\"code\":\"internal_error\",\"message\":\"Internal Server Error\"}"); + + // When + ExecutionException execException = + assertThrows(ExecutionException.class, () -> fga.check(new ClientCheckRequest()) + .get()); + + // Then + mockHttpClient.verify().post(postUrl).called(1); + ApiException exception = assertInstanceOf(ApiException.class, execException.getCause()); + assertEquals(500, exception.getCode()); + assertEquals( + "{\"code\":\"internal_error\",\"message\":\"Internal Server Error\"}", exception.getResponseBody()); + } + + /** + * Expand all relationships in userset tree format, and following userset rewrite rules. Useful to reason + * about and debug a certain relationship. + */ + @Test + public void expandTest() throws Exception { + // Given + String postPath = "https://localhost/stores/01YCP46JKYM8FJCQ37NMBYHE5X/expand"; + String expectedBody = String.format( + "{\"tuple_key\":{\"object\":\"%s\",\"relation\":\"%s\",\"user\":\"%s\"},\"authorization_model_id\":\"%s\"}", + DEFAULT_OBJECT, DEFAULT_RELATION, DEFAULT_USER, DEFAULT_AUTH_MODEL_ID); + String responseBody = String.format( + "{\"tree\":{\"root\":{\"union\":{\"nodes\":[{\"leaf\":{\"users\":{\"users\":[\"%s\"]}}}]}}}}", + DEFAULT_USER); + mockHttpClient.onPost(postPath).withBody(is(expectedBody)).doReturn(200, responseBody); + ClientExpandRequest request = new ClientExpandRequest() + .user(DEFAULT_USER) + .relation(DEFAULT_RELATION) + ._object(DEFAULT_OBJECT); + ClientExpandOptions options = new ClientExpandOptions().authorizationModelId(DEFAULT_AUTH_MODEL_ID); + + // When + ExpandResponse response = fga.expand(request, options).get(); + + // Then + mockHttpClient.verify().post(postPath).withBody(is(expectedBody)).called(1); + assertNotNull(response.getTree()); + assertNotNull(response.getTree().getRoot()); + assertNotNull(response.getTree().getRoot().getUnion()); + assertNotNull(response.getTree().getRoot().getUnion().getNodes()); + assertEquals(1, response.getTree().getRoot().getUnion().getNodes().size()); + assertNotNull(response.getTree().getRoot().getUnion().getNodes().get(0)); + Node node = response.getTree().getRoot().getUnion().getNodes().get(0); + assertNotNull(node.getLeaf()); + assertNotNull(node.getLeaf().getUsers()); + assertNotNull(node.getLeaf().getUsers().getUsers()); + assertEquals(1, node.getLeaf().getUsers().getUsers().size()); + assertEquals(DEFAULT_USER, node.getLeaf().getUsers().getUsers().get(0)); + } + + @Test + public void expand_storeIdRequired() { + // Given + clientConfiguration.storeId(null); + + // When + var exception = assertThrows(FgaInvalidParameterException.class, () -> fga.expand(new ClientExpandRequest()) + .get()); + + // Then + assertEquals( + "Required parameter storeId was invalid when calling ClientConfiguration.", exception.getMessage()); + } + + @Test + public void expand_400() throws Exception { + // Given + String postUrl = "https://localhost/stores/01YCP46JKYM8FJCQ37NMBYHE5X/expand"; + mockHttpClient + .onPost(postUrl) + .doReturn(400, "{\"code\":\"validation_error\",\"message\":\"Generic validation error\"}"); + + // When + ExecutionException execException = + assertThrows(ExecutionException.class, () -> fga.expand(new ClientExpandRequest()) + .get()); + + // Then + mockHttpClient.verify().post(postUrl).called(1); + ApiException exception = assertInstanceOf(ApiException.class, execException.getCause()); + assertEquals(400, exception.getCode()); + assertEquals( + "{\"code\":\"validation_error\",\"message\":\"Generic validation error\"}", + exception.getResponseBody()); + } + + @Test + public void expand_404() throws Exception { + // Given + String postUrl = "https://localhost/stores/01YCP46JKYM8FJCQ37NMBYHE5X/expand"; + mockHttpClient + .onPost(postUrl) + .doReturn(404, "{\"code\":\"undefined_endpoint\",\"message\":\"Endpoint not enabled\"}"); + + // When + ExecutionException execException = + assertThrows(ExecutionException.class, () -> fga.expand(new ClientExpandRequest()) + .get()); + + // Then + mockHttpClient.verify().post(postUrl).called(1); + ApiException exception = assertInstanceOf(ApiException.class, execException.getCause()); + assertEquals(404, exception.getCode()); + assertEquals( + "{\"code\":\"undefined_endpoint\",\"message\":\"Endpoint not enabled\"}", exception.getResponseBody()); + } + + @Test + public void expand_500() throws Exception { + // Given + String postUrl = "https://localhost/stores/01YCP46JKYM8FJCQ37NMBYHE5X/expand"; + mockHttpClient + .onPost(postUrl) + .doReturn(500, "{\"code\":\"internal_error\",\"message\":\"Internal Server Error\"}"); + + // When + ExecutionException execException = + assertThrows(ExecutionException.class, () -> fga.expand(new ClientExpandRequest()) + .get()); + + // Then + mockHttpClient.verify().post(postUrl).called(1); + ApiException exception = assertInstanceOf(ApiException.class, execException.getCause()); + assertEquals(500, exception.getCode()); + assertEquals( + "{\"code\":\"internal_error\",\"message\":\"Internal Server Error\"}", exception.getResponseBody()); + } + + /** + * List all objects of the given type that the user has a relation with. + */ + @Test + public void listObjectsTest() throws Exception { + // Given + String postPath = String.format("https://localhost/stores/%s/list-objects", DEFAULT_STORE_ID); + String expectedBody = String.format( + "{\"authorization_model_id\":\"%s\",\"type\":null,\"relation\":\"%s\",\"user\":\"%s\",\"contextual_tuples\":{\"tuple_keys\":[]}}", + DEFAULT_AUTH_MODEL_ID, DEFAULT_RELATION, DEFAULT_USER); + mockHttpClient + .onPost(postPath) + .withBody(is(expectedBody)) + .doReturn(200, String.format("{\"objects\":[\"%s\"]}", DEFAULT_OBJECT)); + ClientListObjectsRequest request = + new ClientListObjectsRequest().relation(DEFAULT_RELATION).user(DEFAULT_USER); + + // When + ListObjectsResponse response = fga.listObjects(request).get(); + + // Then + mockHttpClient.verify().post(postPath).withBody(is(expectedBody)).called(1); + assertEquals(List.of(DEFAULT_OBJECT), response.getObjects()); + } + + @Test + public void listObjects_storeIdRequired() { + // Given + clientConfiguration.storeId(null); + + // When + var exception = + assertThrows(FgaInvalidParameterException.class, () -> fga.listObjects(new ClientListObjectsRequest()) + .get()); + + // Then + assertEquals( + "Required parameter storeId was invalid when calling ClientConfiguration.", exception.getMessage()); + } + + @Test + public void listObjects_400() throws Exception { + // Given + String postUrl = String.format("https://localhost/stores/%s/list-objects", DEFAULT_STORE_ID); + mockHttpClient + .onPost(postUrl) + .doReturn(400, "{\"code\":\"validation_error\",\"message\":\"Generic validation error\"}"); + + // When + ExecutionException execException = + assertThrows(ExecutionException.class, () -> fga.listObjects(new ClientListObjectsRequest()) + .get()); + + // Then + mockHttpClient.verify().post(postUrl).called(1); + ApiException exception = assertInstanceOf(ApiException.class, execException.getCause()); + assertEquals(400, exception.getCode()); + assertEquals( + "{\"code\":\"validation_error\",\"message\":\"Generic validation error\"}", + exception.getResponseBody()); + } + + @Test + public void listObjects_404() throws Exception { + // Given + String postUrl = String.format("https://localhost/stores/%s/list-objects", DEFAULT_STORE_ID); + mockHttpClient + .onPost(postUrl) + .doReturn(404, "{\"code\":\"undefined_endpoint\",\"message\":\"Endpoint not enabled\"}"); + + // When + ExecutionException execException = + assertThrows(ExecutionException.class, () -> fga.listObjects(new ClientListObjectsRequest()) + .get()); + + // Then + mockHttpClient.verify().post(postUrl).called(1); + ApiException exception = assertInstanceOf(ApiException.class, execException.getCause()); + assertEquals(404, exception.getCode()); + assertEquals( + "{\"code\":\"undefined_endpoint\",\"message\":\"Endpoint not enabled\"}", exception.getResponseBody()); + } + + @Test + public void listObjects_500() throws Exception { + // Given + String postUrl = String.format("https://localhost/stores/%s/list-objects", DEFAULT_STORE_ID); + mockHttpClient + .onPost(postUrl) + .doReturn(500, "{\"code\":\"internal_error\",\"message\":\"Internal Server Error\"}"); + + // When + ExecutionException execException = + assertThrows(ExecutionException.class, () -> fga.listObjects(new ClientListObjectsRequest()) + .get()); + + // Then + mockHttpClient.verify().post(postUrl).called(1); + ApiException exception = assertInstanceOf(ApiException.class, execException.getCause()); + assertEquals(500, exception.getCode()); + assertEquals( + "{\"code\":\"internal_error\",\"message\":\"Internal Server Error\"}", exception.getResponseBody()); + } + + /** + * Read assertions for an authorization model ID. + */ + @Test + public void readAssertionsTest() throws Exception { + // Given + String getUrl = + String.format("https://localhost/stores/%s/assertions/%s", DEFAULT_STORE_ID, DEFAULT_AUTH_MODEL_ID); + String responseBody = String.format( + "{\"assertions\":[{\"tuple_key\":{\"object\":\"%s\",\"relation\":\"%s\",\"user\":\"%s\"},\"expectation\":true}]}", + DEFAULT_OBJECT, DEFAULT_RELATION, DEFAULT_USER); + mockHttpClient.onGet(getUrl).doReturn(200, responseBody); + + // When + ReadAssertionsResponse response = fga.readAssertions().get(); + + // Then + mockHttpClient.verify().get(getUrl).called(1); + assertNotNull(response.getAssertions()); + assertEquals(1, response.getAssertions().size()); + Assertion assertion = response.getAssertions().get(0); + assertNotNull(assertion); + assertTrue(assertion.getExpectation()); + assertEquals(DEFAULT_OBJECT, assertion.getTupleKey().getObject()); + assertEquals(DEFAULT_RELATION, assertion.getTupleKey().getRelation()); + assertEquals(DEFAULT_USER, assertion.getTupleKey().getUser()); + } + + @Test + public void readAssertions_storeIdRequired() { + // Given + clientConfiguration.storeId(null); + + // When + var exception = assertThrows( + FgaInvalidParameterException.class, () -> fga.readAssertions().get()); + + // Then + assertEquals( + "Required parameter storeId was invalid when calling ClientConfiguration.", exception.getMessage()); + } + + @Test + public void readAssertions_authModelIdRequired() { + // Given + clientConfiguration.authorizationModelId(null); + + // When + var exception = assertThrows( + FgaInvalidParameterException.class, () -> fga.readAssertions().get()); + + // Then + assertEquals( + "Required parameter authorizationModelId was invalid when calling ClientConfiguration.", + exception.getMessage()); + } + + @Test + public void readAssertions_400() throws Exception { + // Given + String getUrl = + String.format("https://localhost/stores/%s/assertions/%s", DEFAULT_STORE_ID, DEFAULT_AUTH_MODEL_ID); + mockHttpClient + .onGet(getUrl) + .doReturn(400, "{\"code\":\"validation_error\",\"message\":\"Generic validation error\"}"); + + // When + ExecutionException execException = assertThrows( + ExecutionException.class, () -> fga.readAssertions().get()); + + // Then + mockHttpClient.verify().get(getUrl).called(1); + ApiException exception = assertInstanceOf(ApiException.class, execException.getCause()); + assertEquals(400, exception.getCode()); + assertEquals( + "{\"code\":\"validation_error\",\"message\":\"Generic validation error\"}", + exception.getResponseBody()); + } + + @Test + public void readAssertions_404() throws Exception { + // Given + String getUrl = + String.format("https://localhost/stores/%s/assertions/%s", DEFAULT_STORE_ID, DEFAULT_AUTH_MODEL_ID); + mockHttpClient + .onGet(getUrl) + .doReturn(404, "{\"code\":\"undefined_endpoint\",\"message\":\"Endpoint not enabled\"}"); + + // When + ExecutionException execException = assertThrows( + ExecutionException.class, () -> fga.readAssertions().get()); + + // Then + mockHttpClient.verify().get(getUrl).called(1); + ApiException exception = assertInstanceOf(ApiException.class, execException.getCause()); + assertEquals(404, exception.getCode()); + assertEquals( + "{\"code\":\"undefined_endpoint\",\"message\":\"Endpoint not enabled\"}", exception.getResponseBody()); + } + + @Test + public void readAssertions_500() throws Exception { + // Given + String getUrl = + String.format("https://localhost/stores/%s/assertions/%s", DEFAULT_STORE_ID, DEFAULT_AUTH_MODEL_ID); + mockHttpClient + .onGet(getUrl) + .doReturn(500, "{\"code\":\"internal_error\",\"message\":\"Internal Server Error\"}"); + + // When + ExecutionException execException = assertThrows( + ExecutionException.class, () -> fga.readAssertions().get()); + + // Then + mockHttpClient.verify().get(getUrl).called(1); + ApiException exception = assertInstanceOf(ApiException.class, execException.getCause()); + assertEquals(500, exception.getCode()); + assertEquals( + "{\"code\":\"internal_error\",\"message\":\"Internal Server Error\"}", exception.getResponseBody()); + } + + /** + * Upsert assertions for an authorization model ID. + */ + @Test + public void writeAssertionsTest() throws Exception { + // Given + String putUrl = + String.format("https://localhost/stores/%s/assertions/%s", DEFAULT_STORE_ID, DEFAULT_AUTH_MODEL_ID); + String expectedBody = String.format( + "{\"assertions\":[{\"tuple_key\":{\"object\":\"%s\",\"relation\":\"%s\",\"user\":\"%s\"},\"expectation\":true}]}", + DEFAULT_OBJECT, DEFAULT_RELATION, DEFAULT_USER); + mockHttpClient.onPut(putUrl).withBody(is(expectedBody)).doReturn(200, EMPTY_RESPONSE_BODY); + List assertions = List.of(new ClientAssertion() + .user(DEFAULT_USER) + .relation(DEFAULT_RELATION) + ._object(DEFAULT_OBJECT) + .expectation(true)); + + // When + fga.writeAssertions(assertions).get(); + + // Then + mockHttpClient.verify().put(putUrl).withBody(is(expectedBody)).called(1); + } + + @Test + public void writeAssertions_storeIdRequired() { + // Given + clientConfiguration.storeId(null); + + // When + var exception = assertThrows(FgaInvalidParameterException.class, () -> fga.writeAssertions(List.of()) + .get()); + + // Then + assertEquals( + "Required parameter storeId was invalid when calling ClientConfiguration.", exception.getMessage()); + } + + @Test + public void writeAssertions_authModelIdRequired() { + // Given + clientConfiguration.authorizationModelId(null); + + // When + var exception = assertThrows(FgaInvalidParameterException.class, () -> fga.writeAssertions(List.of()) + .get()); + + // Then + assertEquals( + "Required parameter authorizationModelId was invalid when calling ClientConfiguration.", + exception.getMessage()); + } + + @Test + public void writeAssertions_400() throws Exception { + // Given + String putUrl = + String.format("https://localhost/stores/%s/assertions/%s", DEFAULT_STORE_ID, DEFAULT_AUTH_MODEL_ID); + mockHttpClient + .onPut(putUrl) + .doReturn(400, "{\"code\":\"validation_error\",\"message\":\"Generic validation error\"}"); + + // When + ExecutionException execException = assertThrows( + ExecutionException.class, () -> fga.writeAssertions(List.of()).get()); + + // Then + mockHttpClient.verify().put(putUrl).called(1); + ApiException exception = assertInstanceOf(ApiException.class, execException.getCause()); + assertEquals(400, exception.getCode()); + assertEquals( + "{\"code\":\"validation_error\",\"message\":\"Generic validation error\"}", + exception.getResponseBody()); + } + + @Test + public void writeAssertions_404() throws Exception { + // Given + String putUrl = + String.format("https://localhost/stores/%s/assertions/%s", DEFAULT_STORE_ID, DEFAULT_AUTH_MODEL_ID); + mockHttpClient + .onPut(putUrl) + .doReturn(404, "{\"code\":\"undefined_endpoint\",\"message\":\"Endpoint not enabled\"}"); + + // When + ExecutionException execException = assertThrows( + ExecutionException.class, () -> fga.writeAssertions(List.of()).get()); + + // Then + mockHttpClient.verify().put(putUrl).called(1); + ApiException exception = assertInstanceOf(ApiException.class, execException.getCause()); + assertEquals(404, exception.getCode()); + assertEquals( + "{\"code\":\"undefined_endpoint\",\"message\":\"Endpoint not enabled\"}", exception.getResponseBody()); + } + + @Test + public void writeAssertions_500() throws Exception { + // Given + String putUrl = + String.format("https://localhost/stores/%s/assertions/%s", DEFAULT_STORE_ID, DEFAULT_AUTH_MODEL_ID); + mockHttpClient + .onPut(putUrl) + .doReturn(500, "{\"code\":\"internal_error\",\"message\":\"Internal Server Error\"}"); + + // When + ExecutionException execException = assertThrows( + ExecutionException.class, () -> fga.writeAssertions(List.of()).get()); + + // Then + mockHttpClient.verify().put(putUrl).called(1); + ApiException exception = assertInstanceOf(ApiException.class, execException.getCause()); + assertEquals(500, exception.getCode()); + assertEquals( + "{\"code\":\"internal_error\",\"message\":\"Internal Server Error\"}", exception.getResponseBody()); + } + + /** + * Miscellaneous client behavior tests. + */ + @Test + public void setStoreId() throws Exception { + // Given + String alternateStoreId = "A_UNIQUE_ID"; + fga.setStoreId(alternateStoreId); + String getUrl = String.format("https://localhost/stores/%s", alternateStoreId); + String responseBody = String.format("{\"id\":\"%s\",\"name\":\"%s\"}", alternateStoreId, DEFAULT_STORE_NAME); + mockHttpClient.onGet(getUrl).doReturn(200, responseBody); + + // When + GetStoreResponse response = fga.getStore().get(); + + // Then + mockHttpClient.verify().get(getUrl).called(1); + assertEquals(alternateStoreId, response.getId()); + assertEquals(DEFAULT_STORE_NAME, response.getName()); + assertEquals( + alternateStoreId, + clientConfiguration.getStoreId(), + "OpenFgaClient.setStoreId(String) is expected to persist its Store ID in its ClientConfiguration." + + "If this behavior ever changes, it could be a subtle breaking change."); + } + + @Test + public void setAuthorizationModelId() throws Exception { + // Given + String alternateAuthorizationModelId = "A_UNIQUE_ID"; + fga.setAuthorizationModelId(alternateAuthorizationModelId); + String getUrl = String.format( + "https://localhost/stores/%s/authorization-models/%s", DEFAULT_STORE_ID, alternateAuthorizationModelId); + String getResponse = String.format( + "{\"authorization_model\":{\"id\":\"%s\",\"schema_version\":\"%s\"}}", + alternateAuthorizationModelId, DEFAULT_SCHEMA_VERSION); + mockHttpClient.onGet(getUrl).doReturn(200, getResponse); + + // When + ReadAuthorizationModelResponse response = fga.readAuthorizationModel().get(); + + // Then + mockHttpClient.verify().get(getUrl).called(1); + assertNotNull(response.getAuthorizationModel()); + assertEquals( + alternateAuthorizationModelId, response.getAuthorizationModel().getId()); + assertEquals(DEFAULT_SCHEMA_VERSION, response.getAuthorizationModel().getSchemaVersion()); + assertEquals( + alternateAuthorizationModelId, + clientConfiguration.getAuthorizationModelId(), + "OpenFgaClient.setAuthorizationModelId(String) is expected to persist its Authorization Model ID in its ClientConfiguration." + + "If this behavior ever changes, it could be a subtle breaking change."); + } +} diff --git a/src/test/java/dev/openfga/sdk/api/configuration/ConfigurationTest.java b/src/test/java/dev/openfga/sdk/api/configuration/ConfigurationTest.java index 40ba83a..5f4f8f1 100644 --- a/src/test/java/dev/openfga/sdk/api/configuration/ConfigurationTest.java +++ b/src/test/java/dev/openfga/sdk/api/configuration/ConfigurationTest.java @@ -20,7 +20,7 @@ class ConfigurationTest { private static final String DEFAULT_API_URL = "http://localhost:8080"; - private static final String DEFAULT_USER_AGENT = "openfga-sdk java/0.0.4"; + private static final String DEFAULT_USER_AGENT = "openfga-sdk java/0.0.5"; private static final Duration DEFAULT_READ_TIMEOUT = Duration.ofSeconds(10); private static final Duration DEFAULT_CONNECT_TIMEOUT = Duration.ofSeconds(10); @@ -28,7 +28,7 @@ class ConfigurationTest { void apiUrl_nullDefaults() throws FgaInvalidParameterException { // Given String apiUrl = null; - var config = new Configuration(apiUrl); + var config = new Configuration().apiUrl(apiUrl); // When config.assertValid(); @@ -41,7 +41,7 @@ void apiUrl_nullDefaults() throws FgaInvalidParameterException { void apiUrl_emptyStringDefaults() throws FgaInvalidParameterException { // Given String apiUrl = ""; - var config = new Configuration(apiUrl); + var config = new Configuration().apiUrl(apiUrl); // When config.assertValid(); @@ -54,7 +54,7 @@ void apiUrl_emptyStringDefaults() throws FgaInvalidParameterException { void apiUrl_whitespaceStringDefaults() throws FgaInvalidParameterException { // Given String apiUrl = " \t\r\n"; - var config = new Configuration(apiUrl); + var config = new Configuration().apiUrl(apiUrl); // When config.assertValid(); @@ -70,7 +70,7 @@ void apiUrl_stringNoProtocolFails() { // When FgaInvalidParameterException e = assertThrows(FgaInvalidParameterException.class, () -> { - var config = new Configuration(apiUrl); + var config = new Configuration().apiUrl(apiUrl); config.assertValid(); }); @@ -85,7 +85,7 @@ void apiUrl_stringInvalidProtocolFails() { // When FgaInvalidParameterException e = assertThrows(FgaInvalidParameterException.class, () -> { - var config = new Configuration(apiUrl); + var config = new Configuration().apiUrl(apiUrl); config.assertValid(); }); @@ -100,7 +100,7 @@ void apiUrl_stringNoHostFails() { // When FgaInvalidParameterException e = assertThrows(FgaInvalidParameterException.class, () -> { - var config = new Configuration(apiUrl); + var config = new Configuration().apiUrl(apiUrl); config.assertValid(); }); @@ -115,7 +115,7 @@ void apiUrl_stringBadPortFails() { // When FgaInvalidParameterException e = assertThrows(FgaInvalidParameterException.class, () -> { - var config = new Configuration(apiUrl); + var config = new Configuration().apiUrl(apiUrl); config.assertValid(); });