How to Test Audit Logs and Compliance Trails
Short answer
Compliance fails when privileged actions leave no immutable audit row, logs omit actor or target ids, retention deletes too early, or tamper attempts succeed—not when an admin page lists events. Probe audit entries within seconds of each sensitive action, map audit_event_type scenarios to SOC2/PCI control matrices via requirement traceability, and expand actor_role coverage with TrueCoverage and /testchimp evolve.
Part of Testing Guides by industry.
Who this is for
Teams subject to SOC2, PCI-DSS, HIPAA audit controls, or enterprise procurement requiring who-did-what-when trails for admin actions, privilege changes, data exports, logins, and configuration updates.
Why testing audit trails matters
"If it's not in the audit log, compliance fails" is literal in audits:
- Missing privilege escalation log — admin role granted without record; SOC2 Type II finding.
- Incomplete actor attribution — service account vs human unclear; incident response blocked.
- Mutable logs — admin deletes audit rows; destroys trust in trail.
- Retention violation — logs purged before 1-year policy; regulatory breach.
- DSAR actions unlogged — GDPR export/delete without audit; pairs with GDPR guide.
E2E proves integration: action → log row appears with correct audit_event_type, actor_id, target_id, timestamp—not exhaustive log schema unit tests alone.
Complexity map
| Scenario | Edge case | Why tests break | Approach |
|---|---|---|---|
| Missing event | Admin delete user | No log row | Probe within 5s |
| Wrong actor | Impersonation | Actor mismatch | Probe actor_id |
| Service account | Automated job | Human expected | Probe actor_type |
| Bulk action | Single aggregated log | Missing detail | Probe payload ids |
| Failed action | Login fail | Should still log | Probe failure event |
| Log tamper | Delete audit row | Should fail | API negative 403 |
| Retention | Old row gone | Too early | clock + probe exists |
| Cross-tenant | Tenant A action | Wrong tenant_id | Probe scope |
| Break-glass | Emergency access | Must log reason | Probe reason field |
| Export DSAR | Large job | Missing start/end | Probe dsar events |
| Config change | Feature flag | Silent change | Probe config diff |
| Clock skew | Timestamp order | Audit doubt | NTP + probe monotonic |
Probe pattern after sensitive action
const admin = await request.post('/api/test/seed-admin', {
data: { runId, adminId: `admin-${runId}` },
}).then(r => r.json());
await loginAs(page, admin.token);
await page.goto('/admin/users');
await page.getByRole('button', { name: 'Delete user' }).click();
await page.getByRole('button', { name: 'Confirm' }).click();
await expect.poll(async () => {
const logs = await request.get(
`/api/test/audit?runId=${runId}&event= user_deleted`
).then(r => r.json());
return logs.entries.length;
}, { timeout: 10_000 }).toBeGreaterThan(0);
const entry = logs.entries[0];
expect(entry.actorId).toBe(admin.adminId);
expect(entry.targetUserId).toBe(`user-${runId}`);
expect(entry.tenantId).toBe(admin.tenantId);
expect(entry.immutable).toBe(true);
Tune event names to your schema (user.deleted, USER_DELETE, etc.).
Privilege change (SOC2 CC6)
await page.getByLabel('Role').selectOption('super_admin');
await page.getByRole('button', { name: 'Save' }).click();
const logs = await request.get(`/api/test/audit?runId=${runId}&event=role_changed`).then(r => r.json());
expect(logs.entries[0].metadata.previousRole).toBe('admin');
expect(logs.entries[0].metadata.newRole).toBe('super_admin');
Map to control ids in markdown test plans with // @Scenario: CC6.3-role-change.
Tamper resistance negative
const deleteAudit = await request.delete(`/api/test/audit/entry/${entryId}`);
expect(deleteAudit.status()).toBe(403);
const still = await request.get(`/api/test/audit/entry/${entryId}`);
expect(still.status()).toBe(200);
Requirement traceability matrix
Build compliance matrix in markdown:
| Control | audit_event_type | SmartTest scenario id |
|---|---|---|
| CC6.2 | role_changed | CC6-ROLE-01 |
| CC7.2 | user_deleted | CC7-DEL-01 |
| Art.15 | dsar_export_completed | GDPR-EXP-01 |
Link SmartTests with // @Scenario: comments—auditors trace automated proof to controls (requirement traceability).
Requirement slices to cover
audit_event_type— role_changed, user_deleted, login_failed, dsar_export, config_updatedactor_role— admin, support, system, break_glass
Run /testchimp evolve when prod shows privilege change events but tests lack role_changed coverage.
CI checklist
- Probe audit after every sensitive admin spec in same test
- Actor and target ids match Arrange seeds
- Failed login audit spec (no credential in log payload)
- Tamper delete audit returns 403
- DSAR export/delete paired with GDPR specs
- No PII beyond ids in audit probe responses in traces
Anti-patterns
| Anti-pattern | Why it fails | Better approach |
|---|---|---|
| UI audit table only | UI not source | Probe API |
| wait 30s fixed | Slow/flaky | expect.poll 5-10s |
| Skip failed login | SOC2 gap | failure event probe |
| Log password fields | PCI violation | Redact in app + test |
| One admin action | Matrix incomplete | Traceability map |
| Mutable test logs | False confidence | immutability negative |
Example scenario
Situation: Support agent grants super_admin to user; auditor expects immutable role_changed entry.
Expected outcome: Audit row with actor support id, target user, previous/new roles within 5s.
Why UI-only automation breaks: UI toast success but audit empty—SOC2 observation.
- Arrange: Seed support agent and target user with role admin.
- Act: Support changes role to super_admin in admin console.
- Assert: Probe role_changed entry fields; tamper delete audit 403; user probe has new role.
TestChimp workflow: Compare audit_event_type × actor_role prod vs test; evolve missing privilege events.
Same Arrange/Act/Assert pattern as expired-coupon checkout.
Connect scenarios to your QA workflow
Capture business rules in markdown test plans and enforce them with seed routes and probe Assert. Link SmartTests with // @Scenario: for requirement traceability. Use /testchimp test on PRs; /testchimp explore on SmartTest paths for non-functional gaps (ExploreChimp).
Related scenarios
- GDPR privacy — DSAR audit events
- RBAC permissions — role matrices
- Fintech web apps — PCI contexts
- Healthcare portals — break-glass audit
- Data grids — bulk action logs
External references
Frequently asked questions
Which audit_event_types must have scenario coverage for SOC2?
Build matrix mapping controls to events—role_changed, user_deleted, login_failed, dsar_export, config_updated. Link // @Scenario: in SmartTests to markdown plans; use TrueCoverage audit_event_type × actor_role to find gaps—run /testchimp evolve.
How quickly should audit entries appear?
Poll probe within 5-10s of action—async log pipelines should still meet near-real-time compliance expectations in test env.
Should failed actions be audited?
Yes for auth and privilege attempts—probe login_failed and unauthorized_access without storing passwords in log payload.
How do I test log immutability?
Attempt DELETE on audit entry via API as non-system user—expect 403; entry still readable.
How do bulk actions appear in audit?
Probe either single bulk event with id list metadata or one event per target—match product contract; assert not silent.
How does GDPR DSAR relate?
Export/delete specs must probe dsar_export_requested and account_deleted audit events—pair with GDPR guide completeness tests.
Can TrueCoverage prioritize admin events?
Compare prod audit_event_type frequency to test coverage—evolve SmartTests for high-risk undertested events like role_changed or break_glass.
Apply these patterns in your repo
Run `/testchimp init` to connect TestChimp to your repo, then `/testchimp test` on PRs to turn these patterns into maintained SmartTests. Use `/testchimp evolve` when you want to expand coverage as your app grows.