Reference catalog of GDPR-aligned test patterns - data-subject-rights workflows (Art. 15 access, Art. 16 rectification, Art. 17 erasure / "right to be forgotten", Art. 18 restriction, Art. 20 portability, Art. 21 objection); consent recording + revocation per Art. 7; data-residency assertions per Art. 44 - 50 international transfers; breach-notification timing tests per Art. 33 (72 hours); data-minimization assertions in fixtures per Art. 5(1)(c). Use when authoring GDPR-readiness tests for any product processing EU personal data.
80
100%
Does it follow best practices?
Run evals on this skill
Adds up to 20 points to the overall score
View guide
Passed
No findings from the security scan
Per gdpr.eu (community-maintained reference; canonical text at eur-lex.europa.eu Regulation 2016/679):
GDPR (General Data Protection Regulation, in force 2018-05-25) applies to any processing of EU personal data regardless of the processor's location. Failure to demonstrate compliance carries fines up to €20M or 4% of global annual turnover (whichever higher).
This is a reference skill - defines the test-pattern catalog by Article. Tests use the team's existing test framework; this skill is the per-Article test recipe.
synthetic-pii-generator - never real customer PII.def test_consent_recorded_at_collection_time():
response = client.post('/signup', json={
'email': 'alice@example.com',
'consent_marketing': True,
'consent_timestamp': '2026-05-06T12:00:00Z',
})
user = User.objects.get(email='alice@example.com')
consent = ConsentRecord.objects.get(user=user, scope='marketing')
assert consent.granted is True
assert consent.granted_at is not None
assert consent.granted_via == 'signup-form' # auditable evidence
def test_consent_revocable():
revoke_consent(user, scope='marketing')
consent = ConsentRecord.objects.get(user=user, scope='marketing')
assert consent.granted is False
assert consent.revoked_at is not None
# Subsequent marketing emails must not be sent:
assert not user.is_eligible_for(EmailType.MARKETING)def test_subject_access_request_returns_all_personal_data():
response = authenticated_client.post('/sar', json={'subject_email': 'alice@example.com'})
assert response.status_code == 200
data = response.json()
# Must include data from EVERY system that holds PII for this subject:
assert 'profile' in data
assert 'billing' in data
assert 'support_tickets' in data
assert 'analytics' in data # often forgotten; tests catch it
assert 'third_party_sharing' in data
# Response must arrive within 1 month per Art. 12(3):
assert response.headers['X-Processing-Time'] < timedelta(days=30)def test_erasure_removes_all_personal_data():
erase_subject('alice@example.com')
# Across ALL systems:
assert User.objects.filter(email='alice@example.com').count() == 0
assert BillingRecord.objects.filter(user_email='alice@example.com').count() == 0
assert AnalyticsEvent.objects.filter(user_email='alice@example.com').count() == 0
# Backup retention: erasure marker recorded; backup expires within retention window
assert ErasureMarker.objects.filter(subject='alice@example.com').exists()def test_data_portability_export_machine_readable():
response = client.post('/data-export', json={'subject': 'alice@example.com'})
export = response.json()
# Must be in "structured, commonly used and machine-readable format"
assert response.headers['Content-Type'] in ['application/json', 'text/csv', 'application/xml']
# Format must be self-describing (keys not random IDs):
assert 'profile' in export
assert 'orders' in exportdef test_breach_notification_workflow_within_72h():
# Simulate a breach detection event
breach = BreachIncident.create(detected_at=timezone.now())
# Workflow MUST notify supervisory authority within 72 hours:
deadline = breach.detected_at + timedelta(hours=72)
notification = BreachNotification.objects.filter(incident=breach).first()
assert notification is not None
assert notification.sent_at <= deadline
assert notification.recipient == 'supervisory.authority@dpa.example.eu'def test_eu_user_data_stored_in_eu_region():
user = User.objects.create(email='alice@example.fr', region='EU')
# Storage assertions vary by infra (AWS region, GCP region, etc.):
assert user.storage_region in ['eu-west-1', 'eu-central-1', 'eu-north-1']
# Cross-region replication MUST stay in EU:
backups = Backup.objects.filter(user=user)
for b in backups:
assert b.region in EU_REGIONSdef test_signup_fixtures_contain_only_required_pii():
fixture = load_fixture('user_signup.json')
required = {'email', 'consent_terms', 'consent_marketing'}
optional = {'phone', 'city', 'date_of_birth'}
for k in fixture.keys():
assert k in required | optional, f"Unrecognized field: {k}"
# No SSN, no passport number, no genetic / biometric data unless explicitly justified:
forbidden = {'ssn', 'passport', 'genetic_data', 'biometric_data'}
for k in forbidden:
assert k not in fixture, f"Forbidden PII type: {k}"A support agent triggers an Art. 17 erasure for alice@example.com. The team already has test_erasure_removes_all_personal_data (above), which asserted the app User table was empty and passed. Running the multi-system version reveals AnalyticsEvent still holds rows keyed by user_email, because analytics writes were never wired into the erasure job. The test fails on AnalyticsEvent.objects.filter(...).count() == 0. The team adds analytics to the erasure fan-out, re-runs, and the test goes green with an ErasureMarker recorded so the nightly backup expiry honors the request within the retention window.
| Gap | Detection |
|---|---|
| Marketing emails sent to revoked-consent users | Step Art. 7 + email-flow tests |
| SAR returns incomplete dataset (missing analytics/CRM) | Step Art. 15 multi-system assertion |
| Erasure leaves data in unindexed backup tables | Step Art. 17 multi-system assertion |
| EU user data quietly replicated to US region | Step Art. 44 - 50 region assertion |
| Breach detected but DPO not notified within 72h | Step Art. 33 timing test |
| Anti-pattern | Why it fails | Fix |
|---|---|---|
| Test only main-app data store on SAR/erasure | Misses analytics, CRM, support, backups | Multi-system assertion (Steps Art. 15 + 17) |
| Hardcode 30-day SAR window assumption | GDPR allows extension to 3 months in complex cases | Test timeline against actual policy doc |
| Test consent recording without revocation | Half the workflow uncovered | Both grant + revoke tests (Step Art. 7) |
| Fixture data uses real customer PII | GDPR violation in tests themselves | Use synthetic-pii-generator |
| Skip data-minimization assertions | New PII types creep in via schema changes | Step Art. 5(1)(c) field-allowlist test |
synthetic-pii-generator - cross-plugin: safe test fixture generationccpa-test-patterns - sister: California analogueaudit-trail-test-author - companion: GDPR audit logs