Skip to main content

Compliance-as-Code: Writing Tests That Prove GDPR and SOC 2 Compliance

June 2, 2026

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 proof

The 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
  });
});

// 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.

Recommended Posts