Skip to content

Testing

Melange validates compatibility with OpenFGA using their official test suite. This guide covers running tests, inspecting test cases, and debugging failures.

OpenFGA Test Suite

Running Tests

# Run all supported feature tests (recommended)
just test-openfga

# Run a specific feature category
just test-openfga-feature Wildcards
just test-openfga-feature Exclusion
just test-openfga-feature TupleToUserset

# Run a single test by name
just test-openfga-name wildcard_direct
just test-openfga-name computed_userset

# Run tests matching a pattern
just test-openfga-pattern "^exclusion"
just test-openfga-pattern "tuple_to_userset"

# List all available test names
just test-openfga-list

# Run without gotestfmt formatting
just test-openfga-verbose

# Run full suite (includes unsupported features - many will fail)
just test-openfga-full-check

Feature Test Categories

CategoryCommandTests
Direct Assignmentjust test-openfga-feature DirectAssignmentthis, this_with_contextual_tuples, this_and_union
Computed Usersetjust test-openfga-feature ComputedUsersetRole hierarchy via or
Tuple-to-Usersetjust test-openfga-feature TupleToUsersetParent inheritance via from
Wildcardsjust test-openfga-feature WildcardsPublic access via [user:*]
Exclusionjust test-openfga-feature Exclusionbut not patterns
Unionjust test-openfga-feature UnionMultiple or branches
Complex Patternsjust test-openfga-feature ComplexPatternsNested combinations
Cycle Handlingjust test-openfga-feature CycleHandlingCycle detection

Understanding Test Output

With gotestfmt, passing tests show green checkmarks:

๐Ÿ“ฆ github.com/pthm/melange/test/openfgatests
  โœ… TestOpenFGA_Wildcards (110ms)
  โœ… TestOpenFGA_Wildcards/wildcard_direct (80ms)
  โœ… TestOpenFGA_Wildcards/wildcard_direct/stage_0 (20ms)
  โœ… TestOpenFGA_Wildcards/wildcard_direct/stage_0/check_0 (0s)

Failed tests show red X marks with details:

  โŒ TestOpenFGA_Unsupported/some_test
      Error: expected: true, actual: false
      Messages: check user:alice:viewer on document:1

Generated differential testing

TestOpenFGA_GeneratedAlgebra generates small authorization models and compares the generated PostgreSQL check_permission function directly with a real in-process OpenFGA server. Expected decisions come from OpenFGA, not from another Melange API or imported fixture assertions. It runs with the normal just test-openfga command and the OpenFGA CI job; no oracle opt-in is required.

just test-openfga-generated

# Explore a different deterministic sequence, or replay a reported seed.
MELANGE_ALGEBRA_SEED=123 MELANGE_ALGEBRA_MODELS=30 just test-openfga-generated

# Restrict a replay to one tuple mask printed in a failure.
MELANGE_ALGEBRA_SEED=123 MELANGE_ALGEBRA_MODELS=1 MELANGE_ALGEBRA_MASK=82 \
  just test-openfga-generated

Each model contains aliases, unions, intersections, exclusions, and parent inheritance. Earlier expressions can occur on either side of later operators, including inside an exclusion’s subtract branch. OpenFGA’s validator determines which generated models are valid. Rejected candidates are logged and replaced; the test fails if it cannot reach its requested count of valid models. A Melange compilation error or evaluator error on an accepted model always fails.

The default run evaluates 12 valid models. For each, it enumerates all 128 subsets of seven possible tuples: three direct grants on a document, three on a folder, and a parent link. Assignments use disconnected objects so they can be loaded together. Every generated user-valued relation is checked for both the grant recipient and a user with no grants, including objects with no tuples. Failures print the model, tuple assignment, query, and both decisions.

For mutation-based exploration and automatic input shrinking, use Go fuzzing:

cd test
go test ./openfgatests -run '^$' -fuzz '^FuzzAuthorizationAlgebra$' \
  -fuzztime 5m -parallel 1 -timeout 10m

The fuzz target varies model choices and a tuple-assignment mask. Invalid models are excluded using OpenFGA validation; valid counterexamples fail and are saved by Go under testdata/fuzz/FuzzAuthorizationAlgebra with a replay command. Use one worker to bound the number of disposable databases. Both test modes require Docker and are skipped by -short.

This is exhaustive over the tuple subsets for each generated model, not over all authorization models. The initial domain uses acyclic named relations, one parent object, and concrete users. Inline expression variants, recursive inheritance, multiple parents, userset subjects, wildcards, conditions, and list-operation equivalence are outside this generator’s scope.

Inspecting Test Cases

Use the dumptest utility to understand what a test does:

# Build the dumptest utility
just build-dumptest

# List all available test names (148 tests)
just dump-openfga-list

# Dump a specific test to see its model, tuples, and assertions
just dump-openfga userset_defines_itself_1

# Dump tests matching a pattern
just dump-openfga-pattern "^wildcard"
just dump-openfga-pattern "computed_userset|ttu_"

# Dump all tests (warning: very long output)
just dump-openfga-all

Example output:

Test: userset_defines_itself_1
------------------------------

=== Stage 1 ===

Model:
```fga
model
  schema 1.1
type user
type document
  relations
    define viewer: [user]
```

Tuples: (none)

Check Assertions:
  [1] ALLOW: document:1#viewer | viewer | document:1
  [2] DENY: document:2#viewer | viewer | document:1

ListObjects Assertions:
  [1] user=document:1#viewer relation=viewer type=document
      => [document:1]

