Files
RenewCMS/features/account_export_data.qa.md
T

5.3 KiB

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

  1. Call validator.InitValidator() once.
  2. 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.
  3. 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

  1. 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.
  2. 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.
  3. Empty user. Assert four files, each with empty arrays (never null) and author null.
  4. A failing fake repository gives a non-nil error and no archive bytes.
  5. The audit event user.export_data is recorded only on success.

HTTP checks

  1. 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.
  2. A username with unusual characters gives a sanitised filename.
  3. 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.
  4. A 500 does not consume the slot (fail the fake once, then succeed).
  5. 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".