Assertions - evansims/openfga-php GitHub Wiki

Assertions can be thought of as unit tests for your permission system. They let you define what should and shouldn't be allowed, then verify your authorization model works correctly before deploying it to production.


Prerequisites

Before working with assertions, ensure you have the following setup:

<?php

declare(strict_types=1);

use OpenFGA\Client;

$client = new Client(
    url: $_ENV['FGA_API_URL'] ?? 'http://localhost:8080',
);

$storeId = $_ENV['FGA_STORE_ID'];
$modelId = $_ENV['FGA_MODEL_ID'];


What are assertions

Assertions are test cases that specify expected outcomes for permission checks. Each assertion says "user X should (or shouldn't) have permission Y on resource Z" and verifies this against your authorization model.


Writing your first test

Let's say you're building a document management system. You want to test that owners can edit documents but viewers cannot:

use OpenFGA\Models\{Assertion, AssertionTupleKey};
use OpenFGA\Models\Collections\Assertions;

// Write your first test
$result = $client->writeAssertions(
    store: $storeId,
    model: $modelId,
    assertions: new Assertions(
        // Document owners can edit their documents
        new Assertion(
            tupleKey: new AssertionTupleKey('user:alice', 'can_edit', 'document:strategy'),
            expectation: true,
        ),
        // Users without any relationship cannot edit
        new Assertion(
            tupleKey: new AssertionTupleKey('user:bob', 'can_edit', 'document:strategy'),
            expectation: false,
        ),
        // Viewers cannot edit documents
        new Assertion(
            tupleKey: new AssertionTupleKey('user:charlie', 'can_edit', 'document:strategy'),
            expectation: false,
        ),
    ),
);

if ($result->succeeded()) {
    echo "✓ Assertions written successfully\n";
}


Testing permission inheritance

Complex authorization models often have inherited permissions. Test these relationships to ensure they work as expected:

$result = $client->writeAssertions(
    store: $storeId,
    model: $modelId,
    assertions: new Assertions(
        // Team members can access workspace documents
        new Assertion(
            tupleKey: new AssertionTupleKey('user:alice', 'can_view', 'document:q4-report'),
            expectation: true,
        ),
        // Non-team members cannot access workspace documents
        new Assertion(
            tupleKey: new AssertionTupleKey('user:external', 'can_view', 'document:q4-report'),
            expectation: false,
        ),
        // Workspace admins can delete documents
        new Assertion(
            tupleKey: new AssertionTupleKey('user:admin', 'can_delete', 'document:q4-report'),
            expectation: true,
        ),
    ),
);


Testing edge cases

Test boundary conditions and special cases in your permission model:

$edgeCases = $client->writeAssertions(
    store: $storeId,
    model: $modelId,
    assertions: new Assertions(
        // Users cannot edit their own profile indirectly through groups
        new Assertion(
            tupleKey: new AssertionTupleKey('user:alice', 'can_edit', 'profile:alice'),
            expectation: false,
        ),
        // Super admins can access all resources
        new Assertion(
            tupleKey: new AssertionTupleKey('user:superadmin', 'can_manage', 'system:settings'),
            expectation: true,
        ),
        // Deleted users lose all permissions
        new Assertion(
            tupleKey: new AssertionTupleKey('user:deleted_user', 'can_view', 'document:any'),
            expectation: false,
        ),
        // Service accounts have limited permissions
        new Assertion(
            tupleKey: new AssertionTupleKey('service:backup', 'can_read', 'database:production'),
            expectation: true,
        ),
        new Assertion(
            tupleKey: new AssertionTupleKey('service:backup', 'can_write', 'database:production'),
            expectation: false,
        ),
    ),
);


Managing test data

Organize your assertions logically and keep them maintainable:

use OpenFGA\Models\{Assertion, AssertionTupleKey};

final class DocumentPermissionTests
{
    public function __construct(
        private Client $client,
    ) {
    }

    public function getAssertions(): array
    {
        return [
            // Owner permissions
            new Assertion(
                tupleKey: new AssertionTupleKey(
                    user: 'user:document_owner',
                    relation: 'owner',
                    object: 'document:strategy_2024',
                ),
                expectation: true,
            ),
            new Assertion(
                tupleKey: new AssertionTupleKey(
                    user: 'user:document_owner',
                    relation: 'can_edit',
                    object: 'document:strategy_2024',
                ),
                expectation: true,
            ),
            new Assertion(
                tupleKey: new AssertionTupleKey(
                    user: 'user:document_owner',
                    relation: 'can_share',
                    object: 'document:strategy_2024',
                ),
                expectation: true,
            ),

            // Editor permissions
            new Assertion(
                tupleKey: new AssertionTupleKey(
                    user: 'user:editor',
                    relation: 'can_edit',
                    object: 'document:strategy_2024',
                ),
                expectation: true,
            ),
            new Assertion(
                tupleKey: new AssertionTupleKey(
                    user: 'user:editor',
                    relation: 'can_share',
                    object: 'document:strategy_2024',
                ),
                expectation: false, // Editors cannot share
            ),

            // Viewer permissions
            new Assertion(
                tupleKey: new AssertionTupleKey(
                    user: 'user:viewer',
                    relation: 'can_view',
                    object: 'document:strategy_2024',
                ),
                expectation: true,
            ),
            new Assertion(
                tupleKey: new AssertionTupleKey(
                    user: 'user:viewer',
                    relation: 'can_edit',
                    object: 'document:strategy_2024',
                ),
                expectation: false,
            ),
        ];
    }
}