ListUsers Assertions:
  [1] object=document:1 relation=viewer filters=[document#viewer]
      => [document:1#viewer]

This is useful for:

  • Understanding what a failing test expects
  • Learning OpenFGA patterns by example
  • Debugging why a specific assertion fails

Unit Testing with Decision Overrides

For unit tests in your own code, use decision overrides to mock permission results:

Always Allow

func TestCreateDocument_Allowed(t *testing.T) {
    // Checker always returns true, no database required
    checker := melange.NewChecker(nil, melange.WithDecision(melange.DecisionAllow))

    ok, err := checker.Check(ctx, user, "can_write", doc)
    require.NoError(t, err)
    require.True(t, ok)
}

Always Deny

func TestCreateDocument_Denied(t *testing.T) {
    // Checker always returns false, no database required
    checker := melange.NewChecker(nil, melange.WithDecision(melange.DecisionDeny))

    ok, err := checker.Check(ctx, user, "can_write", doc)
    require.NoError(t, err)
    require.False(t, ok)
}

Context-Based Overrides

For middleware or handler tests:

func TestHandler(t *testing.T) {
    checker := melange.NewChecker(nil, melange.WithContextDecision())
    handler := NewHandler(checker)

    t.Run("authorized request", func(t *testing.T) {
        ctx := melange.WithDecisionContext(context.Background(), melange.DecisionAllow)
        req := httptest.NewRequest("GET", "/docs/1", nil).WithContext(ctx)
        rec := httptest.NewRecorder()

        handler.GetDocument(rec, req)
        require.Equal(t, http.StatusOK, rec.Code)
    })

    t.Run("unauthorized request", func(t *testing.T) {
        ctx := melange.WithDecisionContext(context.Background(), melange.DecisionDeny)
        req := httptest.NewRequest("GET", "/docs/1", nil).WithContext(ctx)
        rec := httptest.NewRecorder()

        handler.GetDocument(rec, req)
        require.Equal(t, http.StatusForbidden, rec.Code)
    })
}

Integration Testing

For full integration tests with a real PostgreSQL database:

import (
    "github.com/pthm/melange/melange"
    "github.com/pthm/melange/pkg/migrator"
    "github.com/testcontainers/testcontainers-go/modules/postgres"
)

func setupTestDB(t *testing.T) *sql.DB {
    ctx := context.Background()

    container, err := postgres.Run(ctx, "postgres:17-alpine",
        postgres.WithDatabase("test"),
    )
    require.NoError(t, err)
    t.Cleanup(func() { container.Terminate(ctx) })

    connStr, err := container.ConnectionString(ctx, "sslmode=disable")
    require.NoError(t, err)

    db, err := sql.Open("postgres", connStr)
    require.NoError(t, err)
    t.Cleanup(func() { db.Close() })

    return db
}

func TestPermissions(t *testing.T) {
    ctx := context.Background()
    db := setupTestDB(t)

    // Apply Melange schema
    err := migrator.MigrateFromString(ctx, db, `
model
  schema 1.1

type user
type document
  relations
    define owner: [user]
    define viewer: [user] or owner
`)
    require.NoError(t, err)

    // Create melange_tuples view
    _, err = db.ExecContext(ctx, `
        CREATE TABLE documents (id TEXT PRIMARY KEY, owner_id TEXT);
        CREATE VIEW melange_tuples AS
        SELECT 'user' AS subject_type, owner_id AS subject_id,
               'owner' AS relation, 'document' AS object_type, id AS object_id
        FROM documents;
    `)
    require.NoError(t, err)

    // Insert test data
    _, err = db.ExecContext(ctx, `INSERT INTO documents VALUES ('doc1', 'alice')`)
    require.NoError(t, err)

    // Test permissions
    checker := melange.NewChecker(db)

    alice := melange.Object{Type: "user", ID: "alice"}
    bob := melange.Object{Type: "user", ID: "bob"}
    doc := melange.Object{Type: "document", ID: "doc1"}

    ok, err := checker.Check(ctx, alice, "viewer", doc)
    require.NoError(t, err)
    require.True(t, ok, "alice should be viewer as owner")

    ok, err = checker.Check(ctx, bob, "viewer", doc)
    require.NoError(t, err)
    require.False(t, ok, "bob should not be viewer")
}

Debugging Failed Tests

Enable SQL Logging

type loggingDB struct {
    *sql.DB
    t *testing.T
}

func (l *loggingDB) QueryRowContext(ctx context.Context, query string, args ...any) *sql.Row {
    l.t.Logf("SQL: %s\nArgs: %v", query, args)
    return l.DB.QueryRowContext(ctx, query, args...)
}

Inspect Database State

-- View all tuples
SELECT * FROM melange_tuples ORDER BY object_type, object_id;

-- Manual permission check
SELECT check_permission('user', 'alice', 'can_read', 'document', 'doc1');

Test Commands Reference

# OpenFGA test suite
just test-openfga              # Supported features with gotestfmt
just test-openfga-feature X    # Single feature category
just test-openfga-name X       # Single test by name
just test-openfga-pattern X    # Tests matching regex
just test-openfga-list         # List all test names
just test-openfga-verbose      # Without gotestfmt
just test-openfga-full-check   # Full suite (many failures expected)

# OpenFGA test inspection
just dump-openfga-list         # List all test names
just dump-openfga X            # Dump a specific test by name
just dump-openfga-pattern X    # Dump tests matching regex
just dump-openfga-all          # Dump all tests

# Tools
just install-gotestfmt         # Install gotestfmt formatter
just build-dumptest            # Build the dumptest utility