Skip to content
ReferenceCommand line

rent permissions test

Usage

rent permissions test
rent permissions test schema/team.permissions.toml --json
rent permissions test --schema schema --database-url "$EMPTY_TEST_DATABASE_URL"

The command discovers *.permissions.toml beside the configured schema, or takes one file or directory. Every file gets its own in-memory SQLite database. It uses the ordinary RSL catalog and runtime for grants, checks, lists and reverse inspection.

--database-url is an explicit alternative for one fixture. It must name an empty dedicated test database. Rent refuses existing tables, views or application enums before changing anything. The resulting fixture database is retained for inspection. The command never uses the application's configured or environment database URL implicitly.

PostgreSQL indexes in SQLite fixtures

Since v0.4.0, SQLite fixture setup omits non-unique performance indexes with a backend-specific access method, operator class or included columns, such as a PostgreSQL GIN trigram index. Each omission appears as WARN in text output or an entry in the JSON warnings array. Production migration planning is unchanged.

Primary keys, foreign keys, uniqueness and other constraints are retained. An unsupported unique index is rejected with an instruction to use --database-url. It is never silently dropped. PostgreSQL-specific expression or constraint semantics still need native-backend tests. Use an empty dedicated PostgreSQL database with --database-url to verify the real indexes and backend behavior. A SQLite permission fixture does not establish PostgreSQL schema or query-plan correctness.

Fixture format

version = 1

[viewers]
maya = { subject = "User:maya", tenant = "acme" }

[[rows.User]]
id = "maya"
tenant_id = "acme"

[[check]]
name = "Maya can view her post"
viewer = "maya"
can = "Post.viewer"
on = "notes"
expect = true

Define all the models in the configured RSL and seed the referenced resources with rows. Rows use RSL field names and stored enum values. Seed data is trusted test setup: it does not run application write hooks or creator stamping. Database defaults and constraints still apply. Rows are inserted in foreign-key order. Since v0.4.0, nullable circular references are assembled in two steps, as described below. Use generated-client Rust tests for mutation hooks, application write behavior and concurrency.

Circular references in seed data

An account can point to its root workspace while that workspace points back to the account. Write the complete final values in rows. No separate linking section is needed:

[[rows.Account]]
tenant_id = "acme"
root_workspace_id = "root"

[[rows.Workspace]]
tenant_id = "acme"
id = "root"
manager_id = "maya"

This excerpt assumes an existing seeded manager and matching RSL relationships. Define the account and workspace relationships in your application schema.

When a cycle prevents insertion, the loader temporarily sets an eligible nullable foreign-key component to SQL NULL, inserts the rows, then restores the declared link using the row's complete primary key. Tenant columns, primary-key columns, generated columns and columns referenced by other foreign keys are never temporarily changed. A row that needs restoration must supply every primary-key component explicitly. Self-references to the same inserted row need no temporary change.

All seed-row inserts and link restorations run in one transaction, with foreign-key, unique and check constraints enforced throughout. A failed insert or restoration rolls back every seed row, and no permission assertions run against incomplete state. Schema creation is a separate step. An explicit dedicated database can retain empty tables after a failure. Direct permission grants are seeded after the row transaction commits.

Cycles with no eligible nullable component are rejected with an actionable error. The loader does not defer database constraints or change the schema to make a fixture pass. Backend-specific checks or triggers can also reject an intermediate nullable state. Use generated-client Rust setup when the schema requires a different construction strategy.

Client-maintained timestamps

@updatedAt runs in the generated client, not in the database or the raw fixture seeder. If a model has updated_at DateTime @updatedAt without a database default, supply a fixed RFC3339 value in every seed row:

[[rows.User]]
id = "maya"
tenant_id = "acme"
updated_at = "2026-01-01T00:00:00Z"

Use the RSL field name even when it has @map. This also applies to fields inherited from a timestamp mixin. Explicit timestamps are preserved. Fixtures do not replace them with the current clock. A missing required timestamp reports the model, row, and field and asks for an explicit RFC3339 value. Nullable timestamp fields can be omitted, and fields with a database default, such as @default(now()), still use that default.

SectionRequired expectation fields
[[check]]name, viewer, can = "Model.rule", on, and either expect or error.
[[list]]name, viewer, model, can = "rule", expect = [keys].
[[who]]name, viewer (the inspector), can, on, subject model, expect = [keys], complete.
[[grant]]Requires can, on, and subject = "Model:key". Optionally accepts userset and RFC3339 expires_at.

Scalar keys use their declared value type. Compound keys use TOML objects containing every primary-key field, such as on = { tenant_id = "acme", id = "notes" }. Viewer subjects and groups use Model:key strings. Put a complete JSON object after the colon for compound keys.

Viewers support subject, tenant, groups, admin, on_behalf_of, represented_groups, and represented_admin. Represented claims require on_behalf_of. Tenant scope can be inferred from the actor's complete key, never from a requested resource.

Checks can expect error = "tenant_mismatch", "missing_tenant", "evaluation_depth_limit", or "evaluation_work_limit" instead of a Boolean. Lists compare complete identity sets independently of row order. Reverse checks compare both stored identity sets and completeness. External group membership can make complete = false even when the expected stored subjects match.

Results and boundaries

Every executed expectation prints a result, and any mismatch makes the command fail. --json prints fixture counts and per-case results, including failures. Unknown fields, unsupported versions, duplicate names, empty fixtures and unrecognized procedure sections are errors, never skipped tests.

Application-installed Rust privacy policies and codecs require the configured generated client. Declared privacy factories and unsupported field codecs fail explicitly. Recursive checks, lists and reverse inspection use the normal bounded runtime. A fixture does not replace generated-client mutation and concurrency coverage.

See the complete runnable example.

A successful run ends with a count of the expectations it executed:

12 passed, 0 failed across 1 fixture(s)