test(audit): move audit middleware logic to unit tests, one integration suite for live-stack cases - #6952
Conversation
POC for discussion — shifts audit coverage off per-endpoint integration tests: - auditWiring.ts: tag markers + a router-stack walker + checkers that assert every audit handler is installed after auth and before authz, and that every defined audit spec is wired to a route. - auditWiring.unit.test.ts / auditable.unit.test.ts: prove the checker bites and the middleware logic (event shape, redaction, outcome, resolve-before-next) unit-tests with no stack. - auditWiring.integration.test.ts: the same checks over the real router table (one test replacing the per-endpoint denial tests). - Minimal tagging on auditable()/can()/withScope(); apiAuth/webAuth exported. See proposals/audit-testing-strategy.md. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Move the pure-logic cases (deleted connection, connection update, env variables, webhook URLs) to auditable.unit.test.ts and drop the denied-role-change case (now covered by the structural wiring check). Keep only the three cases that need the live stack: target/metadata resolved from a real controller mutation (pre-change role, removed-member email) or a real authz rejection (cross-account). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
There was a problem hiding this comment.
All reported issues were addressed
Reply with feedback, questions, or to request a fix.
Re-trigger cubic
…live-stack cases Revert the structural-check machinery and middleware tagging. Instead: - auditable.unit.test.ts: event shape, redaction, actor resolution, outcome mapping, the disabled-account gate, resolve-before-next — off-stack, no containers. - audit.integration.test.ts: one suite for the cases that need the live stack — wiring order (a denied request is still recorded, probed per wiring shape), resolve-before-next against a real controller mutation, cross-account no-leak. - Delete audit.private/audit.public integration files (10 tests -> 5 in one integration file + 7 unit). Measured on the merged first set: integration wall-clock 50.77s -> 32.84s. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
|
||
| // Unit-level coverage of the middleware's behavior — the part the per-endpoint integration tests were | ||
| // really asserting (event shape, redaction, outcome, resolve-before-next). No server, no auth stack, | ||
| // no database, no ClickHouse: the audit client is mocked and a fake req/res drives the same code path. |
There was a problem hiding this comment.
not sure this comment adds much value
There was a problem hiding this comment.
Removed in c3c3f5f — agreed, it was narration.
| expect(JSON.stringify(event)).not.toContain('shh-secret'); | ||
| }); | ||
|
|
||
| it('maps a 4xx response to a denied outcome', async () => { |
There was a problem hiding this comment.
nit: this is only testing 403. Description is a bit of an overstatement
There was a problem hiding this comment.
Fixed in c3c3f5f — it now covers both mappings (403 → denied, 5xx → failure) and the name reflects that.
| resource: 'connection', | ||
| action: 'deleted', | ||
| outcome: 'success', | ||
| accountId: account.id, |
There was a problem hiding this comment.
nit: new test don't seem to assert the accountId
There was a problem hiding this comment.
Good catch — the migrated tests dropped it. Added accountId (and environment) back to the unit tests in c3c3f5f.
| await vi.waitFor(() => { | ||
| expect(auditSpy).toHaveBeenCalled(); | ||
| }); | ||
| expect(auditSpy.mock.calls[0]?.[0]).toMatchObject({ resource: 'member', action: 'role_changed', outcome: 'denied' }); |
There was a problem hiding this comment.
should common attribute like environment be asserted?
There was a problem hiding this comment.
Added in c3c3f5f — the deny probes now assert accountId + environment (null for account-scoped role_changed, {id,display} for the env-scoped connection event) + actor, and the same on the resolve-before-next / cross-account cases.
…common attrs - Drop the narration header comment in auditable.unit.test.ts. - Broaden the outcome test to cover 403->denied and 5xx->failure (was labelled 4xx but only tested 403). - Assert accountId + environment on the migrated/consolidated tests (parity with the deleted per-endpoint tests). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Production code is untouched (middleware/routes are unchanged) — this is a test-only reshape of how the audit middleware is covered.
next— intoauditable.unit.test.ts: a container-free unit suite driving the realauditable()handler with a fake req/res and the audit client mocked.audit.integration.test.ts: wiring order (a denied request is still recorded — proves audit runs before authorization; probed once per wiring shape, publicwithScope+ privatecan), resolve-before-next against a real controller mutation (pre-change role, removed-member email), and a real authorization rejection that must not leak a cross-account email.audit.private/audit.publicintegration files. For the merged first set: 10 integration tests in 2 files → 5 in one integration file + 7 unit tests.Measured (same machine + configs)
Integration wall-clock drops ~18s (−35%), driven almost entirely by removing one file's ~19s module-graph import (container boot is one-time and shared). The saving scales with integration files removed — the same pattern applies to the in-flight audit PRs (~6 audit integration files today), where consolidating toward one suite is the bulk of the win.
What stays integration, and why
Only where a fake can't honestly reproduce the behaviour: a real controller that mutates the row it audits (the pre-change role / removed-member email are only knowable if resolved before the handler ran), and a real authz rejection. Everything else is logic and moves to unit.
Test plan