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,101 @@
|
||||
# QA procedure: account_delete
|
||||
|
||||
Package: `test` (black-box), extending `test/user_test.go` (`mockUserRepo`, `stubHasher`), `test/mail_test.go` (`mockMailRepo`, `failSwitch`) and the existing role, author, api-key and audit fakes. Reuse them and keep new type names unique.
|
||||
|
||||
## Test infrastructure additions needed first
|
||||
|
||||
1. A fake of the atomic domain operation behind `DeleteOwnAccountUseCase` that mutates the in-memory fakes together.
|
||||
- It honours `fail.trigger` at each step ("DeleteAPIKeys", "ScrubAuthor", "AnonymiseEvents", "DeleteUser").
|
||||
- On any failure it restores a snapshot, so rollback is observable.
|
||||
2. The role fake: seed built-in Owner, Admin, Writer and a custom role with all admin permissions. Add a `CountHolders`-style method (or whichever method the coder adds to the domain interface) returning verified holders only.
|
||||
3. The audit fake: record events and support the anonymise method; assert the rewritten events by id.
|
||||
4. The session revocation: reuse the `authtoken` revocation fake; record revoked ids.
|
||||
|
||||
## Setup shared by every case
|
||||
|
||||
5. Call `validator.InitValidator()` once.
|
||||
6. Build `useCases.NewDeleteOwnAccountUseCase(...)` over the fakes, `stubHasher`, `mockMailRepo`, a clock, `publicURL "https://cms.example.com"` and the audit fake.
|
||||
7. Handler checks mount `DELETE /v1/user/me` on a fresh `chi.NewRouter()` with the `NewUserRouter` session chain and the 5 per 15 minutes limiter. Also mount `DELETE /{id}` behind its permission middleware, to prove the routing order. Mint jwt cookies with `user_id` and `token_version`.
|
||||
8. Seed marta (id 7, Writer, "hashed:limewash2026"), jonas (2, Admin), ines (3, Owner). No live server, real SMTP or real SQLite.
|
||||
|
||||
## Use-case checks
|
||||
|
||||
9. Happy path for a Writer. Assert:
|
||||
- The user is gone, keys 1 and 2 are gone.
|
||||
- The author row is scrubbed (name, slug `deleted-user-<id>`, bio "", avatar/website nil, links empty, user_id nil) when it credits 3 articles.
|
||||
- The author is deleted when it credits none.
|
||||
- Articles, entries and images are byte-for-byte unchanged (`reflect.DeepEqual` before and after).
|
||||
- Her 12 events are anonymised and the other users' events are untouched.
|
||||
- The session id is revoked.
|
||||
- Exactly one `account-deleted` mail goes to the old address, with data `useCases.MailAccountDeletedData{Host}` and no other field.
|
||||
- Exactly one `user.delete_account` event, with actor and target name "Deleted user" and no `@` in the params.
|
||||
10. Password. A wrong password returns `user.ErrWrongPassword` (`errors.Is`), changes nothing, sends nothing and records one failed security event.
|
||||
11. Last-role matrix. Table-driven from the feature (9 rows):
|
||||
- Owner/Admin blocked rows return `user.ErrLastAdmin` carrying the role label "Owner" or "Admin" (assert with `errors.As` on whatever the coder exports).
|
||||
- Unverified holders do not count.
|
||||
- A custom all-permission role does not count and is not blocked.
|
||||
- Allowed rows delete.
|
||||
12. Concurrent role change. The fake changes a holder between the check and the operation. Assert `ErrAccountNotDeletable` and an untouched state.
|
||||
13. Rollback. For each step failure in case 1, assert that all fakes equal their pre-state, no mail is sent, no revocation is recorded and the error is returned.
|
||||
14. Mail failure with the log captured. Assert nil error, the user gone, and a log line naming the failed send.
|
||||
15. Order of checks. A wrong password plus a last-Owner caller returns `ErrWrongPassword`, not last_admin (no role probing without the password).
|
||||
|
||||
## HTTP checks
|
||||
|
||||
16. Success. Assert 204, an empty body, and a `Set-Cookie` for `jwt` with `Max-Age=0` or an expired date. A follow-up `GET` with the old cookie through the real middleware chain gives 401.
|
||||
17. Refusals table of the feature: status and exact body for each row. For the 400 rows, compare to the `validator.Validate.Struct` message. Assert the fakes are unchanged.
|
||||
18. Last role: 409, body `last_admin`, header `X-Last-Role` equal to `Owner` or `Admin`.
|
||||
19. `account_not_deletable` gives 409 with that body.
|
||||
20. Rate limit. Make 5 attempts (mix wrong-password and blocked), then assert the sixth is 429 with `Retry-After` and that no state changed.
|
||||
21. Auth gates: 401, API key 403 and unverified 403, with exact bodies.
|
||||
22. Routing. `DELETE /v1/user/me` reaches the new handler, never `removeHandler`. `DELETE /v1/user/7?expected_role_id=3` still behaves as `features/users_row_actions.qa.md` says.
|
||||
23. Mail rendering. Render `account-deleted` with the real `MailRepository.Render`. Assert the subject, no `@`, no username, no button, no leftover `{{`.
|
||||
|
||||
## Persistence (real in-memory SQLite, same models as `api/dependecyInjection.go`)
|
||||
|
||||
24. One test runs the real atomic operation. Assert the user, the keys, the scrub-or-delete of the author, the audit anonymisation, a forced mid-way failure leaving everything intact, and the portability of the SQL (no driver-specific syntax). If a real-DB test already exists for MySQL/PostgreSQL (`task test:mysql`/`test:postgres`), run the same test there.
|
||||
|
||||
## 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.
|
||||
- French render: for the Danger zone, the dialog, each Blocked variant and the Login notice with `setLocale('fr')`, assert the French text is shown and no English string remains; switching `en`, `fr`, `en` restores the English copy.
|
||||
- The confirmation word stays the literal "DELETE" in both languages, and the French dialog label tells the user to type it.
|
||||
- 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 Danger zone title, text and button; the dialog title, text, the three list items, the "Your articles and media stay" heading and text, "Export my data first", the two field labels, "Keep my account", "Delete my account", "Deleting…"; the wrong-password, 429 and server errors; the three Blocked variants (Owner, administrator, cannot be deleted) with their buttons; and the Login notice title and text.
|
||||
|
||||
| State | Mocked /v1 |
|
||||
|---|---|
|
||||
| Danger zone, Confirm: empty/typing/ready | none |
|
||||
| Wrong password (403) | `DELETE /user/me` 403 "wrong_password" |
|
||||
| Deleting | DELETE pending |
|
||||
| Rate limited (429), Server error | 429; 500 and a rejected fetch |
|
||||
| Blocked: last administrator | 409 "last_admin" + `X-Last-Role: Admin` |
|
||||
| Blocked: last Owner | 409 "last_admin" + `X-Last-Role: Owner` |
|
||||
| Blocked: cannot be deleted | 409 "account_not_deletable" |
|
||||
| Success | 204; assert no `POST /auth/logout`, `session.clear()`, router at `/login?reason=account-deleted` |
|
||||
|
||||
Also assert:
|
||||
- Confirm enablement for "DELETE", "delete", "DELETE ", "".
|
||||
- A blocked dialog ignores Escape and the backdrop and has no close button.
|
||||
- The Login notice appears for `reason=account-deleted` only, and the query is gone after it is consumed.
|
||||
- Typed values are kept on 429 and 500.
|
||||
- The "Deleted user" fallback where a creator is missing from the lookup.
|
||||
|
||||
## Visual
|
||||
|
||||
`task visual:check -- account` and `-- auth`.
|
||||
- Board data to reuse: password mask `••••••••••`, the typed word "DEL" in Confirm: typing and "DELETE" elsewhere, and the Auth board's "Account deleted" notice.
|
||||
- States: all 10 delete states of the Account board and "Account deleted" on the Auth board. The Owner variant has no frame.
|
||||
- None is clock-sensitive.
|
||||
|
||||
## Manual verification
|
||||
|
||||
- Real SMTP: the account-deleted mail arrives once at the old address, and the link-free rendering in a real client.
|
||||
- Delete a real account in a browser against a real database file: log back in as another user and check an article byline reads "Deleted user", and Activity shows "Deleted user".
|
||||
- On the embedded frontend, check that after deletion `/login?reason=account-deleted` shows the notice once.
|
||||
- Keyboard and focus: the dialog focus trap, the focus on the cleared password after 403, Tab order in the blocked dialog.
|
||||
- MySQL and PostgreSQL via `task test:mysql` / `task test:postgres`.
|
||||
Reference in new issue
Block a user