zynactradocs

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:

policy-tests/leave_request_test.yaml
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 Nest

requireLive() 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

  1. No subject: 401. A subject without the permission anywhere: 403.
  2. Allowed on a record in scope; denied (403, or 404 for reads) on one out of scope.
  3. The condition: denied when it fails (already approved, their own).
  4. Another org's record: never visible, never actionable.
  5. 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.
  6. Sensitive fields: present for those with the field permission, absent for everyone else.
  7. 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.

Edit on GitHub

On this page