Best practices

Start with critical paths: test the most important permission checks first - admin access, user data privacy, billing permissions.

Test both positive and negative cases: don't just test what should work, test what should be blocked.

Use realistic data: test with actual user IDs, resource names, and permission types from your application.

Update tests when models change: assertions should evolve with your authorization model. Treat them like any other test suite.

Validate before deployment: run assertions in your CI/CD pipeline to catch permission regressions before they reach production.

$assertions = $client->readAssertions()->unwrap();

echo "Current test assertions:\n";

foreach ($assertions->getAssertions() as $assertion) {
    $status = $assertion->getExpectation() ? '' : '';
    echo sprintf(
        "%s %s %s %s\n",
        $status,
        $assertion->getUser(),
        $assertion->getRelation(),
        $assertion->getObject(),
    );
}


CI/CD Integration

Integrate assertion testing into your deployment pipeline to catch permission regressions before they reach production.


GitHub Actions Example

# .github/workflows/authorization-tests.yml
name: Authorization Model Tests

on:
  push:
    paths:
      - "authorization-models/**"
      - "tests/authorization/**"
  pull_request:
    paths:
      - "authorization-models/**"
      - "tests/authorization/**"

jobs:
  test-authorization:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v4

      - name: Setup PHP
        uses: shivammathur/setup-php@v2
        with:
          php-version: "8.3"

      - name: Install dependencies
        run: composer install --no-dev --optimize-autoloader

      - name: Start OpenFGA Server
        run: |
          docker run -d --name openfga \
            -p 8080:8080 \
            openfga/openfga:latest \
            run --playground-enabled

      - name: Wait for OpenFGA
        run: |
          timeout 30 bash -c 'until curl -f http://localhost:8080/healthz; do sleep 1; done'

      - name: Run Authorization Tests
        run: php tests/authorization/run-assertions.php
        env:
          FGA_API_URL: http://localhost:8080

Test Runner Script

<?php

declare(strict_types=1);

use OpenFGA\Client;

use function OpenFGA\tuple;

final class AuthorizationTestRunner
{
    private ?Client $client = null;

    public function __construct(
        private string $apiUrl,
        private bool $verbose = false,
    ) {
    }

    public function runTests(string $storeId, string $modelId): bool
    {
        $this->log("Running authorization tests...\n");

        // Read and validate assertions
        $result = $this->getClient()->readAssertions(
            store: $storeId,
            model: $modelId,
        );

        if ($result->failed()) {
            $this->log('❌ Failed to read assertions: ' . $result->err()->getMessage());

            return false;
        }

        $assertions = $result->unwrap()->getAssertions();
        $this->log(sprintf("Found %d assertions to test\n", count($assertions)));

        $passed = 0;
        $failed = 0;

        foreach ($assertions as $assertion) {
            $checkResult = $this->getClient()->check(
                store: $storeId,
                model: $modelId,
                tupleKey: tuple(
                    user: $assertion->getUser(),
                    relation: $assertion->getRelation(),
                    object: $assertion->getObject(),
                ),
            );

            if ($checkResult->failed()) {
                $this->log(sprintf(
                    "❌ Error checking %s %s %s: %s\n",
                    $assertion->getUser(),
                    $assertion->getRelation(),
                    $assertion->getObject(),
                    $checkResult->err()->getMessage(),
                ));
                $failed++;

                continue;
            }

            $allowed = $checkResult->unwrap()->getAllowed();
            $expected = $assertion->getExpectation();

            if ($allowed === $expected) {
                $passed++;

                if ($this->verbose) {
                    $this->log(sprintf(
                        "✅ %s %s %s = %s (expected)\n",
                        $assertion->getUser(),
                        $assertion->getRelation(),
                        $assertion->getObject(),
                        $allowed ? 'allowed' : 'denied',
                    ));
                }
            } else {
                $failed++;
                $this->log(sprintf(
                    "❌ %s %s %s = %s (expected %s)\n",
                    $assertion->getUser(),
                    $assertion->getRelation(),
                    $assertion->getObject(),
                    $allowed ? 'allowed' : 'denied',
                    $expected ? 'allowed' : 'denied',
                ));
            }
        }

        $this->log(sprintf(
            "\nTest Results: %d passed, %d failed\n",
            $passed,
            $failed,
        ));

        return 0 === $failed;
    }

    private function getClient(): Client
    {
        if (null === $this->client) {
            $this->client = new Client(
                url: $this->apiUrl,
            );
        }

        return $this->client;
    }

    private function log(string $message): void
    {
        echo $message;
    }
}

// Usage in CI/CD
$runner = new AuthorizationTestRunner(
    apiUrl: $_ENV['OPENFGA_API_URL'] ?? 'http://localhost:8080',
    verbose: $_ENV['VERBOSE'] ?? false,
);

