diff --git a/.gitignore b/.gitignore
index 9b21127..e1db716 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,13 +1,184 @@
-# Ignore Gradle project-specific cache directory
+# Created by https://www.toptal.com/developers/gitignore/api/java,gradle,cmake,intellij+all,visualstudiocode
+# Edit at https://www.toptal.com/developers/gitignore?templates=java,gradle,cmake,intellij+all,visualstudiocode
+
+### CMake ###
+CMakeLists.txt.user
+CMakeCache.txt
+CMakeFiles
+CMakeScripts
+Testing
+Makefile
+cmake_install.cmake
+install_manifest.txt
+compile_commands.json
+CTestTestfile.cmake
+_deps
+
+### CMake Patch ###
+CMakeUserPresets.json
+
+# External projects
+*-prefix/
+
+### Intellij+all ###
+# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
+# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
+
+# User-specific stuff
+.idea/**/workspace.xml
+.idea/**/tasks.xml
+.idea/**/usage.statistics.xml
+.idea/**/dictionaries
+.idea/**/shelf
+
+# AWS User-specific
+.idea/**/aws.xml
+
+# Generated files
+.idea/**/contentModel.xml
+
+# Sensitive or high-churn files
+.idea/**/dataSources/
+.idea/**/dataSources.ids
+.idea/**/dataSources.local.xml
+.idea/**/sqlDataSources.xml
+.idea/**/dynamic.xml
+.idea/**/uiDesigner.xml
+.idea/**/dbnavigator.xml
+
+# Gradle
+.idea/**/gradle.xml
+.idea/**/libraries
+
+# Gradle and Maven with auto-import
+# When using Gradle or Maven with auto-import, you should exclude module files,
+# since they will be recreated, and may cause churn. Uncomment if using
+# auto-import.
+# .idea/artifacts
+# .idea/compiler.xml
+# .idea/jarRepositories.xml
+# .idea/modules.xml
+# .idea/*.iml
+# .idea/modules
+# *.iml
+# *.ipr
+
+# CMake
+cmake-build-*/
+
+# Mongo Explorer plugin
+.idea/**/mongoSettings.xml
+
+# File-based project format
+*.iws
+
+# IntelliJ
+out/
+
+# mpeltonen/sbt-idea plugin
+.idea_modules/
+
+# JIRA plugin
+atlassian-ide-plugin.xml
+
+# Cursive Clojure plugin
+.idea/replstate.xml
+
+# SonarLint plugin
+.idea/sonarlint/
+
+# Crashlytics plugin (for Android Studio and IntelliJ)
+com_crashlytics_export_strings.xml
+crashlytics.properties
+crashlytics-build.properties
+fabric.properties
+
+# Editor-based Rest Client
+.idea/httpRequests
+
+# Android studio 3.1+ serialized cache file
+.idea/caches/build_file_checksums.ser
+
+### Intellij+all Patch ###
+# Ignore everything but code style settings and run configurations
+# that are supposed to be shared within teams.
+
+.idea/*
+
+!.idea/codeStyles
+!.idea/runConfigurations
+
+### Java ###
+# Compiled class file
+*.class
+
+# Log file
+*.log
+
+# BlueJ files
+*.ctxt
+
+# Mobile Tools for Java (J2ME)
+.mtj.tmp/
+
+# Package Files #
+*.jar
+*.war
+*.nar
+*.ear
+*.zip
+*.tar.gz
+*.rar
+
+# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
+hs_err_pid*
+replay_pid*
+
+### VisualStudioCode ###
+.vscode/*
+!.vscode/settings.json
+!.vscode/tasks.json
+!.vscode/launch.json
+!.vscode/extensions.json
+!.vscode/*.code-snippets
+
+# Local History for Visual Studio Code
+.history/
+
+# Built Visual Studio Code Extensions
+*.vsix
+
+### VisualStudioCode Patch ###
+# Ignore all local history of files
+.history
+.ionide
+
+### Gradle ###
.gradle
+**/build/
+!src/**/build/
+
+# Ignore Gradle GUI config
+gradle-app.setting
+
+# Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored)
+!gradle-wrapper.jar
+
+# Avoid ignore Gradle wrappper properties
+!gradle-wrapper.properties
+
+# Cache of project
+.gradletasknamecache
-# ignore code editor stuff
-.idea
-.vscode
+# Eclipse Gradle plugin generated files
+# Eclipse Core
+.project
+# JDT-specific (Eclipse Java Development Tools)
+.classpath
-# Ignore Gradle build output directory
-build
+### Gradle Patch ###
+# Java heap dump
+*.hprof
-# Ignore stg schemas
-stg-schemas/
-bin
+# End of https://www.toptal.com/developers/gitignore/api/java,gradle,cmake,intellij+all,visualstudiocode
+/generated/
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
new file mode 100644
index 0000000..8b47be4
--- /dev/null
+++ b/CONTRIBUTING.md
@@ -0,0 +1,249 @@
+# Contributing to Permit.io Java SDK
+
+Thank you for your interest in contributing to the Permit.io Java SDK! This document provides guidelines and
+instructions for contributing to the project.
+
+## Table of Contents
+
+- [Getting Started](#getting-started)
+- [Prerequisites](#prerequisites)
+- [Development Workflow](#development-workflow)
+- [Regenerating OpenAPI Models](#regenerating-openapi-models)
+- [Project Structure](#project-structure)
+- [Code Style](#code-style)
+- [Submitting Changes](#submitting-changes)
+- [Reporting Issues](#reporting-issues)
+- [Community](#community)
+
+## Getting Started
+
+1. **Fork the repository** on GitHub
+2. **Clone your fork** locally:
+ ```bash
+ git clone https://github.com/YOUR_USERNAME/permit-java.git
+ cd permit-java
+ ```
+3. **Add the upstream remote**:
+ ```bash
+ git remote add upstream https://github.com/permitio/permit-java.git
+ ```
+
+## Prerequisites
+
+Before contributing, ensure you have the following installed:
+
+- **Java JDK 8+**: For building and running the SDK
+- **Java JDK 11+**: Required for running the OpenAPI Generator (the SDK itself targets Java 8)
+- **Gradle**: Version 7.0+ (or use the included Gradle wrapper `./gradlew`)
+- **OpenAPI Generator**: Required for regenerating API models from the Permit.io OpenAPI specification
+
+### Installing OpenAPI Generator
+
+The OpenAPI Generator is used to generate Java model classes from the Permit.io API specification. Install it using one
+of the following methods:
+
+**Using Homebrew (macOS):**
+
+```bash
+brew install openapi-generator
+```
+
+**Using npm:**
+
+```bash
+npm install @openapitools/openapi-generator-cli -g
+```
+
+**Using Docker:**
+
+```bash
+docker pull openapitools/openapi-generator-cli
+```
+
+For more installation options, see
+the [OpenAPI Generator Installation Guide](https://openapi-generator.tech/docs/installation).
+
+## Development Workflow
+
+1. **Create a feature branch** from `master`:
+ ```bash
+ git checkout -b feature/your-feature-name
+ ```
+
+2. **Make your changes** and ensure all tests pass:
+ ```bash
+ ./gradlew build
+ ./gradlew test
+ ```
+
+3. **Commit your changes** with clear, descriptive messages:
+ ```bash
+ git commit -m "Add feature: description of your changes"
+ ```
+
+4. **Push to your fork**:
+ ```bash
+ git push origin feature/your-feature-name
+ ```
+
+5. **Submit a pull request** with a clear description of your changes
+
+## Regenerating OpenAPI Models
+
+The SDK includes auto-generated model classes from the Permit.io OpenAPI specification. If you need to regenerate these
+models (e.g., when the API is updated), use the provided Makefile targets:
+
+### Generate OpenAPI Models
+
+```bash
+make generate-openapi
+```
+
+If your default Java is version 8, you need to use Java 11+ for the generator:
+
+```bash
+# macOS - use Java 11+ for the generator
+JAVA_HOME=$(/usr/libexec/java_home -v 17) make generate-openapi
+```
+
+This command:
+
+1. Fetches the latest OpenAPI specification from `https://api.permit.io/v2/openapi.json`
+2. Generates Java model classes using the configuration in `openapi-config.json`
+3. Outputs the generated code to the `generated/` directory
+
+Note: The `--skip-validate-spec` flag is used to bypass minor validation issues in the upstream API specification that
+do not affect code generation.
+
+### Copying Generated Files to the Project
+
+After running `make generate-openapi`, you need to **manually copy** the generated model files to the SDK source
+directory:
+
+```bash
+# Copy generated models to the SDK
+cp generated/src/main/java/io/permit/sdk/openapi/model/*.java \
+ src/main/java/io/permit/sdk/openapi/models/
+```
+
+**Important notes:**
+
+- The generator outputs to `generated/src/main/java/io/permit/sdk/openapi/model/` (singular "model")
+- The SDK uses `src/main/java/io/permit/sdk/openapi/models/` (plural "models")
+- Only copy the model files you need; not all generated files are used in the SDK
+- Review the changes before committing to ensure compatibility
+
+### Clean Generated Files
+
+```bash
+make clean-openapi
+```
+
+This removes the `generated/` directory and all auto-generated files.
+
+### Generate JSON Schema (Optional)
+
+```bash
+make generate-jsonschema
+```
+
+This generates JSON schema files from the OpenAPI specification and outputs them to the `schemas/` directory. This
+requires the `openapi2jsonschema` tool.
+
+## Project Structure
+
+The SDK source code is organized under `src/main/java/io/permit/sdk/`:
+
+```
+src/main/java/io/permit/sdk/
+|-- api/ # API client classes for Permit.io REST API
+| |-- models/ # SDK-specific models for API operations
+| | |-- CreateOrUpdateResult.java # Result wrapper for sync operations
+| | |-- UserModel.java # User data model
+| | |-- RoleModel.java # Role data model
+| | +-- ... # Other API models
+| |-- UsersApi.java # User management operations
+| |-- RolesApi.java # Role management operations
+| |-- TenantsApi.java # Tenant management operations
+| |-- ResourcesApi.java # Resource management operations
+| +-- ... # Other API clients
+|
+|-- enforcement/ # Enforcement models for permission checks
+| |-- User.java # User model for check() calls
+| |-- Resource.java # Resource model for check() calls
+| |-- Enforcer.java # Core enforcement logic
+| |-- CheckQuery.java # Permission check query builder
+| +-- ... # Other enforcement models
+|
+|-- examples/ # Example code demonstrating SDK usage
+| |-- BasicPermissionCheckExample.java
+| |-- AdvancedPermissionCheckExample.java
+| +-- UserSyncExample.java
+|
+|-- openapi/ # Auto-generated OpenAPI models
+| +-- models/ # Generated model classes from Permit.io API spec
+| # (copy generated files here from generated/src/main/java/...)
+|
+|-- util/ # Utility classes and helpers
+|
+|-- Permit.java # Main SDK entry point
+|-- PermitConfig.java # SDK configuration builder
+|-- PermitContext.java # Context management for API calls
+|-- ApiKeyLevel.java # API key permission levels
+|-- ApiContextLevel.java # API context level definitions
++-- FactsSyncTimeoutPolicy.java # Timeout policy for facts synchronization
+```
+
+## Code Style
+
+Please follow these guidelines when contributing:
+
+- Follow standard Java naming conventions
+- Use meaningful variable and method names
+- Include Javadoc comments for public APIs
+- Write unit tests for new functionality
+- Keep methods focused and concise
+- Avoid introducing new dependencies unless necessary
+
+## Submitting Changes
+
+### Pull Request Guidelines
+
+- **One feature per PR**: Keep pull requests focused on a single feature or fix
+- **Clear description**: Explain what your changes do and why
+- **Tests**: Include tests for new functionality
+- **Documentation**: Update documentation if needed
+- **Clean history**: Rebase your branch on `master` before submitting
+
+### Pull Request Template
+
+When submitting a PR, please include:
+
+1. **Summary**: Brief description of the changes
+2. **Motivation**: Why these changes are needed
+3. **Testing**: How the changes were tested
+4. **Breaking Changes**: Note any breaking changes (if applicable)
+
+## Reporting Issues
+
+When reporting issues, please include:
+
+- **Java version**: Output of `java -version`
+- **SDK version**: The version of the Permit.io SDK you're using
+- **Steps to reproduce**: Clear steps to reproduce the issue
+- **Expected behavior**: What you expected to happen
+- **Actual behavior**: What actually happened
+- **Stack trace**: If applicable, include the full stack trace
+
+Use the [GitHub Issues](https://github.com/permitio/permit-java/issues) page to report bugs or request features.
+
+## Community
+
+- **Documentation**: [Permit.io Docs](https://docs.permit.io)
+- **Slack**: [Permit.io Community](https://permit.io/slack)
+- **GitHub Discussions**: For questions and discussions about the SDK
+
+## License
+
+By contributing to this project, you agree that your contributions will be licensed under
+the [Apache License 2.0](LICENSE).
diff --git a/Makefile b/Makefile
index 144fa2c..05e8485 100644
--- a/Makefile
+++ b/Makefile
@@ -6,7 +6,7 @@
## generate openapi models
generate-openapi:
- openapi-generator generate -i https://api.permit.io/v2/openapi.json -g java -o generated/ -c openapi-config.json
+ openapi-generator generate -i https://api.permit.io/v2/openapi.json -g java -o generated/ -c openapi-config.json --skip-validate-spec
clean-openapi:
rm -rf generated/
diff --git a/README.md b/README.md
index 8547414..44aaf85 100644
--- a/README.md
+++ b/README.md
@@ -1,154 +1,156 @@

-# Java SDK for Permit.io
-Java SDK for interacting with the Permit.io full-stack permissions platform.
+# Permit.io Java SDK
+
+> **Important Notice**: This is the last version of the SDK that supports Java 8. Starting from the next major release,
+> the SDK will require **Java 17** or higher, aligning with the most widely adopted LTS version in production
+> environments.
+
+The official Java SDK for interacting with the Permit.io full-stack permissions platform.
## Overview
-This guide will walk you through the steps of installing the Permit.io Java SDK and integrating it into your code.
+This guide walks you through installing the Permit.io Java SDK and integrating it into your application.
+
+## Table of Contents
+
+- [Requirements](#requirements)
+- [Installation](#installation)
+ - [Maven](#maven)
+ - [Gradle](#gradle)
+- [Usage](#usage)
+- [Examples](#examples)
+ - [Running Tests](#running-tests)
+- [API Reference](#api-reference)
+- [Contributing](#contributing)
+- [License](#license)
+- [Support](#support)
+
+## Requirements
+
+- **Java**: Version 8 or higher
+- **Build Tool**: Maven 3.6+ or Gradle 7.0+
+- **Permit.io Account**: An API key from the [Permit.io Dashboard](https://app.permit.io)
+- **PDP (Policy Decision Point)**: A running Permit.io PDP container for authorization checks
## Installation
-For [Maven](https://maven.apache.org/) projects, use:
+### Maven
+
+Add the following dependency to your `pom.xml` file:
```xml
+
- io.permit
- permit-sdk-java
- 2.0.0
+ io.permit
+ permit-sdk-java
+ 2.0.0
```
-For [Gradle](https://gradle.org/) projects, configure `permit-sdk-java` as a dependency in your `build.gradle` file:
+### Gradle
+
+Add the following dependency to your `build.gradle` file:
```groovy
dependencies {
- // ...
-
implementation 'io.permit:permit-sdk-java:2.0.0'
}
```
+For Kotlin DSL (`build.gradle.kts`):
+
+```kotlin
+dependencies {
+ implementation("io.permit:permit-sdk-java:2.0.0")
+}
+```
+
## Usage
+For complete, runnable examples, see the [Examples](#examples) section below.
+
### Initializing the SDK
-To init the SDK, you need to create a new Permit client with the API key you got from the Permit.io dashboard.
+Create a `Permit` client with your API key from the [Permit.io Dashboard](https://app.permit.io):
-First we will create a new `PermitConfig` object so we can pass it to the Permit client.
+- See: [BasicPermissionCheckExample.java](src/main/java/io/permit/sdk/examples/BasicPermissionCheckExample.java) - Shows
+ SDK initialization with `PermitConfig.Builder`
-Second, we will create a new `Permit` client with the `PermitConfig` object we created.
+### Checking Permissions
-```java
-import io.permit.sdk.Permit;
-import io.permit.sdk.PermitConfig;
+Use `permit.check()` to verify if a user can perform an action on a resource:
-// This line initializes the SDK and connects your Java app
-// to the Permit.io PDP container you've set up in the previous step.
-Permit permit = new Permit(
- new PermitConfig.Builder("[YOUR_API_KEY]")
- // in production, you might need to change this url to fit your deployment
- .withPdpAddress("http://localhost:7766")
- // optionally, if you wish to get more debug messages to your log, set this to true
- .withDebugMode(false)
- .build()
- );
-```
+- **Basic checks**:
+ [BasicPermissionCheckExample.java](src/main/java/io/permit/sdk/examples/BasicPermissionCheckExample.java) -
+ Permission checks with default tenant
+- **Advanced checks**:
+ [AdvancedPermissionCheckExample.java](src/main/java/io/permit/sdk/examples/AdvancedPermissionCheckExample.java) -
+ Checks with user attributes and explicit tenant.
-### Checking permissions
+### Syncing Users
-To check permissions using our `permit.check()` method, you will have to create User and Resource models as input to the permission check.
-The models are located in ``
+Before running permission checks, sync users to Permit.io using `permit.api.users.sync()`:
-Follow the example below:
+- See: [UserSyncExample.java](src/main/java/io/permit/sdk/examples/UserSyncExample.java) - User synchronization with
+ full profile and minimal information.
-```java
-import io.permit.sdk.enforcement.Resource;
-import io.permit.sdk.enforcement.User;
-import io.permit.sdk.Permit;
+## Examples
-boolean permitted = permit.check(
- // building the user object using User.fromString()
- // the user key (this is the unique identifier of the user in the permission system).
- User.fromString("[USER KEY]"),
- // the action key (string)
- "create",
- // the resource object, can be initialized from string if the "default" tenant is used.
- Resource.fromString("document")
-);
+Complete, runnable examples are available in the [
+`src/main/java/io/permit/sdk/examples`](src/main/java/io/permit/sdk/examples) directory:
-if (permitted) {
- System.out.println("User is PERMITTED to create a document in the 'default' tenant");
-} else {
- System.out.println("User is NOT PERMITTED to create a document in the 'default' tenant");
-}
-```
+| Example | Description |
+|-----------------------------------------------------------------------------------------------------------------|-----------------------------------------------------------|
+| [BasicPermissionCheckExample.java](src/main/java/io/permit/sdk/examples/BasicPermissionCheckExample.java) | SDK initialization and basic permission checks |
+| [AdvancedPermissionCheckExample.java](src/main/java/io/permit/sdk/examples/AdvancedPermissionCheckExample.java) | Permission checks with user attributes and tenant context |
+| [UserSyncExample.java](src/main/java/io/permit/sdk/examples/UserSyncExample.java) | Synchronizing users with the Permit API |
-A more complicated example (passing attributes on the user object, using an explicit tenant in the resource):
+### Running Tests
-```java
-import io.permit.sdk.enforcement.Resource;
-import io.permit.sdk.enforcement.User;
-import java.util.HashMap;
+Each example has a corresponding unit test that uses Mockito mocks (no PDP required):
+| Example | Test |
+|--------------------------------|-------------------------------------------------------------------------------------------------------------------------|
+| BasicPermissionCheckExample | [BasicPermissionCheckExampleTest.java](src/test/java/io/permit/sdk/examples/BasicPermissionCheckExampleTest.java) |
+| AdvancedPermissionCheckExample | [AdvancedPermissionCheckExampleTest.java](src/test/java/io/permit/sdk/examples/AdvancedPermissionCheckExampleTest.java) |
+| UserSyncExample | [UserSyncExampleTest.java](src/test/java/io/permit/sdk/examples/UserSyncExampleTest.java) |
-HashMap userAttributes = new HashMap<>();
-userAttributes.put("age", Integer.valueOf(20));
-userAttributes.put("favorite_color", "yellow");
+```bash
+# Run all tests
+./gradlew test
-boolean permitted = permit.check(
- // building the user object using the User.Builder class
- new User.Builder("[USER KEY]").withAttributes(userAttributes).build(),
- // the action key (string)
- "create",
- // building the resource object using the Resource.Builder in order to pass an explicit tenant key: "awesome-inc"
- new Resource.Builder("document").withTenant("awesome-inc").build()
-);
+# Run only example tests
+./gradlew test --tests "io.permit.sdk.examples.*"
-if (permitted) {
- System.out.println("User is PERMITTED to create a document in the 'awesome-inc' tenant");
-} else {
- System.out.println("User is NOT PERMITTED to create a document in the 'awesome-inc' tenant");
-}
+# Run a specific test
+./gradlew test --tests "io.permit.sdk.examples.BasicPermissionCheckExampleTest"
```
-### Syncing users
-
-When the user first logins, and after you check if he authenticated successfully (i.e: **by checking the JWT access token**) -
-you need to declare the user in the permission system so you can run `permit.check()` on that user.
+## API Reference
-To declare (or "sync") a user in the Permit.io API, use the `permit.api.users.sync()` method.
+For complete API documentation, refer to
+the [Javadoc reference](https://javadoc.io/doc/io.permit/permit-sdk-java/latest/index.html).
-Follow the example below:
+The recommended starting point is
+the [Permit](https://javadoc.io/doc/io.permit/permit-sdk-java/latest/io/permit/sdk/Permit.html) class, which serves as
+the main entry point for the SDK.
-```java
-import io.permit.sdk.api.models.CreateOrUpdateResult;
-import io.permit.sdk.enforcement.User;
+## Contributing
-HashMap userAttributes = new HashMap<>();
-userAttributes.put("age", Integer.valueOf(50));
-userAttributes.put("fav_color", "red");
+We welcome contributions to the Permit.io Java SDK! Please see our [Contributing Guide](CONTRIBUTING.md) for details on:
-CreateOrUpdateResult result = permit.api.users.sync(
- (new User.Builder("auth0|elon"))
- .withEmail("elonmusk@tesla.com")
- .withFirstName("Elon")
- .withLastName("Musk")
- .withAttributes(userAttributes)
- .build()
-);
-UserRead user = result.getResult();
-assertTrue(result.wasCreated());
-```
-
-Most params to UserCreates are optional, and only the unique user key is needed. This is valid:
+- Setting up your development environment
+- Development workflow and code style guidelines
+- How to regenerate OpenAPI models
+- Submitting pull requests
-```java
-CreateOrUpdateResult result = permit.api.users.sync(new UserCreate("[USER KEY]"));
-```
+## License
-## Javadoc reference
+This SDK is licensed under the Apache License 2.0. See the [LICENSE](LICENSE) file for details.
-To view the javadoc reference, [click here](https://javadoc.io/doc/io.permit/permit-sdk-java/2.0.0/index.html).
+## Support
-It's easiest to start with the root [Permit](https://javadoc.io/static/io.permit/permit-sdk-java/2.0.0/io/permit/sdk/Permit.html) class.
+- **Documentation**: [Permit.io Docs](https://docs.permit.io)
+- **GitHub Issues**: [Report an issue](https://github.com/permitio/permit-java/issues)
+- **Community**: [Permit.io Slack](https://permit.io/slack)
diff --git a/build.gradle b/build.gradle
index cc9bfec..66bd526 100644
--- a/build.gradle
+++ b/build.gradle
@@ -36,7 +36,7 @@ repositories {
java {
toolchain {
- languageVersion = JavaLanguageVersion.of(8)
+ languageVersion = JavaLanguageVersion.of(8)
}
// sources are required by maven central in order to accept the package
withSourcesJar()
@@ -65,15 +65,21 @@ dependencies {
implementation "jakarta.annotation:jakarta.annotation-api:1.3.5"
// logger
- implementation 'ch.qos.logback:logback-classic:1.4.14'
- implementation 'ch.qos.logback:logback-core:1.4.14'
- implementation 'org.slf4j:slf4j-api:1.7.33'
-
+ // SLF4J API is the logging facade used by main code - consumers provide their own implementation
+ implementation 'org.slf4j:slf4j-api:2.0.9'
+ // Logback is only needed for tests (not used in main code, only SLF4J API is)
+ // Note: 1.3.x is required for Java 8 compatibility (1.4.x requires Java 11+)
+ testImplementation 'ch.qos.logback:logback-classic:1.3.14'
+ testImplementation 'ch.qos.logback:logback-core:1.3.14'
// Use JUnit Jupiter for testing.
testImplementation 'org.junit.jupiter:junit-jupiter:5.7.2'
+ // Mockito for unit testing with mocks (4.6.x supports Java 8 and works with older Gradle)
+ testImplementation 'org.mockito:mockito-core:4.6.1'
+ testImplementation 'org.mockito:mockito-junit-jupiter:4.6.1'
+
// These dependencies are used internally, and not exposed to consumers on their own compile classpath.
// google standard java library
implementation 'com.google.guava:guava:32.0.0-jre'
@@ -228,6 +234,6 @@ tasks.named('test') {
tasks.named('jar') {
manifest {
attributes('Implementation-Title': project.name,
- 'Implementation-Version': project.version)
+ 'Implementation-Version': project.version)
}
-}
\ No newline at end of file
+}
diff --git a/src/main/java/io/permit/sdk/examples/AdvancedPermissionCheckExample.java b/src/main/java/io/permit/sdk/examples/AdvancedPermissionCheckExample.java
new file mode 100644
index 0000000..a271415
--- /dev/null
+++ b/src/main/java/io/permit/sdk/examples/AdvancedPermissionCheckExample.java
@@ -0,0 +1,213 @@
+package io.permit.sdk.examples;
+
+import io.permit.sdk.Permit;
+import io.permit.sdk.PermitConfig;
+import io.permit.sdk.api.PermitApiError;
+import io.permit.sdk.enforcement.Resource;
+import io.permit.sdk.enforcement.User;
+
+import java.io.IOException;
+import java.util.HashMap;
+
+/**
+ * AdvancedPermissionCheckExample demonstrates advanced permission checking
+ * with user attributes, resource attributes, and explicit tenant specification.
+ *
+ * This example shows:
+ * - How to create users with attributes (for ABAC - Attribute-Based Access Control)
+ * - How to create resources with specific instances and tenants
+ * - How to perform permission checks with full context
+ */
+public class AdvancedPermissionCheckExample {
+
+ private final Permit permit;
+
+ /**
+ * Creates a new AdvancedPermissionCheckExample with the given Permit instance.
+ * This constructor allows dependency injection for testing.
+ *
+ * @param permit The Permit SDK instance to use for permission checks
+ */
+ public AdvancedPermissionCheckExample(Permit permit) {
+ this.permit = permit;
+ }
+
+ /**
+ * Creates a new AdvancedPermissionCheckExample with the given API key.
+ *
+ * @param apiKey Your Permit.io API key
+ * @param pdpAddress The address of your Policy Decision Point
+ */
+ public AdvancedPermissionCheckExample(String apiKey, String pdpAddress) {
+ PermitConfig config = new PermitConfig.Builder(apiKey)
+ .withPdpAddress(pdpAddress)
+ .withDebugMode(false)
+ .build();
+ this.permit = new Permit(config);
+ }
+
+ /**
+ * Checks permission for a user with attributes against a resource in a specific tenant.
+ *
+ * @param userKey The unique identifier for the user
+ * @param userEmail The user's email address
+ * @param userAttributes Additional attributes for the user (for ABAC)
+ * @param action The action to check (e.g., "read", "write", "delete")
+ * @param resourceType The type of resource (e.g., "document", "folder")
+ * @param resourceKey The specific resource instance key (can be null for type-level check)
+ * @param tenant The tenant context for the permission check
+ * @return true if the user is permitted, false otherwise
+ * @throws IOException if there's a network error communicating with the PDP
+ * @throws PermitApiError if the PDP returns an error response
+ */
+ public boolean checkPermissionWithContext(
+ String userKey,
+ String userEmail,
+ HashMap userAttributes,
+ String action,
+ String resourceType,
+ String resourceKey,
+ String tenant) throws IOException, PermitApiError {
+
+ // Build a User with attributes using the Builder pattern
+ User.Builder userBuilder = new User.Builder(userKey);
+ if (userEmail != null) {
+ userBuilder.withEmail(userEmail);
+ }
+ if (userAttributes != null) {
+ userBuilder.withAttributes(userAttributes);
+ }
+ User user = userBuilder.build();
+
+ // Build a Resource with tenant and optional key using the Builder pattern
+ Resource.Builder resourceBuilder = new Resource.Builder(resourceType);
+ if (resourceKey != null) {
+ resourceBuilder.withKey(resourceKey);
+ }
+ if (tenant != null) {
+ resourceBuilder.withTenant(tenant);
+ }
+ Resource resource = resourceBuilder.build();
+
+ // Perform the permission check
+ return permit.check(user, action, resource);
+ }
+
+ /**
+ * Checks permission for a user with attributes against a resource with attributes.
+ * This is useful for Attribute-Based Access Control (ABAC) scenarios.
+ *
+ * @param userKey The unique identifier for the user
+ * @param userAttributes Attributes for the user (for ABAC)
+ * @param action The action to check
+ * @param resourceType The type of resource
+ * @param resourceKey The specific resource instance key
+ * @param resourceAttributes Attributes for the resource (for ABAC)
+ * @param tenant The tenant context
+ * @return true if the user is permitted, false otherwise
+ * @throws IOException if there's a network error
+ * @throws PermitApiError if the PDP returns an error
+ */
+ public boolean checkPermissionWithAttributes(
+ String userKey,
+ HashMap userAttributes,
+ String action,
+ String resourceType,
+ String resourceKey,
+ HashMap resourceAttributes,
+ String tenant) throws IOException, PermitApiError {
+
+ // Build a User with attributes
+ User user = new User.Builder(userKey)
+ .withAttributes(userAttributes)
+ .build();
+
+ // Build a Resource with attributes and tenant
+ Resource.Builder resourceBuilder = new Resource.Builder(resourceType);
+ if (resourceKey != null) {
+ resourceBuilder.withKey(resourceKey);
+ }
+ if (resourceAttributes != null) {
+ resourceBuilder.withAttributes(resourceAttributes);
+ }
+ if (tenant != null) {
+ resourceBuilder.withTenant(tenant);
+ }
+ Resource resource = resourceBuilder.build();
+
+ // Perform the permission check
+ return permit.check(user, action, resource);
+ }
+
+ /**
+ * Main method demonstrating advanced usage of the Permit SDK.
+ *
+ * @param args Command line arguments (not used)
+ */
+ public static void main(String[] args) {
+ // Get API key from environment variable
+ String apiKey = System.getenv("PERMIT_API_KEY");
+ if (apiKey == null || apiKey.isEmpty()) {
+ System.err.println("Please set the PERMIT_API_KEY environment variable");
+ System.exit(1);
+ }
+
+ String pdpAddress = System.getenv().getOrDefault("PDP_URL", "http://localhost:7766");
+ AdvancedPermissionCheckExample example = new AdvancedPermissionCheckExample(apiKey, pdpAddress);
+
+ try {
+ // Example 1: Check permission with user attributes and tenant
+ String userKey = "john@acme.com";
+ String userEmail = "john@acme.com";
+
+ // User attributes for ABAC (e.g., department, clearance level)
+ HashMap userAttributes = new HashMap();
+ userAttributes.put("department", "engineering");
+ userAttributes.put("clearance_level", 3);
+
+ String action = "edit";
+ String resourceType = "document";
+ String resourceKey = "doc-123";
+ String tenant = "acme-corp";
+
+ boolean permitted = example.checkPermissionWithContext(
+ userKey, userEmail, userAttributes,
+ action, resourceType, resourceKey, tenant
+ );
+
+ System.out.println("Example 1 - Permission check with tenant:");
+ System.out.println(" User: " + userKey);
+ System.out.println(" Action: " + action);
+ System.out.println(" Resource: " + resourceType + ":" + resourceKey);
+ System.out.println(" Tenant: " + tenant);
+ System.out.println(" Result: " + (permitted ? "PERMITTED" : "DENIED"));
+ System.out.println();
+
+ // Example 2: Check permission with both user and resource attributes
+ HashMap resourceAttributes = new HashMap();
+ resourceAttributes.put("classification", "confidential");
+ resourceAttributes.put("owner", "engineering");
+
+ boolean permitted2 = example.checkPermissionWithAttributes(
+ userKey, userAttributes,
+ "delete", resourceType, resourceKey,
+ resourceAttributes, tenant
+ );
+
+ System.out.println("Example 2 - Permission check with ABAC attributes:");
+ System.out.println(" User: " + userKey + " (department=engineering, clearance=3)");
+ System.out.println(" Action: delete");
+ System.out.println(" Resource: " + resourceType + ":" + resourceKey + " (classification=confidential)");
+ System.out.println(" Tenant: " + tenant);
+ System.out.println(" Result: " + (permitted2 ? "PERMITTED" : "DENIED"));
+
+ } catch (IOException e) {
+ System.err.println("Network error while checking permission: " + e.getMessage());
+ e.printStackTrace();
+ } catch (PermitApiError e) {
+ System.err.println("Permit API error: " + e.getMessage());
+ System.err.println("Response code: " + e.getResponseCode());
+ e.printStackTrace();
+ }
+ }
+}
diff --git a/src/main/java/io/permit/sdk/examples/BasicPermissionCheckExample.java b/src/main/java/io/permit/sdk/examples/BasicPermissionCheckExample.java
new file mode 100644
index 0000000..1ac3605
--- /dev/null
+++ b/src/main/java/io/permit/sdk/examples/BasicPermissionCheckExample.java
@@ -0,0 +1,108 @@
+package io.permit.sdk.examples;
+
+import io.permit.sdk.Permit;
+import io.permit.sdk.PermitConfig;
+import io.permit.sdk.api.PermitApiError;
+import io.permit.sdk.enforcement.Resource;
+import io.permit.sdk.enforcement.User;
+
+import java.io.IOException;
+
+/**
+ * BasicPermissionCheckExample demonstrates how to initialize the Permit SDK
+ * and perform a basic permission check.
+ *
+ * This example shows:
+ * - How to create a PermitConfig with your API key
+ * - How to initialize the Permit SDK
+ * - How to create User and Resource objects
+ * - How to perform a permission check
+ */
+public class BasicPermissionCheckExample {
+
+ private final Permit permit;
+
+ /**
+ * Creates a new BasicPermissionCheckExample with the given Permit instance.
+ * This constructor allows dependency injection for testing.
+ *
+ * @param permit The Permit SDK instance to use for permission checks
+ */
+ public BasicPermissionCheckExample(Permit permit) {
+ this.permit = permit;
+ }
+
+ /**
+ * Creates a new BasicPermissionCheckExample with the given API key.
+ * Uses default PDP address (localhost:7766).
+ *
+ * @param apiKey Your Permit.io API key
+ */
+ public BasicPermissionCheckExample(String apiKey) {
+ PermitConfig config = new PermitConfig.Builder(apiKey)
+ .withPdpAddress("http://localhost:7766")
+ .withDebugMode(false)
+ .build();
+ this.permit = new Permit(config);
+ }
+
+ /**
+ * Checks if a user is permitted to perform an action on a resource.
+ *
+ * @param userKey The unique identifier for the user
+ * @param action The action to check (e.g., "read", "write", "delete")
+ * @param resourceType The type of resource (e.g., "document", "folder")
+ * @return true if the user is permitted, false otherwise
+ * @throws IOException if there's a network error communicating with the PDP
+ * @throws PermitApiError if the PDP returns an error response
+ */
+ public boolean checkPermission(String userKey, String action, String resourceType)
+ throws IOException, PermitApiError {
+ // Create a User object from the user key
+ User user = User.fromString(userKey);
+
+ // Create a Resource object from the resource type
+ Resource resource = Resource.fromString(resourceType);
+
+ // Perform the permission check
+ return permit.check(user, action, resource);
+ }
+
+ /**
+ * Main method demonstrating basic usage of the Permit SDK.
+ *
+ * @param args Command line arguments (not used)
+ */
+ public static void main(String[] args) {
+ // Get API key from environment variable or use a placeholder
+ String apiKey = System.getenv("PERMIT_API_KEY");
+ if (apiKey == null || apiKey.isEmpty()) {
+ System.err.println("Please set the PERMIT_API_KEY environment variable");
+ System.exit(1);
+ }
+
+ BasicPermissionCheckExample example = new BasicPermissionCheckExample(apiKey);
+
+ try {
+ // Example: Check if user "john@example.com" can "read" a "document"
+ String userKey = "john@example.com";
+ String action = "read";
+ String resourceType = "document";
+
+ boolean permitted = example.checkPermission(userKey, action, resourceType);
+
+ if (permitted) {
+ System.out.println("User '" + userKey + "' is PERMITTED to " + action + " " + resourceType);
+ } else {
+ System.out.println("User '" + userKey + "' is DENIED to " + action + " " + resourceType);
+ }
+ } catch (IOException e) {
+ System.err.println("Network error while checking permission: " + e.getMessage());
+ e.printStackTrace();
+ } catch (PermitApiError e) {
+ System.err.println("Permit API error: " + e.getMessage());
+ System.err.println("Response code: " + e.getResponseCode());
+ e.printStackTrace();
+ }
+ }
+}
diff --git a/src/main/java/io/permit/sdk/examples/UserSyncExample.java b/src/main/java/io/permit/sdk/examples/UserSyncExample.java
new file mode 100644
index 0000000..af51d76
--- /dev/null
+++ b/src/main/java/io/permit/sdk/examples/UserSyncExample.java
@@ -0,0 +1,214 @@
+package io.permit.sdk.examples;
+
+import io.permit.sdk.Permit;
+import io.permit.sdk.PermitConfig;
+import io.permit.sdk.api.PermitApiError;
+import io.permit.sdk.api.PermitContextError;
+import io.permit.sdk.api.models.CreateOrUpdateResult;
+import io.permit.sdk.enforcement.User;
+import io.permit.sdk.openapi.models.UserRead;
+
+import java.io.IOException;
+import java.util.HashMap;
+
+/**
+ * UserSyncExample demonstrates how to synchronize users with the Permit API.
+ *
+ * User synchronization is the process of creating or updating users in Permit.
+ * This is typically done when:
+ * - A new user signs up in your application
+ * - User information changes (email, name, attributes)
+ * - You want to pre-populate users before they first access your application
+ *
+ * This example shows:
+ * - How to sync a simple user by key
+ * - How to sync a user with full profile information
+ * - How to sync a user with custom attributes for ABAC
+ * - How to handle the sync result (created vs updated)
+ */
+public class UserSyncExample {
+
+ private final Permit permit;
+
+ /**
+ * Creates a new UserSyncExample with the given Permit instance.
+ * This constructor allows dependency injection for testing.
+ *
+ * @param permit The Permit SDK instance to use for user synchronization
+ */
+ public UserSyncExample(Permit permit) {
+ this.permit = permit;
+ }
+
+ /**
+ * Creates a new UserSyncExample with the given API key.
+ *
+ * @param apiKey Your Permit.io API key
+ * @param pdpAddress The address of your Policy Decision Point
+ */
+ public UserSyncExample(String apiKey, String pdpAddress) {
+ PermitConfig config = new PermitConfig.Builder(apiKey)
+ .withPdpAddress(pdpAddress)
+ .withDebugMode(false)
+ .build();
+ this.permit = new Permit(config);
+ }
+
+ /**
+ * Syncs a simple user identified only by their key.
+ * If the user already exists, this will update them (no-op if no changes).
+ * If the user doesn't exist, this will create them.
+ *
+ * @param userKey The unique identifier for the user
+ * @return The result containing the user data and whether it was created
+ * @throws IOException if there's a network error
+ * @throws PermitApiError if the Permit API returns an error
+ * @throws PermitContextError if the API context is not properly configured
+ */
+ public CreateOrUpdateResult syncSimpleUser(String userKey)
+ throws IOException, PermitApiError, PermitContextError {
+ User user = User.fromString(userKey);
+ return permit.api.users.sync(user);
+ }
+
+ /**
+ * Syncs a user with full profile information.
+ *
+ * @param userKey The unique identifier for the user
+ * @param email The user's email address
+ * @param firstName The user's first name
+ * @param lastName The user's last name
+ * @return The result containing the user data and whether it was created
+ * @throws IOException if there's a network error
+ * @throws PermitApiError if the Permit API returns an error
+ * @throws PermitContextError if the API context is not properly configured
+ */
+ public CreateOrUpdateResult syncUserWithProfile(
+ String userKey,
+ String email,
+ String firstName,
+ String lastName) throws IOException, PermitApiError, PermitContextError {
+
+ User user = new User.Builder(userKey)
+ .withEmail(email)
+ .withFirstName(firstName)
+ .withLastName(lastName)
+ .build();
+
+ return permit.api.users.sync(user);
+ }
+
+ /**
+ * Syncs a user with custom attributes for Attribute-Based Access Control (ABAC).
+ * Attributes can be used in permission policies to make fine-grained access decisions.
+ *
+ * @param userKey The unique identifier for the user
+ * @param email The user's email address
+ * @param firstName The user's first name
+ * @param lastName The user's last name
+ * @param attributes Custom attributes for ABAC (e.g., department, role, clearance)
+ * @return The result containing the user data and whether it was created
+ * @throws IOException if there's a network error
+ * @throws PermitApiError if the Permit API returns an error
+ * @throws PermitContextError if the API context is not properly configured
+ */
+ public CreateOrUpdateResult syncUserWithAttributes(
+ String userKey,
+ String email,
+ String firstName,
+ String lastName,
+ HashMap attributes) throws IOException, PermitApiError, PermitContextError {
+
+ User user = new User.Builder(userKey)
+ .withEmail(email)
+ .withFirstName(firstName)
+ .withLastName(lastName)
+ .withAttributes(attributes)
+ .build();
+
+ return permit.api.users.sync(user);
+ }
+
+ /**
+ * Main method demonstrating user synchronization with the Permit SDK.
+ *
+ * @param args Command line arguments (not used)
+ */
+ public static void main(String[] args) {
+ // Get API key from environment variable
+ String apiKey = System.getenv("PERMIT_API_KEY");
+ if (apiKey == null || apiKey.isEmpty()) {
+ System.err.println("Please set the PERMIT_API_KEY environment variable");
+ System.exit(1);
+ }
+
+ String pdpAddress = System.getenv().getOrDefault("PDP_URL", "http://localhost:7766");
+ UserSyncExample example = new UserSyncExample(apiKey, pdpAddress);
+
+ try {
+ // Example 1: Sync a simple user
+ System.out.println("Example 1: Syncing a simple user...");
+ CreateOrUpdateResult result1 = example.syncSimpleUser("user-001");
+
+ UserRead user1 = result1.getResult();
+ System.out.println(" User key: " + user1.key);
+ System.out.println(" User ID: " + user1.id);
+ System.out.println(" Was created: " + result1.wasCreated());
+ System.out.println();
+
+ // Example 2: Sync a user with profile information
+ System.out.println("Example 2: Syncing a user with profile...");
+ CreateOrUpdateResult result2 = example.syncUserWithProfile(
+ "john.doe@example.com",
+ "john.doe@example.com",
+ "John",
+ "Doe"
+ );
+
+ UserRead user2 = result2.getResult();
+ System.out.println(" User key: " + user2.key);
+ System.out.println(" Email: " + user2.email);
+ System.out.println(" First name: " + user2.firstName);
+ System.out.println(" Last name: " + user2.lastName);
+ System.out.println(" Was created: " + result2.wasCreated());
+ System.out.println();
+
+ // Example 3: Sync a user with ABAC attributes
+ System.out.println("Example 3: Syncing a user with ABAC attributes...");
+ HashMap attributes = new HashMap();
+ attributes.put("department", "engineering");
+ attributes.put("clearance_level", 5);
+ attributes.put("is_manager", true);
+ attributes.put("teams", new String[]{"backend", "devops"});
+
+ CreateOrUpdateResult result3 = example.syncUserWithAttributes(
+ "alice.smith@example.com",
+ "alice.smith@example.com",
+ "Alice",
+ "Smith",
+ attributes
+ );
+
+ UserRead user3 = result3.getResult();
+ System.out.println(" User key: " + user3.key);
+ System.out.println(" Email: " + user3.email);
+ System.out.println(" First name: " + user3.firstName);
+ System.out.println(" Last name: " + user3.lastName);
+ System.out.println(" Attributes: " + user3.attributes);
+ System.out.println(" Was created: " + result3.wasCreated());
+
+ } catch (IOException e) {
+ System.err.println("Network error while syncing user: " + e.getMessage());
+ e.printStackTrace();
+ } catch (PermitApiError e) {
+ System.err.println("Permit API error: " + e.getMessage());
+ System.err.println("Response code: " + e.getResponseCode());
+ System.err.println("Raw response: " + e.getRawResponse());
+ e.printStackTrace();
+ } catch (PermitContextError e) {
+ System.err.println("Permit context error: " + e.getMessage());
+ System.err.println("Make sure you are using an Environment-level API key");
+ e.printStackTrace();
+ }
+ }
+}
diff --git a/src/test/java/io/permit/sdk/examples/AdvancedPermissionCheckExampleTest.java b/src/test/java/io/permit/sdk/examples/AdvancedPermissionCheckExampleTest.java
new file mode 100644
index 0000000..ca66621
--- /dev/null
+++ b/src/test/java/io/permit/sdk/examples/AdvancedPermissionCheckExampleTest.java
@@ -0,0 +1,309 @@
+package io.permit.sdk.examples;
+
+import io.permit.sdk.Permit;
+import io.permit.sdk.PermitConfig;
+import io.permit.sdk.api.PermitApiError;
+import io.permit.sdk.enforcement.Resource;
+import io.permit.sdk.enforcement.User;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+import java.io.IOException;
+import java.util.HashMap;
+
+import static org.junit.jupiter.api.Assertions.*;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.*;
+
+/**
+ * Unit tests for AdvancedPermissionCheckExample.
+ * These tests use Mockito to mock the Permit class and verify that user attributes,
+ * resource attributes, and tenant context are handled correctly by the actual example class.
+ */
+@ExtendWith(MockitoExtension.class)
+class AdvancedPermissionCheckExampleTest {
+
+ @Mock
+ private Permit mockPermit;
+
+ private AdvancedPermissionCheckExample example;
+
+ @BeforeEach
+ void setUp() {
+ example = new AdvancedPermissionCheckExample(mockPermit);
+ }
+
+ @Test
+ @DisplayName("Should check permission with user email and tenant")
+ void testCheckPermissionWithContext_FullContext() throws IOException, PermitApiError {
+ // Given
+ String userKey = "user-123";
+ String userEmail = "user@example.com";
+ HashMap userAttributes = new HashMap();
+ userAttributes.put("department", "engineering");
+ String action = "edit";
+ String resourceType = "document";
+ String resourceKey = "doc-456";
+ String tenant = "acme-corp";
+
+ when(mockPermit.check(any(User.class), eq(action), any(Resource.class)))
+ .thenReturn(true);
+
+ // When
+ boolean result = example.checkPermissionWithContext(
+ userKey, userEmail, userAttributes,
+ action, resourceType, resourceKey, tenant
+ );
+
+ // Then
+ assertTrue(result);
+
+ // Verify that check was called with correct parameters
+ ArgumentCaptor userCaptor = ArgumentCaptor.forClass(User.class);
+ ArgumentCaptor resourceCaptor = ArgumentCaptor.forClass(Resource.class);
+ verify(mockPermit).check(userCaptor.capture(), eq(action), resourceCaptor.capture());
+
+ User capturedUser = userCaptor.getValue();
+ assertEquals(userKey, capturedUser.getKey());
+ assertEquals(userEmail, capturedUser.getEmail());
+ assertEquals("engineering", capturedUser.getAttributes().get("department"));
+
+ Resource capturedResource = resourceCaptor.getValue();
+ assertEquals(resourceType, capturedResource.getType());
+ assertEquals(resourceKey, capturedResource.getKey());
+ assertEquals(tenant, capturedResource.getTenant());
+ }
+
+ @Test
+ @DisplayName("Should check permission with null optional fields")
+ void testCheckPermissionWithContext_NullOptionalFields() throws IOException, PermitApiError {
+ // Given
+ String userKey = "user-123";
+ String action = "read";
+ String resourceType = "document";
+
+ when(mockPermit.check(any(User.class), eq(action), any(Resource.class)))
+ .thenReturn(true);
+
+ // When - email, attributes, resourceKey, and tenant are all null
+ boolean result = example.checkPermissionWithContext(
+ userKey, null, null,
+ action, resourceType, null, null
+ );
+
+ // Then
+ assertTrue(result);
+
+ ArgumentCaptor userCaptor = ArgumentCaptor.forClass(User.class);
+ ArgumentCaptor resourceCaptor = ArgumentCaptor.forClass(Resource.class);
+ verify(mockPermit).check(userCaptor.capture(), eq(action), resourceCaptor.capture());
+
+ User capturedUser = userCaptor.getValue();
+ assertEquals(userKey, capturedUser.getKey());
+ assertNull(capturedUser.getEmail());
+ assertNull(capturedUser.getAttributes());
+
+ Resource capturedResource = resourceCaptor.getValue();
+ assertEquals(resourceType, capturedResource.getType());
+ assertNull(capturedResource.getKey());
+ assertNull(capturedResource.getTenant());
+ }
+
+ @Test
+ @DisplayName("Should check permission with ABAC attributes on both user and resource")
+ void testCheckPermissionWithAttributes_FullABAC() throws IOException, PermitApiError {
+ // Given
+ String userKey = "user-123";
+ HashMap userAttributes = new HashMap();
+ userAttributes.put("clearance_level", 5);
+ userAttributes.put("department", "security");
+
+ String action = "access";
+ String resourceType = "classified-document";
+ String resourceKey = "secret-doc-001";
+
+ HashMap resourceAttributes = new HashMap();
+ resourceAttributes.put("classification", "top-secret");
+ resourceAttributes.put("required_clearance", 5);
+
+ String tenant = "gov-agency";
+
+ when(mockPermit.check(any(User.class), eq(action), any(Resource.class)))
+ .thenReturn(true);
+
+ // When
+ boolean result = example.checkPermissionWithAttributes(
+ userKey, userAttributes,
+ action, resourceType, resourceKey,
+ resourceAttributes, tenant
+ );
+
+ // Then
+ assertTrue(result);
+
+ ArgumentCaptor userCaptor = ArgumentCaptor.forClass(User.class);
+ ArgumentCaptor resourceCaptor = ArgumentCaptor.forClass(Resource.class);
+ verify(mockPermit).check(userCaptor.capture(), eq(action), resourceCaptor.capture());
+
+ User capturedUser = userCaptor.getValue();
+ assertEquals(5, capturedUser.getAttributes().get("clearance_level"));
+ assertEquals("security", capturedUser.getAttributes().get("department"));
+
+ Resource capturedResource = resourceCaptor.getValue();
+ assertEquals("top-secret", capturedResource.getAttributes().get("classification"));
+ assertEquals(5, capturedResource.getAttributes().get("required_clearance"));
+ }
+
+ @Test
+ @DisplayName("Should return false when permission is denied with ABAC")
+ void testCheckPermissionWithAttributes_Denied() throws IOException, PermitApiError {
+ // Given
+ String userKey = "user-123";
+ HashMap userAttributes = new HashMap();
+ userAttributes.put("clearance_level", 2); // Lower clearance
+
+ HashMap resourceAttributes = new HashMap();
+ resourceAttributes.put("required_clearance", 5); // Requires higher clearance
+
+ when(mockPermit.check(any(User.class), eq("access"), any(Resource.class)))
+ .thenReturn(false); // Permission denied
+
+ // When
+ boolean result = example.checkPermissionWithAttributes(
+ userKey, userAttributes,
+ "access", "classified-document", "doc-001",
+ resourceAttributes, "tenant"
+ );
+
+ // Then
+ assertFalse(result, "User with lower clearance should be denied");
+ }
+
+ @Test
+ @DisplayName("Should propagate IOException from Permit SDK")
+ void testCheckPermissionWithContext_IOException() throws IOException, PermitApiError {
+ // Given
+ when(mockPermit.check(any(User.class), any(String.class), any(Resource.class)))
+ .thenThrow(new IOException("Connection timeout"));
+
+ // When/Then
+ IOException exception = assertThrows(IOException.class, new org.junit.jupiter.api.function.Executable() {
+ @Override
+ public void execute() throws Throwable {
+ example.checkPermissionWithContext(
+ "user", "user@example.com", null,
+ "read", "document", null, null
+ );
+ }
+ });
+ assertEquals("Connection timeout", exception.getMessage());
+ }
+
+ @Test
+ @DisplayName("Should propagate PermitApiError from Permit SDK")
+ void testCheckPermissionWithAttributes_PermitApiError() throws IOException, PermitApiError {
+ // Given
+ when(mockPermit.check(any(User.class), any(String.class), any(Resource.class)))
+ .thenThrow(new PermitApiError("Unauthorized", 401, "{\"error\":\"Invalid token\"}"));
+
+ // When/Then
+ PermitApiError exception = assertThrows(PermitApiError.class, new org.junit.jupiter.api.function.Executable() {
+ @Override
+ public void execute() throws Throwable {
+ example.checkPermissionWithAttributes(
+ "user", new HashMap(),
+ "read", "document", null,
+ null, null
+ );
+ }
+ });
+ assertEquals(401, exception.getResponseCode());
+ }
+
+ @Test
+ @DisplayName("Should handle multiple permission checks with different tenants")
+ void testCheckPermissionWithContext_MultipleTenants() throws IOException, PermitApiError {
+ // Given - user is permitted in tenant1 but not in tenant2
+ when(mockPermit.check(any(User.class), eq("edit"), any(Resource.class)))
+ .thenAnswer(invocation -> {
+ Resource resource = invocation.getArgument(2);
+ return "tenant1".equals(resource.getTenant());
+ });
+
+ // When
+ boolean resultTenant1 = example.checkPermissionWithContext(
+ "user", null, null, "edit", "document", "doc-1", "tenant1"
+ );
+ boolean resultTenant2 = example.checkPermissionWithContext(
+ "user", null, null, "edit", "document", "doc-1", "tenant2"
+ );
+
+ // Then
+ assertTrue(resultTenant1, "User should be permitted in tenant1");
+ assertFalse(resultTenant2, "User should be denied in tenant2");
+ }
+
+ @Test
+ @DisplayName("Should handle complex user attributes")
+ void testCheckPermissionWithContext_ComplexAttributes() throws IOException, PermitApiError {
+ // Given
+ HashMap userAttributes = new HashMap();
+ userAttributes.put("roles", new String[]{"admin", "editor"});
+ userAttributes.put("is_active", true);
+ userAttributes.put("login_count", 42);
+ HashMap metadata = new HashMap();
+ metadata.put("source", "oauth");
+ metadata.put("provider", "google");
+ userAttributes.put("metadata", metadata);
+
+ when(mockPermit.check(any(User.class), eq("manage"), any(Resource.class)))
+ .thenReturn(true);
+
+ // When
+ boolean result = example.checkPermissionWithContext(
+ "admin-user", "admin@example.com", userAttributes,
+ "manage", "settings", "global-settings", "main-tenant"
+ );
+
+ // Then
+ assertTrue(result);
+
+ ArgumentCaptor userCaptor = ArgumentCaptor.forClass(User.class);
+ verify(mockPermit).check(userCaptor.capture(), eq("manage"), any(Resource.class));
+
+ User capturedUser = userCaptor.getValue();
+ assertNotNull(capturedUser.getAttributes());
+ assertTrue((Boolean) capturedUser.getAttributes().get("is_active"));
+ assertEquals(42, capturedUser.getAttributes().get("login_count"));
+ }
+
+ @Test
+ @DisplayName("Should verify the actual example class can be instantiated with a real Permit instance")
+ void testAdvancedPermissionCheckExample_CanBeInstantiated() {
+ try {
+ // Given
+ PermitConfig config = new PermitConfig.Builder("test-api-key")
+ .withPdpAddress("http://localhost:7766")
+ .build();
+ Permit permit = new Permit(config);
+
+ // When
+ AdvancedPermissionCheckExample realExample = new AdvancedPermissionCheckExample(permit);
+
+ // Then
+ assertNotNull(realExample, "Should be able to create example with real Permit instance");
+ } catch (UnsupportedClassVersionError e) {
+ // Skip test if there's a class version mismatch (e.g., running with incompatible JVM)
+ System.out.println("Skipping test due to class version incompatibility: " + e.getMessage());
+ } catch (NoClassDefFoundError e) {
+ // Skip test if there's a class version mismatch (e.g., running with incompatible JVM)
+ System.out.println("Skipping test due to class version incompatibility: " + e.getMessage());
+ }
+ }
+}
diff --git a/src/test/java/io/permit/sdk/examples/BasicPermissionCheckExampleTest.java b/src/test/java/io/permit/sdk/examples/BasicPermissionCheckExampleTest.java
new file mode 100644
index 0000000..110063f
--- /dev/null
+++ b/src/test/java/io/permit/sdk/examples/BasicPermissionCheckExampleTest.java
@@ -0,0 +1,143 @@
+package io.permit.sdk.examples;
+
+import io.permit.sdk.Permit;
+import io.permit.sdk.PermitConfig;
+import io.permit.sdk.api.PermitApiError;
+import io.permit.sdk.enforcement.Resource;
+import io.permit.sdk.enforcement.User;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+import java.io.IOException;
+
+import static org.junit.jupiter.api.Assertions.*;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.*;
+
+/**
+ * Unit tests for BasicPermissionCheckExample.
+ * These tests use Mockito to mock the Permit class, testing the actual example class
+ * rather than a test double.
+ */
+@ExtendWith(MockitoExtension.class)
+class BasicPermissionCheckExampleTest {
+
+ @Mock
+ private Permit mockPermit;
+
+ private BasicPermissionCheckExample example;
+
+ @BeforeEach
+ void setUp() {
+ example = new BasicPermissionCheckExample(mockPermit);
+ }
+
+ @Test
+ @DisplayName("Should return true when permission is granted")
+ void testCheckPermission_Granted() throws IOException, PermitApiError {
+ // Given
+ String userKey = "test-user";
+ String action = "read";
+ String resourceType = "document";
+
+ when(mockPermit.check(any(User.class), eq(action), any(Resource.class)))
+ .thenReturn(true);
+
+ // When
+ boolean result = example.checkPermission(userKey, action, resourceType);
+
+ // Then
+ assertTrue(result);
+ verify(mockPermit).check(any(User.class), eq(action), any(Resource.class));
+ }
+
+ @Test
+ @DisplayName("Should return false when permission is denied")
+ void testCheckPermission_Denied() throws IOException, PermitApiError {
+ // Given
+ String userKey = "test-user";
+ String action = "delete";
+ String resourceType = "document";
+
+ when(mockPermit.check(any(User.class), eq(action), any(Resource.class)))
+ .thenReturn(false);
+
+ // When
+ boolean result = example.checkPermission(userKey, action, resourceType);
+
+ // Then
+ assertFalse(result);
+ verify(mockPermit).check(any(User.class), eq(action), any(Resource.class));
+ }
+
+ @Test
+ @DisplayName("Should propagate IOException from Permit SDK")
+ void testCheckPermission_IOException() throws IOException, PermitApiError {
+ // Given
+ String userKey = "test-user";
+ String action = "read";
+ String resourceType = "document";
+
+ when(mockPermit.check(any(User.class), eq(action), any(Resource.class)))
+ .thenThrow(new IOException("Network error"));
+
+ // When/Then
+ IOException exception = assertThrows(IOException.class, new org.junit.jupiter.api.function.Executable() {
+ @Override
+ public void execute() throws Throwable {
+ example.checkPermission(userKey, action, resourceType);
+ }
+ });
+ assertEquals("Network error", exception.getMessage());
+ }
+
+ @Test
+ @DisplayName("Should propagate PermitApiError from Permit SDK")
+ void testCheckPermission_PermitApiError() throws IOException, PermitApiError {
+ // Given
+ String userKey = "test-user";
+ String action = "read";
+ String resourceType = "document";
+
+ when(mockPermit.check(any(User.class), eq(action), any(Resource.class)))
+ .thenThrow(new PermitApiError("API Error", 500, "{\"error\":\"Internal Server Error\"}"));
+
+ // When/Then
+ PermitApiError exception = assertThrows(PermitApiError.class, new org.junit.jupiter.api.function.Executable() {
+ @Override
+ public void execute() throws Throwable {
+ example.checkPermission(userKey, action, resourceType);
+ }
+ });
+ assertEquals(500, exception.getResponseCode());
+ }
+
+ @Test
+ @DisplayName("Should verify the actual example class can be instantiated with a real Permit instance")
+ void testBasicPermissionCheckExample_CanBeInstantiated() {
+ try {
+ // Given
+ PermitConfig config = new PermitConfig.Builder("test-api-key")
+ .withPdpAddress("http://localhost:7766")
+ .build();
+ Permit permit = new Permit(config);
+
+ // When
+ BasicPermissionCheckExample realExample = new BasicPermissionCheckExample(permit);
+
+ // Then
+ assertNotNull(realExample, "Should be able to create example with real Permit instance");
+ } catch (UnsupportedClassVersionError e) {
+ // Skip test if there's a class version mismatch (e.g., running with incompatible JVM)
+ System.out.println("Skipping test due to class version incompatibility: " + e.getMessage());
+ } catch (NoClassDefFoundError e) {
+ // Skip test if there's a class version mismatch (e.g., running with incompatible JVM)
+ System.out.println("Skipping test due to class version incompatibility: " + e.getMessage());
+ }
+ }
+}
diff --git a/src/test/java/io/permit/sdk/examples/UserSyncExampleTest.java b/src/test/java/io/permit/sdk/examples/UserSyncExampleTest.java
new file mode 100644
index 0000000..3f6e843
--- /dev/null
+++ b/src/test/java/io/permit/sdk/examples/UserSyncExampleTest.java
@@ -0,0 +1,334 @@
+package io.permit.sdk.examples;
+
+import io.permit.sdk.Permit;
+import io.permit.sdk.PermitConfig;
+import io.permit.sdk.api.ApiClient;
+import io.permit.sdk.api.PermitApiError;
+import io.permit.sdk.api.PermitContextError;
+import io.permit.sdk.api.UsersApi;
+import io.permit.sdk.api.models.CreateOrUpdateResult;
+import io.permit.sdk.enforcement.User;
+import io.permit.sdk.openapi.models.UserRead;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+import java.io.IOException;
+import java.lang.reflect.Field;
+import java.util.HashMap;
+
+import static org.junit.jupiter.api.Assertions.*;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.*;
+
+/**
+ * Unit tests for UserSyncExample.
+ * These tests use Mockito to mock the Permit class and its nested API objects,
+ * testing the actual UserSyncExample class rather than a test double.
+ */
+@ExtendWith(MockitoExtension.class)
+class UserSyncExampleTest {
+
+ @Mock
+ private Permit mockPermit;
+
+ @Mock
+ private ApiClient mockApiClient;
+
+ @Mock
+ private UsersApi mockUsersApi;
+
+ private UserSyncExample example;
+
+ @BeforeEach
+ void setUp() throws Exception {
+ // Set up the mock chain: mockPermit.api -> mockApiClient, mockApiClient.users -> mockUsersApi
+ // Since 'api' is a public final field, we need to use reflection to set it
+ Field apiField = Permit.class.getDeclaredField("api");
+ apiField.setAccessible(true);
+ apiField.set(mockPermit, mockApiClient);
+
+ Field usersField = ApiClient.class.getDeclaredField("users");
+ usersField.setAccessible(true);
+ usersField.set(mockApiClient, mockUsersApi);
+
+ example = new UserSyncExample(mockPermit);
+ }
+
+ @Test
+ @DisplayName("Should sync a simple user and return created result")
+ void testSyncSimpleUser_Created() throws IOException, PermitApiError, PermitContextError {
+ // Given
+ String userKey = "new-user-001";
+ UserRead expectedUser = new UserRead(userKey, "uuid-123", "org-1", "proj-1", "env-1");
+ CreateOrUpdateResult expectedResult = new CreateOrUpdateResult(expectedUser, true);
+
+ when(mockUsersApi.sync(any(User.class))).thenReturn(expectedResult);
+
+ // When
+ CreateOrUpdateResult result = example.syncSimpleUser(userKey);
+
+ // Then
+ assertNotNull(result);
+ assertTrue(result.wasCreated(), "User should be marked as created");
+ assertEquals(userKey, result.getResult().key);
+ assertEquals("uuid-123", result.getResult().id);
+
+ ArgumentCaptor userCaptor = ArgumentCaptor.forClass(User.class);
+ verify(mockUsersApi).sync(userCaptor.capture());
+ assertEquals(userKey, userCaptor.getValue().getKey());
+ }
+
+ @Test
+ @DisplayName("Should sync a simple user and return updated result")
+ void testSyncSimpleUser_Updated() throws IOException, PermitApiError, PermitContextError {
+ // Given
+ String userKey = "existing-user-001";
+ UserRead expectedUser = new UserRead(userKey, "uuid-456", "org-1", "proj-1", "env-1");
+ CreateOrUpdateResult expectedResult = new CreateOrUpdateResult(expectedUser, false);
+
+ when(mockUsersApi.sync(any(User.class))).thenReturn(expectedResult);
+
+ // When
+ CreateOrUpdateResult result = example.syncSimpleUser(userKey);
+
+ // Then
+ assertNotNull(result);
+ assertFalse(result.wasCreated(), "User should be marked as updated (not created)");
+ assertEquals(userKey, result.getResult().key);
+ }
+
+ @Test
+ @DisplayName("Should sync user with full profile information")
+ void testSyncUserWithProfile() throws IOException, PermitApiError, PermitContextError {
+ // Given
+ String userKey = "john.doe@example.com";
+ String email = "john.doe@example.com";
+ String firstName = "John";
+ String lastName = "Doe";
+
+ UserRead expectedUser = new UserRead(userKey, "uuid-789", "org-1", "proj-1", "env-1")
+ .withEmail(email)
+ .withFirstName(firstName)
+ .withLastName(lastName);
+ CreateOrUpdateResult expectedResult = new CreateOrUpdateResult(expectedUser, true);
+
+ when(mockUsersApi.sync(any(User.class))).thenReturn(expectedResult);
+
+ // When
+ CreateOrUpdateResult result = example.syncUserWithProfile(userKey, email, firstName, lastName);
+
+ // Then
+ assertNotNull(result);
+ assertTrue(result.wasCreated());
+ assertEquals(email, result.getResult().email);
+ assertEquals(firstName, result.getResult().firstName);
+ assertEquals(lastName, result.getResult().lastName);
+
+ // Verify the User object passed to sync had the correct values
+ ArgumentCaptor userCaptor = ArgumentCaptor.forClass(User.class);
+ verify(mockUsersApi).sync(userCaptor.capture());
+
+ User capturedUser = userCaptor.getValue();
+ assertEquals(userKey, capturedUser.getKey());
+ assertEquals(email, capturedUser.getEmail());
+ assertEquals(firstName, capturedUser.getFirstName());
+ assertEquals(lastName, capturedUser.getLastName());
+ }
+
+ @Test
+ @DisplayName("Should sync user with ABAC attributes")
+ void testSyncUserWithAttributes() throws IOException, PermitApiError, PermitContextError {
+ // Given
+ String userKey = "alice@example.com";
+ String email = "alice@example.com";
+ String firstName = "Alice";
+ String lastName = "Smith";
+
+ HashMap attributes = new HashMap();
+ attributes.put("department", "engineering");
+ attributes.put("clearance_level", 5);
+ attributes.put("is_manager", true);
+
+ UserRead expectedUser = new UserRead(userKey, "uuid-abc", "org-1", "proj-1", "env-1")
+ .withEmail(email)
+ .withFirstName(firstName)
+ .withLastName(lastName)
+ .withAttributes(attributes);
+ CreateOrUpdateResult expectedResult = new CreateOrUpdateResult(expectedUser, true);
+
+ when(mockUsersApi.sync(any(User.class))).thenReturn(expectedResult);
+
+ // When
+ CreateOrUpdateResult result = example.syncUserWithAttributes(
+ userKey, email, firstName, lastName, attributes
+ );
+
+ // Then
+ assertNotNull(result);
+ assertTrue(result.wasCreated());
+
+ UserRead syncedUser = result.getResult();
+ assertEquals("engineering", syncedUser.attributes.get("department"));
+ assertEquals(5, ((Number) syncedUser.attributes.get("clearance_level")).intValue());
+ assertTrue((Boolean) syncedUser.attributes.get("is_manager"));
+
+ // Verify the User object passed to sync had the correct attributes
+ ArgumentCaptor userCaptor = ArgumentCaptor.forClass(User.class);
+ verify(mockUsersApi).sync(userCaptor.capture());
+
+ User capturedUser = userCaptor.getValue();
+ assertEquals("engineering", capturedUser.getAttributes().get("department"));
+ assertEquals(5, capturedUser.getAttributes().get("clearance_level"));
+ assertTrue((Boolean) capturedUser.getAttributes().get("is_manager"));
+ }
+
+ @Test
+ @DisplayName("Should propagate IOException from sync service")
+ void testSyncSimpleUser_IOException() throws IOException, PermitApiError, PermitContextError {
+ // Given
+ when(mockUsersApi.sync(any(User.class)))
+ .thenThrow(new IOException("Network unreachable"));
+
+ // When/Then
+ IOException exception = assertThrows(IOException.class, new org.junit.jupiter.api.function.Executable() {
+ @Override
+ public void execute() throws Throwable {
+ example.syncSimpleUser("test-user");
+ }
+ });
+ assertEquals("Network unreachable", exception.getMessage());
+ }
+
+ @Test
+ @DisplayName("Should propagate PermitApiError from sync service")
+ void testSyncSimpleUser_PermitApiError() throws IOException, PermitApiError, PermitContextError {
+ // Given
+ when(mockUsersApi.sync(any(User.class)))
+ .thenThrow(new PermitApiError("Bad Request", 400, "{\"error\":\"Invalid user data\"}"));
+
+ // When/Then
+ PermitApiError exception = assertThrows(PermitApiError.class, new org.junit.jupiter.api.function.Executable() {
+ @Override
+ public void execute() throws Throwable {
+ example.syncSimpleUser("test-user");
+ }
+ });
+ assertEquals(400, exception.getResponseCode());
+ assertTrue(exception.getRawResponse().contains("Invalid user data"));
+ }
+
+ @Test
+ @DisplayName("Should propagate PermitContextError from sync service")
+ void testSyncSimpleUser_PermitContextError() throws IOException, PermitApiError, PermitContextError {
+ // Given
+ when(mockUsersApi.sync(any(User.class)))
+ .thenThrow(new PermitContextError("Environment context required"));
+
+ // When/Then
+ PermitContextError exception = assertThrows(PermitContextError.class, new org.junit.jupiter.api.function.Executable() {
+ @Override
+ public void execute() throws Throwable {
+ example.syncSimpleUser("test-user");
+ }
+ });
+ assertTrue(exception.getMessage().contains("Environment context required"));
+ }
+
+ @Test
+ @DisplayName("Should handle syncing multiple users")
+ void testSyncMultipleUsers() throws IOException, PermitApiError, PermitContextError {
+ // Given
+ UserRead user1 = new UserRead("user1", "id1", "org", "proj", "env");
+ UserRead user2 = new UserRead("user2", "id2", "org", "proj", "env");
+
+ when(mockUsersApi.sync(any(User.class)))
+ .thenReturn(new CreateOrUpdateResult(user1, true))
+ .thenReturn(new CreateOrUpdateResult(user2, false));
+
+ // When
+ CreateOrUpdateResult result1 = example.syncSimpleUser("user1");
+ CreateOrUpdateResult result2 = example.syncSimpleUser("user2");
+
+ // Then
+ assertTrue(result1.wasCreated(), "First user should be created");
+ assertFalse(result2.wasCreated(), "Second user should be updated");
+
+ verify(mockUsersApi, times(2)).sync(any(User.class));
+ }
+
+ @Test
+ @DisplayName("Should handle null attributes gracefully")
+ void testSyncUserWithAttributes_NullAttributes() throws IOException, PermitApiError, PermitContextError {
+ // Given
+ UserRead expectedUser = new UserRead("user1", "id1", "org", "proj", "env");
+ when(mockUsersApi.sync(any(User.class)))
+ .thenReturn(new CreateOrUpdateResult(expectedUser, true));
+
+ // When - passing null attributes
+ CreateOrUpdateResult result = example.syncUserWithAttributes(
+ "user1", "user1@example.com", "First", "Last", null
+ );
+
+ // Then
+ assertNotNull(result);
+ assertTrue(result.wasCreated());
+
+ ArgumentCaptor userCaptor = ArgumentCaptor.forClass(User.class);
+ verify(mockUsersApi).sync(userCaptor.capture());
+ assertNull(userCaptor.getValue().getAttributes());
+ }
+
+ @Test
+ @DisplayName("Should handle empty attributes")
+ void testSyncUserWithAttributes_EmptyAttributes() throws IOException, PermitApiError, PermitContextError {
+ // Given
+ HashMap emptyAttributes = new HashMap();
+ UserRead expectedUser = new UserRead("user1", "id1", "org", "proj", "env")
+ .withAttributes(emptyAttributes);
+ when(mockUsersApi.sync(any(User.class)))
+ .thenReturn(new CreateOrUpdateResult(expectedUser, true));
+
+ // When
+ CreateOrUpdateResult result = example.syncUserWithAttributes(
+ "user1", "user1@example.com", "First", "Last", emptyAttributes
+ );
+
+ // Then
+ assertNotNull(result);
+ assertTrue(result.wasCreated());
+
+ ArgumentCaptor userCaptor = ArgumentCaptor.forClass(User.class);
+ verify(mockUsersApi).sync(userCaptor.capture());
+ assertNotNull(userCaptor.getValue().getAttributes());
+ assertTrue(userCaptor.getValue().getAttributes().isEmpty());
+ }
+
+ @Test
+ @DisplayName("Should verify the actual example class can be instantiated with a real Permit instance")
+ void testUserSyncExample_CanBeInstantiated() {
+ try {
+ // Given
+ PermitConfig config = new PermitConfig.Builder("test-api-key")
+ .withPdpAddress("http://localhost:7766")
+ .build();
+ Permit permit = new Permit(config);
+
+ // When
+ UserSyncExample realExample = new UserSyncExample(permit);
+
+ // Then
+ assertNotNull(realExample, "Should be able to create example with real Permit instance");
+ } catch (UnsupportedClassVersionError e) {
+ // Skip test if there's a class version mismatch (e.g., running with incompatible JVM)
+ System.out.println("Skipping test due to class version incompatibility: " + e.getMessage());
+ } catch (NoClassDefFoundError e) {
+ // Skip test if there's a class version mismatch (e.g., running with incompatible JVM)
+ System.out.println("Skipping test due to class version incompatibility: " + e.getMessage());
+ }
+ }
+}