From 9876207781c098a646090eafe55d33ea27df6d1d Mon Sep 17 00:00:00 2001 From: null Date: Fri, 3 Jul 2026 09:53:37 -0500 Subject: [PATCH] =?UTF-8?q?docs(qa):=20add=20B16=20(migrations,=20secrets?= =?UTF-8?q?=20&=20deploy)=20=E2=80=94=20close=20plan=20coverage=20gaps?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Gap analysis of the codebase vs the plan surfaced surfaces with no QA home: - DB migration system (idempotency/rollback/fresh==migrated, money conversions) - encryption-key lifecycle (missing/rotated key → graceful degrade, no plaintext/leak) - container deploy (docker-entrypoint: dir perms chmod 700, non-root, run migrations) - update-check phone-home (external request → disclosed + opt-out) - rate-limiter completeness (backupOperationLimiter, skipRateLimitIfNoUsers) Added the B16 batch + playbook, and extended B0 recon to enumerate middleware/workers/migrations/deploy so future cycles can't miss them. Co-Authored-By: Claude Opus 4.8 --- docs/QA_PLAN.md | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/docs/QA_PLAN.md b/docs/QA_PLAN.md index 3d62b77..6d36ee3 100644 --- a/docs/QA_PLAN.md +++ b/docs/QA_PLAN.md @@ -104,6 +104,7 @@ before cross-cutting; regression last). Update **Status** and **Findings** every | B13 | API / backend direct | all `/api/*`: auth, CSRF, validation, rate limits, error shape, IDOR, cents | via HTTP client | ✅ | 0 / 1 | | B14 | Non-functional | a11y, performance, PWA/offline, XSS/secrets, timezone/DST | large + adversarial | ✅ | 0 / 4 | | B15 | Regression & sign-off | full smoke on **production build**, exit criteria | seeded | ✅ | 0 / 0 | +| B16 | Migrations, secrets & deploy | migration idempotency/rollback/fresh==migrated, encryption-key lifecycle, `docker-entrypoint` (perms/first-run/migrate), update-check phone-home | scratch + docker | ⬜ | 0 / 0 | > After B15, if any batch is 🔁 or has open S1/S2, loop back. Then start a new > cycle from B0 against the next build/version. @@ -316,6 +317,8 @@ Run these, then compare the output to the batch playbooks (§7) and the [route m - [ ] **API route mounts** — `grep -nE "app.use\('/api" server.js` — every mounted route group is in B13's list and mapped in Appendix C. - [ ] **Services & components** — `ls services/` and `ls client/components/**/` — new service/component families have a home in a playbook. - [ ] **UI primitives** — `ls client/components/ui/` — every shared primitive is covered by the [B-UI](#b-ui--design-system-primitives) playbook; a new primitive gets a row there. +- [ ] **Middleware & workers** — `ls middleware/ workers/` (+ `services/*Worker*`, `*Scheduler*`) — each is covered (csrf/rateLimiter/securityHeaders/requireAuth → B13; dailyWorker/bankSyncWorker/backupScheduler → B10). +- [ ] **Migrations & deploy** — new `db/database.js` migrations, `Dockerfile`/`docker-entrypoint.sh` changes, and `encryptionService`/`updateCheckService` behavior are covered by [B16](#b16--migrations-secrets--deployment). - [ ] **Interactive-control census (makes "every button tested" *provable*)** — for each page, enumerate every button, link, toggle/switch, checkbox, select, text/number/date/file input, tab, menu, and filter control, and record it in a per-page control checklist (template: [Appendix E](#appendix-e--per-page-control-census)). A control that isn't on a checklist hasn't been tested — the census is the completeness guarantee the batch playbooks alone don't give you. Quick starting inventory: `grep -rnoE "type=[\"'][a-z]+[\"']" client/pages client/components` and `grep -rn "onClick=" client/pages/.jsx`. - [ ] **Feature flags / conditional surfaces** — search for `Only`, `enabled`, `featureFlag`, env gates that hide/show pages; ensure each state is tested. - [ ] **What changed since last cycle** — skim `git log`/`HISTORY.md` since the previous cycle's commit (see [Cycle Log](#11-qa-cycle-log)) for new features/pages. @@ -467,6 +470,36 @@ Run on the **production build** (`npm start`), not dev: - [ ] Bogus URL → 404; logout → login redirect. Console clean throughout. - [ ] Confirm [exit criteria](#appendix-b--exit--sign-off-criteria). +### B16 — Migrations, secrets & deployment +Added Cycle 1 (previously uncovered). These run on every boot / container start and +touch money columns and at-rest secrets — a bug here corrupts data or leaks/breaks +secrets silently. + +**Migrations** (`db/database.js` migration system, `scripts/migrate-db.js`, `schema_migrations`, `rollbackMigration`) +- [ ] **Idempotent:** boot twice on the same DB → second run applies nothing ("Skipping already applied"), no errors, no duplicate rows/columns. +- [ ] **Fresh == migrated:** a brand-new DB (schema.sql + all migrations) has the same schema as a DB migrated up from an old version — same tables/columns/indexes, money columns are **integer cents**. +- [ ] **Rollback:** `rollbackMigration` on the latest migration reverts cleanly and re-applying works; partial/failed migration leaves the DB consistent (transactions per migration). +- [ ] **Money conversions correct:** v1.03 (dollars→cents) and v1.04 (template JSON) convert exact values, no ×100 drift, run once only. +- [ ] Migrating a large/real DB doesn't lose or duplicate bills/payments/categories. + +**Encryption-key lifecycle** (`services/encryptionService.js`, `TOKEN_ENCRYPTION_KEY`, HKDF v1/v2) +- [ ] **Key present:** secrets (SMTP pw, OIDC secret, push tokens, login IP/UA) encrypt at rest and decrypt correctly. +- [ ] **Key missing:** app boots; secret features degrade gracefully (no crash); confirm secrets are **not** silently stored/served in plaintext. +- [ ] **Key rotated/wrong:** old ciphertext fails to decrypt **gracefully** (no crash, no stack leak); `safeDecrypt` fallback path is sane; re-encryption migrations (v0.77–0.79) behave. +- [ ] Encryption key is never committed, logged, or returned in any API response. + +**Container / deploy** (`Dockerfile`, `docker-compose.yml`, `docker-entrypoint.sh`, `deploy.sh`) +- [ ] Image **builds**; container **starts**; app reachable; `/api/version` responds. +- [ ] Entrypoint: creates `DATA_DIR`/`DB_DIR`/`BACKUP_DIR`, sets **`chmod 700`** (not world-readable), `chown`s to the non-root `bill` user, runs migrations when `RUN_DB_MIGRATIONS=true`. +- [ ] Data **persists** across container restart (mounted volume); DB not re-created. +- [ ] Runs as **non-root**; secrets come from env, not baked into the image. + +**Update check / phone-home** (`services/updateCheckService.js`) +- [ ] Confirm the external request to `REPO_API_URL` (default `dream.scheller.ltd`) is **disclosed** (privacy page) and **opt-out-able**; it must send no user data, only fetch the latest release; failure/offline degrades silently. + +**Rate-limiter completeness** (`middleware/rateLimiter.js`) — beyond B13's list +- [ ] `backupOperationLimiter` throttles admin backup/restore/cleanup; `skipRateLimitIfNoUsers` only relaxes limits on a genuinely empty instance (first-run), never afterward. + --- ## 8. Appendices