$success = $runner->runTests(
    storeId: $_ENV['FGA_STORE_ID'],
    modelId: $_ENV['FGA_MODEL_ID'],
);

exit($success ? 0 : 1);

Model Assertions File Example

<?php

declare(strict_types=1);

use OpenFGA\Models\{Assertion, AssertionTupleKey};
use OpenFGA\Models\Collections\Assertions;

// authorization-models/document-system.assertions.php

return new Assertions(
    // Document ownership tests
    new Assertion(
        tupleKey: new AssertionTupleKey(
            user: 'user:alice',
            relation: 'owner',
            object: 'document:1',
        ),
        expectation: true,
    ),
    new Assertion(
        tupleKey: new AssertionTupleKey(
            user: 'user:bob',
            relation: 'owner',
            object: 'document:1',
        ),
        expectation: false,
    ),

    // Inherited permissions tests
    new Assertion(
        tupleKey: new AssertionTupleKey(
            user: 'user:alice',
            relation: 'editor',
            object: 'document:1',
        ),
        expectation: true, // Owner can edit
    ),
    new Assertion(
        tupleKey: new AssertionTupleKey(
            user: 'user:alice',
            relation: 'viewer',
            object: 'document:1',
        ),
        expectation: true, // Owner can view
    ),

    // Team permissions tests
    new Assertion(
        tupleKey: new AssertionTupleKey(
            user: 'team:engineering#member',
            relation: 'viewer',
            object: 'document:roadmap',
        ),
        expectation: true,
    ),
    new Assertion(
        tupleKey: new AssertionTupleKey(
            user: 'team:marketing#member',
            relation: 'viewer',
            object: 'document:roadmap',
        ),
        expectation: false,
    ),

    // Conditional permissions tests
    new Assertion(
        tupleKey: new AssertionTupleKey(
            user: 'user:contractor',
            relation: 'viewer',
            object: 'document:sensitive',
        ),
        expectation: true,
        // Note: Conditional assertions require context to be set during testing
    ),
);

Integration with Testing Frameworks

PHPUnit Integration

<?php

declare(strict_types=1);

use OpenFGA\Client;
use PHPUnit\Framework\TestCase;

use function OpenFGA\{dsl, model, store, tuple};

final class AuthorizationModelTest extends TestCase
{
    private Client $client;

    private string $modelId;

    private string $storeId;

    protected function setUp(): void
    {
        $this->client = new Client(
            url: $_ENV['FGA_API_URL'] ?? 'http://localhost:8080',
        );

        // Create test store and model
        $this->storeId = store(
            name: 'test-' . uniqid(),
            client: $this->client,
        );

        $authModel = dsl(file_get_contents(__DIR__ . '/../../authorization-models/main.fga'));

        $this->modelId = model(
            model: $authModel,
            store: $this->storeId,
            client: $this->client,
        );
    }

    protected function tearDown(): void
    {
        // Clean up test store
        $this->client->deleteStore($this->storeId);
    }

    public function testDocumentPermissions(): void
    {
        $assertions = require __DIR__ . '/../../authorization-models/document-system.assertions.php';

        $result = $this->client->writeAssertions(
            store: $this->storeId,
            model: $this->modelId,
            assertions: $assertions,
        );

        $this->assertTrue($result->succeeded());

        // Verify the assertions by running checks
        $checks = $this->client->readAssertions(
            store: $this->storeId,
            model: $this->modelId,
        )->unwrap();

        foreach ($checks->getAssertions() as $assertion) {
            $checkResult = $this->client->check(
                store: $this->storeId,
                model: $this->modelId,
                tupleKey: tuple(
                    user: $assertion->getUser(),
                    relation: $assertion->getRelation(),
                    object: $assertion->getObject(),
                ),
            );

            $this->assertTrue($checkResult->succeeded());
            $this->assertEquals(
                $assertion->getExpectation(),
                $checkResult->unwrap()->getAllowed(),
                sprintf(
                    'Failed assertion: %s %s %s',
                    $assertion->getUser(),
                    $assertion->getRelation(),
                    $assertion->getObject(),
                ),
            );
        }
    }
}

Docker Compose for Local Testing

# docker-compose.test.yml
version: "3.8"

services:
  openfga:
    image: openfga/openfga:latest
    command: run --playground-enabled
    ports:
      - "8080:8080"
    environment:
      - OPENFGA_DATASTORE_ENGINE=memory

  php-tests:
    build: .
    depends_on:
      - openfga
    environment:
      - FGA_API_URL=http://openfga:8080
    volumes:
      - ./authorization-models:/app/authorization-models
      - ./tests:/app/tests
    command: |
      sh -c "
        composer install --no-interaction &&
        vendor/bin/phpunit tests/Unit/AuthorizationModelTest.php
      "

Run with: docker-compose -f docker-compose.test.yml up --build

Remember: assertions replace all existing tests for a model when you call writeAssertions(). Always include your complete test suite in each call.

⚠️ **GitHub.com Fallback** ⚠️