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 = trueDefine 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.
| Section | Required 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)