Compliance audits are broken. The standard approach — annual assessments, questionnaires, PDF evidence packages, and a compliance consultant billing by the hour — creates a snapshot of your security posture on one day of the year. It tells you nothing about what your systems looked like for the other 364 days.
Compliance-as-Code is the practice of encoding compliance requirements as automated tests that run continuously in CI. Instead of proving compliance with a point-in-time snapshot, you prove it with an unbroken history of automated test results that auditors can review directly.
This post shows how to translate key GDPR and SOC 2 controls into executable test specifications.
The Compliance-as-Code Philosophy
TRADITIONAL COMPLIANCE: COMPLIANCE-AS-CODE:
Annual audit Continuous testing
│ │
▼ ▼
Questionnaire vs. Automated test suite
│ │
▼ ▼
PDF evidence CI artifacts
│ │
▼ ▼
Point-in-time proof Continuous proofThe output of compliance-as-code is not a PDF. It is a time-series of CI build results that auditors can inspect to see that every required control was verified on every deployment.
GDPR: Encoding Data Subject Rights
Test: Right to Erasure (Right to Be Forgotten)
GDPR Article 17 requires you to delete a user's personal data upon request. An automated test verifies this works correctly and completely:
// tests/compliance/gdpr-erasure.spec.ts
import { test, expect } from '@playwright/test';
import { createUser } from '../factories';
import { db } from '@/lib/db';
test.describe('GDPR: Right to Erasure', () => {
test('deleting a user removes all personal data from the database', async () => {
const { user } = await createUser({ email: 'erasure-test@example.com' });
// Trigger account deletion via the API
const response = await fetch(`/api/users/${user.id}/delete`, {
method: 'DELETE',
headers: { Authorization: `Bearer ${process.env.TEST_ADMIN_TOKEN}` },
});
expect(response.status).toBe(200);
// Verify all PII is removed from users table
const userRecord = await db.query('SELECT * FROM users WHERE id = $1', [user.id]);
expect(userRecord.rows).toHaveLength(0);
// Verify email is not in any related tables
const emailCheck = await db.query(
'SELECT COUNT(*) FROM audit_logs WHERE user_email = $1',
[user.email]
);
expect(parseInt(emailCheck.rows[0].count)).toBe(0);
// Verify sessions are invalidated
const sessions = await db.query('SELECT * FROM sessions WHERE user_id = $1', [user.id]);
expect(sessions.rows).toHaveLength(0);
});
test('deletion returns confirmation with timestamp for audit trail', async () => {
const { user } = await createUser();
const response = await fetch(`/api/users/${user.id}/delete`, {
method: 'DELETE',
headers: { Authorization: `Bearer ${process.env.TEST_ADMIN_TOKEN}` },
});
const body = await response.json();
expect(body.deletedAt).toBeDefined();
expect(body.requestId).toBeDefined(); // For cross-referencing with audit logs
expect(new Date(body.deletedAt).getTime()).toBeGreaterThan(0);
});
});Test: Data Portability
test('GDPR: data export contains all personal data fields', async () => {
const { user, cleanup } = await createUser();
try {
const response = await fetch(`/api/users/${user.id}/export`, {
headers: { Authorization: `Bearer ${process.env.TEST_ADMIN_TOKEN}` },
});
expect(response.status).toBe(200);
expect(response.headers.get('content-type')).toContain('application/json');
const exportData = await response.json();
// Verify all required GDPR data categories are present
expect(exportData).toHaveProperty('personalData.email');
expect(exportData).toHaveProperty('personalData.name');
expect(exportData).toHaveProperty('activityLog');
expect(exportData).toHaveProperty('purchaseHistory');
expect(exportData).toHaveProperty('exportedAt');
expect(exportData).toHaveProperty('dataController');
} finally {
await cleanup();
}
});SOC 2: Encoding Trust Service Criteria
Test: Access Control (CC6.1)
SOC 2 CC6.1 requires logical access controls. Automated tests verify these controls enforce correctly:
// tests/compliance/soc2-access-control.spec.ts
import { test, expect } from '@playwright/test';
import { createUser } from '../factories';
test.describe('SOC 2 CC6.1: Logical Access Controls', () => {
test('unauthenticated users cannot access protected resources', async ({ page }) => {
// Attempt to access admin dashboard without authentication
await page.goto('/admin/dashboard');
await expect(page).toHaveURL(/\/login/);
});
test('regular users cannot access admin endpoints', async ({ request }) => {
const { user, cleanup } = await createUser({ role: 'USER' });
try {
const userToken = await getAuthToken(user.email);
const response = await request.get('/api/admin/users', {
headers: { Authorization: `Bearer ${userToken}` },
});
expect(response.status()).toBe(403);
} finally {
await cleanup();
}
});
test('sessions expire after configured inactivity period', async ({ page }) => {
await page.goto('/login');
await page.getByLabel('Email').fill('user@test.example.com');
await page.getByLabel('Password').fill(process.env.TEST_USER_PASSWORD!);
await page.getByRole('button', { name: 'Sign In' }).click();
await page.waitForURL('/dashboard');
// Simulate session expiry by manipulating the cookie expiry
await page.evaluate(() => {
document.cookie = 'session_id=; expires=Thu, 01 Jan 1970 00:00:00 GMT';
});
// Attempt to access a protected page
await page.goto('/dashboard');
// Must redirect to login
await expect(page).toHaveURL(/\/login/);
});
test('failed login attempts are rate-limited', async ({ request }) => {
const maxAttempts = 5;
for (let i = 0; i < maxAttempts; i++) {
await request.post('/api/auth/login', {
data: { email: 'admin@test.example.com', password: 'wrongpassword' },
});
}
// The 6th attempt should be rate-limited
const limitedResponse = await request.post('/api/auth/login', {
data: { email: 'admin@test.example.com', password: 'wrongpassword' },
});
expect(limitedResponse.status()).toBe(429);
});
});Test: Audit Logging (CC7.2)
test.describe('SOC 2 CC7.2: Audit Logging', () => {
test('admin actions are logged with timestamp, actor, and target', async () => {
const { user: targetUser, cleanup } = await createUser({ role: 'USER' });
const adminToken = await getAuthToken('admin@test.example.com');
try {
// Perform a tracked admin action
await fetch(`/api/admin/users/${targetUser.id}/suspend`, {
method: 'POST',
headers: { Authorization: `Bearer ${adminToken}` },
});
// Verify the audit log entry
const auditEntry = await db.query(
"SELECT * FROM audit_logs WHERE action = 'user.suspend' AND target_id = $1 ORDER BY created_at DESC LIMIT 1",
[targetUser.id]
);
expect(auditEntry.rows).toHaveLength(1);
expect(auditEntry.rows[0]).toMatchObject({
action: 'user.suspend',
target_id: targetUser.id,
actor_role: 'ADMIN',
});
expect(auditEntry.rows[0].created_at).toBeTruthy();
expect(auditEntry.rows[0].actor_id).toBeTruthy();
} finally {
await cleanup();
}
});
test('audit logs are immutable — no DELETE permission exists', async ({ request }) => {
const response = await request.delete('/api/admin/audit-logs', {
headers: { Authorization: `Bearer ${process.env.TEST_SUPER_ADMIN_TOKEN}` },
});
// DELETE on audit logs must always be forbidden, even for super admins
expect(response.status()).toBe(405); // Method Not Allowed
});
});Cookie and Data Retention Compliance
// tests/compliance/cookies.spec.ts
test.describe('Cookie Compliance', () => {
test('analytics cookies are not set before consent', async ({ page }) => {
await page.goto('/');
// Before accepting cookies
const cookies = await page.context().cookies();
const analyticsCookies = cookies.filter(c =>
['_ga', '_gid', '_fbp', 'hotjar'].some(name => c.name.startsWith(name))
);
expect(analyticsCookies).toHaveLength(0);
});
test('all cookies are set with Secure and SameSite flags in production', async ({ page }) => {
await page.goto('/');
await page.getByRole('button', { name: 'Accept All' }).click();
const cookies = await page.context().cookies();
const insecureCookies = cookies.filter(c =>
!c.secure || c.sameSite === 'None'
);
expect(insecureCookies.map(c => c.name)).toEqual([]);
});
});Generating Audit Evidence from CI
Configure your CI pipeline to export compliance test results as auditor-readable artifacts:
# .github/workflows/compliance.yml
name: Compliance Verification
on:
push:
branches: [main]
schedule:
- cron: '0 6 * * *' # Daily compliance run
jobs:
compliance:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm ci
- run: npx playwright install chromium --with-deps
- name: Run Compliance Tests
run: |
npx playwright test tests/compliance/ \
--reporter=html,json \
--output-dir=compliance-report
- name: Upload Compliance Evidence
uses: actions/upload-artifact@v4
with:
name: compliance-evidence-${{ github.sha }}
path: compliance-report/
retention-days: 365 # Retain for 1 year (audit requirement)The artifact history in GitHub Actions becomes your continuous compliance evidence trail.
Conclusion
Compliance is not a once-a-year documentation exercise. It is a property of your running system that should be verified continuously. By encoding GDPR rights, SOC 2 access controls, and audit logging requirements as automated tests, you transform compliance from a stressful annual scramble into a routine engineering practice. Every merge proves compliance. Every deployment generates evidence. Auditors get a year of CI history instead of a point-in-time document. That is a fundamentally more trustworthy system.