feat: change email, export my data, delete account (with data anonymization)
This commit is contained in:
1 parent
bb191c389a
commit
10a66cd979
123 files changed
+10789
-76
No files matched your search
@@ -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".
|
||||
Reference in new issue
Block a user