Legal — QA Plan
1. Document fetch
| # | Scenario | Expected |
|---|---|---|
| 1.1 | GET /legal/documents with no locale | Returns latest per type in he |
| 1.2 | GET /legal/documents?locale=en | Returns latest English version of each type |
| 1.3 | Two terms_of_use rows with same published_at | Deterministic tiebreak by id ASC |
| 1.4 | Locale missing for a type (e.g. no ru cookie_policy) | That type omitted from response |
2. Consent recording
| # | Scenario | Expected |
|---|---|---|
| 2.1 | New user accepts TOS + privacy + waiver during signup | Three legal_consents rows inserted with context='owner_onboarding', IP + UA captured |
| 2.2 | User accepts the same set again | No new rows — recordConsent filters already-consented document IDs |
| 2.3 | TOS bumped to v2; user re-accepts via reconsent flow | New row for v2 inserted with context='reconsent'; v1 row remains |
| 2.4 | Request behind proxy with X-Forwarded-For: 1.2.3.4 | ip_address='1.2.3.4' |
| 2.5 | Request with no user agent | user_agent NULL in DB |
| 2.6 | Consent recorded with organizationId set | Row carries the org id for audit context |
3. Consent status / gating
| # | Scenario | Expected |
|---|---|---|
| 3.1 | New user has accepted no documents | hasRequiredConsents=false; access gated on TOS + privacy + waiver |
| 3.2 | User accepted TOS + privacy but not waiver | Still gated |
| 3.3 | User accepted all three | Access allowed |
| 3.4 | TOS bumped after user accepted v1 | getConsentStatus returns requiresReconsent=true for TOS; banner appears |
| 3.5 | Concurrent acceptances on two devices | Race-safe: at worst two rows for the same doc — currently prevented by the existence check; verify no constraint blows up if the race wins both sides |
4. Web UI
| # | Scenario | Expected |
|---|---|---|
| 4.1 | Acceptance form in Hebrew | RTL layout, links open marketing-site Hebrew page |
| 4.2 | Click marketing link with no internet | Graceful — link target unreachable but form still submittable |
| 4.3 | Check all boxes, click Accept | API call sends correct types array; on success, dialog dismisses |
| 4.4 | Uncheck a required box | Submit button disabled / form invalid |
| 4.5 | Wizard mode (standalone=false) | Parent receives onAccepted callback after API success |
5. Permissions
| # | Scenario | Expected |
|---|---|---|
| 5.1 | Unauthenticated GET /legal/documents | 200 — public route |
| 5.2 | Unauthenticated POST /legal/consents | 401 |
| 5.3 | Authenticated user fetches their own status | 200 |
| 5.4 | User cannot fetch another user’s status | No such endpoint exists — status is always self |
6. Edge cases
| # | Scenario | Expected |
|---|---|---|
| 6.1 | recordConsent with empty types array | IsArray + IsEnum each: true rejects — 400 |
| 6.2 | recordConsent with unknown type | 400 from DTO validator |
| 6.3 | recordConsent with malformed UUID organizationId | 400 |
| 6.4 | DB connection drops mid-insert | Caught error; partial rows for some types possible (atomicity is per-statement only — not wrapped in a transaction) |
7. Smoke
- New owner can complete signup with the acceptance step.
- Member onboarding picks up
context='member_onboarding'. - Status endpoint returns
requiresReconsent=trueafter a doc bump. - Banner appears in Hebrew RTL with correct copy.