feat: change email, export my data, delete account (with data anonymization)

This commit is contained in:
Florian Sylvain committed 2026-10-30 20:19:07 +01:00
1 parent bb191c389a
commit 10a66cd979
123 files changed
+10789 -76

No files matched your search

+72
View File
@@ -0,0 +1,72 @@
# QA procedure: account_export_data
Package: `test` (black-box), extending `test/user_test.go` (`mockUserRepo`), the existing article, entry, image, author, api-key, role and audit fakes (reuse; do not duplicate type names), and `stubHasher`.
## Test infrastructure additions needed first
1. Fakes: add the list-by-creator and list-by-uploader methods to the article, entry and image fakes, and an actor-filtered list to the audit fake (honouring `audit.Filter.Actor`).
2. A `zipEntries(t, body []byte) map[string][]byte` helper that reads the archive with `archive/zip` and fails the test if there are directory entries or duplicate names.
3. A fixed clock for the use case (`func() time.Time`) so the filename date is deterministic.
## Setup shared by every case
4. Call `validator.InitValidator()` once.
5. Build `useCases.NewExportUserDataUseCase(...)` over the fakes. Mount `GET /v1/user/me/export` on a fresh `chi.NewRouter()` with the session chain of `NewUserRouter` and the per-user success-only limiter (one per 15 minutes). The limiter takes a clock, or the tests drive the window with two `httptest` calls and a short injected window for the expiry case.
6. Seed marta (id 7), ines (id 8), articles, entries, images, audit events and an author per the feature. No live server, real SMTP or real SQLite.
## Use-case checks
7. Run the use case for marta. Assert:
- Exactly four entries with the expected names.
- Each parses as JSON, with the field names of the feature.
- `profile.json` has no key containing `password`, `code`, `token` or `hash` (walk the decoded JSON recursively).
- The API-key rows have neither the hashed key nor the raw key.
8. Scope. Assert that ines's rows never appear. Assert article 4 (credited to marta's author, not created by her) is included. Assert that soft-deleted rows are excluded and that anonymous failed-login events are excluded.
9. Empty user. Assert four files, each with empty arrays (never null) and `author` null.
10. A failing fake repository gives a non-nil error and no archive bytes.
11. The audit event `user.export_data` is recorded only on success.
## HTTP checks
12. Success. Assert the status, `Content-Type: application/zip`, `Cache-Control: no-store`, and `Content-Disposition` equal to `attachment; filename="renewcms-export-marta-2026-10-29.zip"`. Also assert the archive opens.
13. A username with unusual characters gives a sanitised filename.
14. Rate limit. A second call within 15 minutes gives 429 with a numeric `Retry-After` (<= 900 and > 0) and the exact body "Too many requests. Please try again later.". Another user is unaffected. A call after the window gives 200.
15. A 500 does not consume the slot (fail the fake once, then succeed).
16. Auth gates: 401, API key 403 and unverified 403, with exact bodies.
## Vitest (SPA)
Strings live in `messages.ts` in English and French under the same keys.
I18n checks (extend the existing `web/src/i18n/*.spec.ts` style, e.g. `spa.spec.ts`):
- Key parity: every new key exists in both `en` and `fr`, with no empty value and the same placeholders (filename, minutes, countdown).
- French render: for each state with `setLocale('fr')`, assert the French text is shown and no English string remains; switching `en`, `fr`, `en` restores the English copy.
- English values equal the design copy verbatim.
Flagged assumption: the French wording is authored by the coder because docs/design has none. French strings to author, for the user's review: the card title and text, the four content rows (title and description), "Download my data", "Ready in a few seconds.", "Preparing download…", the "Download started" toast pair, the rate-limit title, text and "Available in {time}", and the error title and text.
| State | Mocked /v1 |
|---|---|
| Direct (recommended): default | none |
| Direct: preparing | `GET /user/me/export` pending |
| Direct: started | 200, `application/zip`, the Content-Disposition header; assert the toast text and that a download was triggered with that filename |
| Direct: rate limited (429) | 429 with Retry-After 720; assert the copy and the "Available in 12:00" label, no date mocking |
| Direct: error | 500 and a rejected fetch |
- Also assert that a second click while preparing sends no second request.
- Also assert that focus returns to the button after an error.
## Visual
`task visual:check -- account`. Board data to reuse: username marta, the filename `renewcms-export-marta-2026-10-29.zip` in the toast, and the Retry-After of 720.
- Checked states: "Direct (recommended): default", "Direct: preparing", "Direct: started", "Direct: error".
- `skip: clock-sensitive`: "Direct: rate limited (429)".
- Async states ("Alternative, emailed link: default", "Alternative: requested", "Alternative: already in progress", "Alternative: error"): the user accepted the recommendation to skip them in `web/visual/states.ts` through `exception` entries. The `approvedBy` field of each exception must still be recorded as the user at coder time; the coder must not invent an approver, and the entries are not valid until it is set.
## Manual verification
- Open a real downloaded ZIP and read all four files.
- Download in Firefox and Chromium and check the saved filename.
- Large account: time a user with thousands of events.
- The 15-minute window against a real clock and a restart.
- Keyboard: the button and the toast announce "Download started".