You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Integration tests use `describe.skipIf` when `DATABASE_URL` (and admin URL for `CREATE DATABASE`) are unset; they clone template `dn_test_tpl_session_email` (isolated from `@deepnotes/db`’s `dn_test_tpl_deepnotes` so **Turbo** can run both packages in parallel).
46
46
47
+
**How to run locally:** ensure `.env` at `new-deepnotes/.env` has `DATABASE_URL` and (for template create/drop) `DATABASE_ADMIN_URL` with a role that can `CREATE DATABASE`. Then:
48
+
49
+
-`pnpm --filter @deepnotes/session exec vitest run src/account-flows.integration.test.ts`
50
+
-`pnpm --filter @deepnotes/db exec vitest run src/template-db.test.ts`
51
+
52
+
CI should set the same vars against the workflow Postgres service (role with `CREATEDB`).
@@ -55,12 +62,17 @@ Integration tests use `describe.skipIf` when `DATABASE_URL` (and admin URL for `
55
62
| Password change invalidates sessions |`performUserPasswordChange` + explicit `devices` / `sessions` insert |`sessions.invalidated === true` for the user. |
56
63
| Password change, wrong old password |`performUserPasswordChange`|**400**`BAD_REQUEST`. |
57
64
|**Login → refresh → refresh**|`performUserRegister`, `performSessionLogin`, `performSessionRefresh`| Login sets `Set-Cookie` (`refreshToken`, `loggedIn=true`); DB `sessions.refresh_code` + `encryption_key` change on refresh; JSON `oldSessionKey` / `newSessionKey` match pre/post row `encryption_key`; **second** refresh with rotated cookies succeeds. |
65
+
|**Replay pre-rotation refresh JWT**| Same as above, then third call with **first** login’s `refreshToken` + original `loggedIn`|**401**`UNAUTHORIZED` “Session was invalidated.” — JWT still verifies but `payload.rfc` no longer matches `sessions.refresh_code` after rotation. |
66
+
|**Refresh, `loggedIn` ≠ true**|`performSessionLogin`, `performSessionRefresh` with `loggedInCookie: "false"`|**401** “User not logged in.” |
67
+
|**Refresh, no refresh cookie**|`performSessionRefresh` with `refreshCookie: undefined`, `loggedIn: "true"`|**401** “No refresh token received.” |
|**2FA finish, wrong code**|`EnableRequest` then `EnableFinish` with `"000000"`|**400**`BAD_REQUEST` (“Authenticator token is incorrect.”). |
62
72
|**2FA login without MFA**| After finish, `performSessionLogin` without `authenticatorToken`|**401** “Requires two-factor authentication.” (untrusted device). |
63
73
|**2FA login, bad TOTP**|`authenticatorToken: "111111"`|**401** “Invalid authenticator token.” |
74
+
|**2FA login with recovery code**|`performUserTwoFactorEnableFinish` → `performSessionLogin` with `recoveryCode` (no TOTP) |**200**-equivalent (`sessionId`); `decryptRecoveryCodes` on row shows **5** hashes left (one consumed). |
75
+
|**2FA recovery code reuse**| Second `performSessionLogin` with same plaintext recovery code, new IP/UA |**401** “Invalid recovery code.” |
64
76
65
77
**`@deepnotes/db` real Postgres (`template-db.test.ts`):**
66
78
@@ -70,8 +82,10 @@ Integration tests use `describe.skipIf` when `DATABASE_URL` (and admin URL for `
70
82
|**Sessions FK**|`INSERT sessions` without parent `users` / `devices`| Insert **rejects** (FK violation) for orphan `user_id` / `device_id`. |
71
83
|**Devices FK**|`INSERT devices` with non-existent `user_id`| Insert **rejects** (FK to `users`). |
72
84
|**Pages → groups FK**|`INSERT pages` with unknown `group_id`| Insert **rejects** (FK to `groups`). |
85
+
|**`group_members` → `users` FK**|`INSERT group_members` with bogus `user_id` and `group_id`| Insert **rejects** (no parent user). |
86
+
|**`group_members` → `groups` FK**| Insert minimal `users` row, then `group_members` with unknown `group_id`| Insert **rejects** (no parent group). |
73
87
74
-
**Not yet in integration:** Redis failed-login with real `ioredis`/Upstash against `performSessionLogin` (unit tests cover rate-limit helpers); `performSessionRefresh`edge cases (expiredJWT, `loggedIn` false, replayed refresh token invalidation); **2FA recovery-code login**path against real Postgres (logic exists in `two-factor.ts` + `login.ts`).
88
+
**Not yet in integration:** Redis failed-login with real `ioredis`/Upstash against `performSessionLogin` (unit tests cover rate-limit helpers); `performSessionRefresh`with **expired** refresh JWT (would need clock-skew or short-lived token minting in test); **invalid/tampered**refresh JWT where `verifyRefreshToken` fails but `decodeRefreshTokenUnsafe` returns `sid` (invalidates row — behaviour worth an explicit test when touching refresh again).
75
89
76
90
### Sessions + account (current)
77
91
@@ -161,8 +175,8 @@ Cross-cutting work so the new SPA does not repeat **legacy `apps/client`** patte
161
175
162
176
| Package / app | Role | What runs today | Gaps (highest value next) |
|**`@deepnotes/api`**| Zod + OpenAPI |`openapi.test.ts` (health + session + 2FA paths); **`schemas/users.test.ts`** (email/password change, 2fa finish) | Schemas for pages/groups when they land; optional OpenAPI **snapshot**|
167
181
|**`@deepnotes/api-worker`**| Hono on Worker |`index.test.ts`: 503 when env missing — includes **2FA** routes in matrix |**200** tests with stub `SessionEnv` + template DB (heavier) |
168
182
|**`@deepnotes/web`**| SPA |`app.test.ts` (mount `App.vue`) | Auth UI + API client as in §5.8 |
@@ -185,8 +199,8 @@ Cross-cutting work so the new SPA does not repeat **legacy `apps/client`** patte
185
199
-[ ] Drizzle migrations from empty DB documented for production upgrades.
186
200
-[ ] Cold API dev start under **2 s** (no `inspect-brk` by default) — validate on a typical laptop.
187
201
-[ ] Collab + realtime: at least one integration test each (Redis + deps).
-[ ] Auth, crypto, Stripe: automated coverage beyond smoke; **no** generic repository layer (§5.0). **Progress:** crypto + Zod + Resend unit tests; **Postgres**as in [Phase 3 test coverage (detail)](#phase-3-test-coverage-detail) (16 session + 6 db integration cases when DB env is set). **Next:** Redis failed-login integration; refresh**expired** JWT; Stripe when billing exists.
190
204
-[x] No tRPC / superjson / RevenueCat / key-rotation in **this** tree (keep absent); product sign-off for IAP/Stripe when billing ships.
191
205
-[x] Client: zero undocumented forks, or a short owned exception list — see [docs/CLIENT_FORKS.md](./docs/CLIENT_FORKS.md).
192
206
-[ ] Cloudflare: deploy runbook; Hyperdrive + Postgres + Redis proven in staging; collab/realtime topology chosen and load-tested.
@@ -197,14 +211,15 @@ Cross-cutting work so the new SPA does not repeat **legacy `apps/client`** patte
197
211
198
212
## Phase 3 working order (suggested)
199
213
200
-
Use this when resuming: **(done)** account HTTP through 2FA (incl. `load` as POST, see map); **Postgres**coverage for 2FA service layer + extra **`@deepnotes/db` FK**tests. **(next)**`users.pages` + `groups` + `pages` REST from [TRPC_REST_MAP](./docs/TRPC_REST_MAP.md) — each slice should add **template DB** tests for new FKs and happy paths where SQL risk is high. **(then)****realtime + collab** (no key rotation) and **Stripe** + wire billing hooks on account routes.
214
+
Use this when resuming: **(done)** account HTTP through 2FA (incl. `load` as POST, see map); **Postgres** for 2FA (TOTP + recovery codes + refresh replay/cookie guards) + **`@deepnotes/db`**FKs through **`group_members`**. **(next)**`users.pages` + `groups` + `pages` REST from [TRPC_REST_MAP](./docs/TRPC_REST_MAP.md) — each slice should add **template DB** tests for new FKs and happy paths where SQL risk is high. **(then)****realtime + collab** (no key rotation) and **Stripe** + wire billing hooks on account routes.
201
215
202
216
---
203
217
204
218
## Short log (newest first)
205
219
206
220
| Date | Change |
207
221
|------|--------|
222
+
| 2026-04-27 |**More real Postgres tests:**`account-flows.integration.test.ts` — **2FA recovery-code** login + one-time use + `decryptRecoveryCodes` count; **replay** of first refresh JWT after two rotations (**401**); **`loggedIn`** / missing refresh guards. `template-db.test.ts` — **`group_members`** FK to `users` and to `groups`. PLAN_PROGRESS: run commands, expanded tables, matrix + success criteria + working order. |
| 2026-04-27 |**2FA account HTTP:**`user-two-factor-settings.ts`, `encryptUserAuthenticatorSecret` in `session-crypto`, Zod + OpenAPI + Hono for `/api/users/me/2fa/*` (6 routes); [TRPC_REST_MAP](./docs/TRPC_REST_MAP.md) — `load` is POST not GET; see [2FA HTTP routes](#2fa-http-routes-phase-3) below. |
210
225
| 2026-04-27 |**Integration tests:** expanded `account-flows.integration.test.ts` (email wrong code; password change PHC + keyring unwrap with salt from PHC; `sessions` invalidation; wrong old password). Renamed from `email-change.integration.test.ts`. PLAN_PROGRESS: detailed Phase 3 test table + matrix gaps. |
0 commit comments