Testing
Policy tests with Cerbos, service tests in strict mode against real Postgres and Cerbos, and the cases every endpoint needs.
Three layers. All of them run the real Cerbos with the generated policies; none of them mocks a decision.
1. Policy tests
zaccess gen scaffolds <kind>_test.yaml once per new kind in the tests directory; after that the file is yours. zaccess test refuses to pass while any action with a condition appears in no test, then runs cerbos compile --tests.
Principals carry grants exactly as the SDKs build them, with every scope key present for every permission:
name: LeaveRequestSuite
principals:
manager:
id: u_mgr
roles: [user]
attr:
orgId: org_a
reportingPath: /u_ceo/u_mgr/
perms:
"leave.request:approve": { global: false, self: false, scopes: { team: true, org_unit: [] } }
resources:
emp_pending:
id: lr_1
kind: leave.request
attr: { orgId: org_a, ownerId: u_emp, ownerPath: /u_ceo/u_mgr/u_emp/, orgUnitId: fin, status: pending, days: 2 }
emp_approved:
id: lr_2
kind: leave.request
attr: { orgId: org_a, ownerId: u_emp, ownerPath: /u_ceo/u_mgr/u_emp/, orgUnitId: fin, status: approved, days: 2 }
lookalike: # /u_ceo/u_mgrX/ is NOT under /u_ceo/u_mgr/
id: lr_3
kind: leave.request
attr: { orgId: org_a, ownerId: u_other, ownerPath: /u_ceo/u_mgrX/u_other/, orgUnitId: it, status: pending, days: 1 }
other_org:
id: lr_4
kind: leave.request
attr: { orgId: org_b, ownerId: u_emp, ownerPath: /u_ceo/u_mgr/u_emp/, orgUnitId: fin, status: pending, days: 2 }
tests:
- name: Managers approve pending leave in their team, nothing else
input: { principals: [manager], resources: [emp_pending, emp_approved, lookalike, other_org], actions: [approve] }
expected:
- { principal: manager, resource: emp_pending, actions: { approve: EFFECT_ALLOW } }
- { principal: manager, resource: emp_approved, actions: { approve: EFFECT_DENY } }
- { principal: manager, resource: lookalike, actions: { approve: EFFECT_DENY } }
- { principal: manager, resource: other_org, actions: { approve: EFFECT_DENY } }For each condition, cover: allowed; denied by the condition; denied by scope (including a look-alike path); another org. For org rules, add scope: <org> to the resources.
2. Service tests
Run the service's handlers against real Postgres and Cerbos, with strict mode on.
func TestLeave(t *testing.T) {
cerbos := accesstest.StartCerbos(t, "../access/generated/policies") // skips without cerbos, unless ACCESS_REQUIRE_LIVE=1
db, _ := accesstest.NewDB(t) // a fresh, migrated database (and its URL), dropped after
a, err := access.New(access.Config{Manifest: accessgen.Manifest, Cerbos: cerbos.Client, Store: sqlstore.New(db), Resolver: resolver, Strict: true})
// ... routes with accesshttp.Guard{Access: a, Strict: true}, then httptest requests
}accesstest.StaticResolver and accesstest.MemStore cover unit tests that don't need a database.
import { requireLive, startCerbos, staticResolver } from "@zynactra/access/testing";
const cerbos = await startCerbos("access/generated/policies"); // { url, client, stop }
const access = new Access({ manifest, cerbos: cerbos.client, store, resolver, strict: true });
// AccessModule.forRoot({ ..., strict: true }) for NestrequireLive() says whether missing Postgres or Cerbos should fail the suite instead of skipping it; memStore and staticResolver cover unit tests.
Set ACCESS_TEST_DATABASE_URL (a Postgres you can create databases on) and CERBOS_BIN (or have cerbos on PATH). In CI set ACCESS_REQUIRE_LIVE=1, so the tests fail instead of skipping when either is missing.
The cases every endpoint needs
- No subject: 401. A subject without the permission anywhere: 403.
- Allowed on a record in scope; denied (403, or 404 for reads) on one out of scope.
- The condition: denied when it fails (already approved, their own).
- Another org's record: never visible, never actionable.
- Lists: the exact set of IDs for an employee, a manager and an org-unit holder. Include a look-alike path with an underscore (
/u_ceo/uX1/next to/u_ceo/u_1/), so missing LIKE escaping fails the test. - Sensitive fields: present for those with the field permission, absent for everyone else.
- Grant changes take effect: assign through the admin API, then the next request sees it.
examples/go-app/main_test.go and examples/nest-app/test/e2e.test.ts are complete versions of this list.
3. UI tests
fakeAccessApi from @zynactra/access-react/testing answers like the access API, so components using useCan, <Can> or the admin panels can be tested without a server. Assert on what's rendered and on api.calls. See React.
Conformance, inside zynactra/access
conformance/ in the repo holds JSON fixtures (grants, plans, escalation, decisions, admin scenarios) that both SDKs run. A behaviour change in one SDK fails the other's run until it matches, which is how the two stay identical.