diff --git a/requirement/phase3.3-testing.md b/requirement/phase3.3-testing.md deleted file mode 100644 index a8add2a..0000000 --- a/requirement/phase3.3-testing.md +++ /dev/null @@ -1,273 +0,0 @@ -# Phase 3.3 Testing & Outstanding Regression Checklist - -This is a **reminder document** — consolidated testing work for Phase 3.3 plus every outstanding test item carried over from earlier Phase 3 iterations. - -Tick boxes as you verify. - ---- - -## Part 1 — Phase 3.3: Session Topic Sensitivity - -### 1.1 Database / Migration - -- [ ] Migration runs cleanly on an existing dev DB (no errors, all `IF NOT EXISTS` / `ON CONFLICT` paths hit) -- [ ] `chat_sessions.topic_sensitivity` column exists with default `'regular'` and NOT NULL -- [ ] Existing sessions (created before the migration) have `topic_sensitivity = 'regular'` after migration -- [ ] `session_sensitivity_log` table exists with correct FKs (sessions, mitras) -- [ ] `idx_chat_sessions_topic_sensitivity` index created -- [ ] `idx_session_sensitivity_log_session` index created -- [ ] `app_config` has `sensitive_flip_confirmation_enabled = true` by default -- [ ] `app_config` has `sensitive_flag_one_way_latch = false` by default - -### 1.2 Customer Flow (client_app) - -**Topic selection bottom sheet** -- [ ] Tap "Mulai Curhat" → topic selection bottom sheet appears -- [ ] Sheet **cannot** be dismissed by tapping outside -- [ ] Sheet **cannot** be dismissed by swiping down -- [ ] System back button cancels entire "Mulai Curhat" flow (does NOT open pricing) -- [ ] Copy matches PRD (title, body, sub-question, helper line) -- [ ] "Topik umum" button styling is primary -- [ ] "Topik sensitif" button styling is secondary but equal weight (not de-emphasized) - -**Request submission** -- [ ] Tap "Topik umum" → pricing sheet opens with topic pre-selected as regular -- [ ] Tap "Topik sensitif" → pricing sheet opens with topic pre-selected as sensitive -- [ ] After pricing confirm → `POST /api/client/chat/request` body includes `topic_sensitivity: regular|sensitive` -- [ ] Backend rejects request with missing `topic_sensitivity` (400 BAD_REQUEST) -- [ ] Backend rejects request with invalid `topic_sensitivity` value (e.g., `"other"`) -- [ ] Created `chat_sessions` row has correct `topic_sensitivity` value - -**Customer UI after request** -- [ ] Chat screen stays pink (no yellow), regardless of flag -- [ ] Customer history screen: no badge on any row regardless of flag -- [ ] Customer transcript screen: stays pink -- [ ] Customer receives `session_topic_updated` WS message after mitra flip → **no UI change**, no error, no crash - -### 1.3 Mitra Flow — Incoming Request (mitra_app) - -- [ ] Regular request: overlay has no badge, no yellow accent -- [ ] Sensitive request: overlay shows "Topik sensitif" badge + yellow accent -- [ ] Overlay payload from WS includes `topic_sensitivity` -- [ ] Overlay payload from FCM fallback includes `topic_sensitivity` (or app fetches on open) -- [ ] Overlay payload from `getPendingRequestsForMitra` (app-resume path) includes `topic_sensitivity` -- [ ] Mitra accepts sensitive request → lands in active chat screen with correct flag - -### 1.4 Mitra Flow — Active Chat Screen - -- [ ] Sensitive active session: yellow doodle background -- [ ] Regular active session: pink doodle (unchanged behavior) -- [ ] Header banner shows "Topik sensitif" label only when sensitive -- [ ] App-bar toggle icon visible (flag / flag_outlined depending on state) - -**Flip toggle — confirmation enabled (default)** -- [ ] Tap toggle regular → sensitive → dialog appears: "Tandai sesi ini sebagai sensitif?" -- [ ] "Batal" cancels, no state change, no audit log entry, no WS broadcast -- [ ] "Tandai" flips, background turns yellow instantly, log entry created -- [ ] Tap toggle sensitive → regular → dialog: "Tandai sesi ini sebagai topik umum?" - -**Flip toggle — confirmation disabled (via CC config)** -- [ ] Toggle `sensitive_flip_confirmation_enabled` to `false` in CC -- [ ] Flip happens immediately, toast "Sesi ditandai sensitif" / "Sesi ditandai topik umum" -- [ ] No dialog appears - -**One-way latch — disabled (default)** -- [ ] Can flip regular → sensitive → regular → sensitive freely - -**One-way latch — enabled (via CC config)** -- [ ] Toggle `sensitive_flag_one_way_latch` to `true` in CC -- [ ] Session that was regular: can flip to sensitive; toggle then disabled -- [ ] Session that was already sensitive at latch-enable time: toggle disabled with tooltip -- [ ] Attempt to flip sensitive → regular with latch on: API returns 409 `SENSITIVITY_LATCHED` -- [ ] Error dialog shown: "Sesi sudah ditandai sensitif dan tidak bisa diubah kembali." - -**Audit trail** -- [ ] Every successful flip creates a `session_sensitivity_log` row with correct `from_value`, `to_value`, `changed_by_mitra_id` -- [ ] No-op flip (e.g., tap confirm but value didn't actually change) does NOT create a log row -- [ ] Log entries ordered correctly (ascending `created_at`) - -### 1.5 Mitra Flow — Extension - -- [ ] Customer requests extension on regular session → mitra extension card has no badge -- [ ] Customer requests extension on sensitive session → mitra extension card shows "Topik sensitif" badge + yellow accent -- [ ] Mitra flipped session mid-chat regular → sensitive, then customer requests extension → extension card reflects **current** sensitive flag -- [ ] Extension accepted → flag carries over unchanged to extended session - -### 1.6 Mitra Flow — History & Transcript - -- [ ] Mitra history list row shows "Topik sensitif" badge for sensitive sessions -- [ ] Mitra history list row has no badge for regular sessions -- [ ] Mitra transcript view: sensitive session → yellow doodle background -- [ ] Mitra transcript view: regular session → pink doodle -- [ ] Customer-side history and transcript: always pink, no badge - -### 1.7 Mitra Flow — Edge Cases - -- [ ] Mitra tries to flip flag on a session they don't own → 403 FORBIDDEN -- [ ] Mitra tries to flip flag on a `CLOSING` session → 409 SESSION_NOT_ACTIVE -- [ ] Mitra tries to flip flag on a `COMPLETED` session → 409 SESSION_NOT_ACTIVE -- [ ] Mitra tries to flip flag on an `EXPIRED` session → 409 SESSION_NOT_ACTIVE -- [ ] Invalid `topic_sensitivity` value sent to PATCH endpoint → 400 BAD_REQUEST -- [ ] Customer tries to call PATCH endpoint → 403 FORBIDDEN (only mitra allowed) - -### 1.8 Control Center — Settings - -- [ ] Settings page has new "Sensitivitas Topik" section -- [ ] `sensitive_flip_confirmation_enabled` checkbox reflects current backend value -- [ ] `sensitive_flag_one_way_latch` checkbox reflects current backend value -- [ ] PATCH `/internal/config/sensitivity` persists changes -- [ ] Changes take effect immediately on next mitra flip (no app restart needed on mitra side, if mitra fetches config dynamically) - -### 1.9 Control Center — Sessions Page - -- [ ] Sessions list has new filter dropdown (All / Umum / Sensitif) -- [ ] Filter "Sensitif" returns only sessions with `topic_sensitivity = 'sensitive'` -- [ ] Filter "Umum" returns only `regular` -- [ ] Filter "All" returns everything (backward-compatible) -- [ ] Filter works combined with existing status filter -- [ ] Session list has new "Topik" column showing badge (green "Umum" / yellow "Sensitif") - -### 1.10 Control Center — Session Detail - -- [ ] Session detail page shows current `topic_sensitivity` -- [ ] Session detail shows sensitivity audit trail timeline: "Mitra {name} menandai topik sebagai {from→to} pada {timestamp}" -- [ ] Timeline ordered ascending -- [ ] Sessions with no flips show empty timeline (no error) - -### 1.11 Control Center — Dashboard - -- [ ] Dashboard shows "Sesi Sensitif" card with total count + 30-day % breakdown -- [ ] Percentage math correct (sensitive / total × 100, rounded to 1 decimal) -- [ ] Edge case: 0 sessions in last 30 days → shows `0%` not `NaN` - -### 1.12 Control Center — Mitra Activity - -- [ ] Summary table has new columns: Sensitive Total, Sensitive Accepted, Sensitive Rate (%) -- [ ] Mitra with 0 sensitive requests shows `—` (not `0%`) -- [ ] Sensitive rate computed correctly (sensitive_accepted / sensitive_total × 100) -- [ ] Detail log table: optional new "Topik" column with badge -- [ ] Date range filter still works with new columns - -### 1.13 Control Center — Integration Regression - -- [ ] Existing settings (anonymity, free-trial, extension-timeout, early-end, mitra-ping, price-tiers) still work -- [ ] Existing sessions filter (by status) still works -- [ ] Existing dashboard cards still render - ---- - -## Part 2 — Outstanding Items From Phase 3.2 - -Carried over from `project_phase3_testing_status.md` (2026-04-15): - -### 2.1 Chat Request Overlay - -- [ ] **Multiple concurrent chat requests** — verify queue behavior (one shown at a time, next appears when current resolved) -- [ ] Stale request: "cancelled by customer" message shown + requires acknowledge (no auto-dismiss) -- [ ] Stale request: "accepted by other bestie" message shown + requires acknowledge -- [ ] Stale request: "expired" message shown + requires acknowledge -- [ ] Swipe-to-dismiss (ignore) does NOT send reject to backend -- [ ] Ignored request eventually logs as `ignored` in `chat_request_notifications` after 60s timeout -- [ ] Request `missed` (another mitra accepted first) logs correctly -- [ ] `active_session_count` captured correctly at notification creation - -### 2.2 End-to-End Flows - -- [ ] Full chat flow: pair → chat → extension → closure (customer + mitra) -- [ ] Goodbye flow: session expires → closing → both submit goodbye → completed -- [ ] Extension accepted mid-flow → session resumes, timer extends, no grace timer lingering -- [ ] Extension rejected → session moves to closing, both see closure UI -- [ ] Extension timeout (no mitra response) → closing - -### 2.3 iOS Coverage (still partially untested) - -- [ ] OTP login on iOS (customer) -- [ ] OTP login on iOS (mitra) -- [ ] Push notifications on iOS (customer + mitra) -- [ ] FCM token registration on iOS -- [ ] Chat screen rendering on iOS -- [ ] Back button behavior on iOS (deep-link pop fallback) -- [ ] Overlay on iOS (from `project_phase3_testing_status`: iOS setup started but incomplete) -- [ ] Splash screen on iOS -- [ ] Onboarding carousel on iOS -- [ ] Keyboard handling on iOS (chat input, goodbye form) - ---- - -## Part 3 — Outstanding Items From Phase 3 / 3.1 - -### 3.1 Session Lifecycle - -- [ ] Server restart mid-session: session timer is restored from DB (`restoreActiveTimers`) -- [ ] Stale active sessions auto-complete on restart -- [ ] Closing sessions with stale grace timers auto-complete on restart -- [ ] Session expired from customer side (5-min countdown display) -- [ ] Abandoned session during closure grace period → auto-completes -- [ ] **Known limitation**: multi-instance backend sessions not supported until Valkey keyspace notifications implemented (out of scope, just confirm single-instance behavior) - -### 3.2 Chat Mechanics - -- [ ] Message status transitions (sent → delivered → read) work correctly -- [ ] Typing indicator shows/hides correctly on both sides -- [ ] Messages received while backgrounded are marked `delivered` on foreground resume -- [ ] Messages viewed are marked `read` and the read receipt propagates back to sender -- [ ] Unread badge on home screen updates correctly (client_app + mitra_app) - -### 3.3 Navigation / UI - -- [ ] All navigation uses `GoRouter.context.push/go` (no leftover `Navigator.pushNamed`) -- [ ] Deep-linked screens work with `canPop` fallback + `PopScope` -- [ ] `notification_service` uses `go` (not `push`) for terminal states -- [ ] Splash screen hides auth loading flash on both apps -- [ ] Goodbye views use `SingleChildScrollView` (no keyboard overflow) - -### 3.4 Control Center Settings - -- [ ] Free trial config: toggle + duration edit -- [ ] Extension timeout: edit seconds -- [ ] Early end: toggle mitra / customer independently -- [ ] Mitra ping: toggle require + interval -- [ ] Price tiers: add / edit / remove tiers and verify client_app pricing sheet reflects changes - ---- - -## Part 4 — Cross-Cutting / Pre-Release - -### 4.1 Regression Checks (do after Phase 3.3 merge) - -- [ ] Existing customer auth flow still works (welcome → OTP → register → home) -- [ ] Existing mitra auth flow still works -- [ ] Existing control center login still works (admin@halobestie.com) -- [ ] Pairing flow (mulai curhat → matched) still works end-to-end -- [ ] All existing WS messages still processed (no regressions from new `session_topic_updated` handler) - -### 4.2 Platform Coverage - -- [ ] Android: customer app on emulator (Medium_Phone_API_36.1) -- [ ] Android: mitra app on physical device (SM-A530F, 52002a5db8e0c46b) -- [ ] iOS: customer app (Mac + simulator / physical) -- [ ] iOS: mitra app (Mac + simulator / physical) -- [ ] Control center: Chrome latest -- [ ] Control center: Firefox / Safari (if required) - -### 4.3 Load / Concurrency (sanity) - -- [ ] 2 concurrent customers requesting chat at the same time — both find a mitra (or one waits) -- [ ] 1 customer, 5 mitras online — blast notification reaches all 5 -- [ ] Mitra accepts after another mitra already accepted → receives `missed` with `accepted_by_other` -- [ ] Backend restart with active sessions → timers restored, no data loss - -### 4.4 Config Flag Interactions - -- [ ] `sensitive_flag_one_way_latch = true` + existing sensitive session → toggle disabled -- [ ] `sensitive_flip_confirmation_enabled = false` + rapid flips → no race, all logged in order -- [ ] Both config flags toggled together → no conflict - -### 4.5 Known Blockers / Deferred - -Not tests — tracked here so they don't get forgotten: - -- [ ] **Valkey keyspace notifications** — required for multi-instance session timers (noted in memory as future work) -- [ ] **Mitra QC auto-flag** — auto-flagging high-rejection mitras on CC (future phase) -- [ ] **Merge-on-link** for social login (currently reject-on-existing) -- [ ] **Phase 3.4 auth migration** — separate phase, not blocking 3.3 testing diff --git a/requirement/phase3.4-testing.md b/requirement/phase3.4-testing.md new file mode 100644 index 0000000..6f00b5d --- /dev/null +++ b/requirement/phase3.4-testing.md @@ -0,0 +1,456 @@ +# Phase 3.4 Testing & Outstanding Regression Checklist + +Consolidated testing document covering Phase 3.4 (self-managed auth) plus every outstanding test item carried over from Phases 3.0 – 3.3. Replaces `phase3.3-testing.md`. + +Tick boxes as you verify. Cluster labels in brackets: **[BE]** backend / curl, **[CC]** control_center, **[M]** mitra_app, **[C]** client_app. + +Related docs: [phase3.4.md](./phase3.4.md), [phase3.4-plan.md](./phase3.4-plan.md), [phase3.3.md](./phase3.3.md), [phase3.3-plan.md](./phase3.3-plan.md). + +--- + +## Part 1 — Phase 3.4: Self-Managed Auth + +### 1.1 Backend: Schema, Services, Env + +- [ ] **[BE]** `npm run db:migrate` is idempotent — re-running reports "skipping" on existing objects, adds new 3.4 tables/columns, exits 0 +- [ ] **[BE]** `auth_sessions`, `otp_requests` tables exist with correct columns + indexes +- [ ] **[BE]** `customers` has new nullable columns: `email`, `google_sub (UNIQUE)`, `apple_sub (UNIQUE)` +- [ ] **[BE]** `control_center_users` has `password_hash`, `failed_login_count`, `lockout_until` +- [ ] **[BE]** `firebase_uid` columns still exist but are nullable + unused by code (cleanup migration deferred) +- [ ] **[BE]** `app_config` seeded with 6 new OTP / CC-lockout keys at default values +- [ ] **[BE]** `npm run db:seed` creates the super admin with bcrypt-hashed password (runs twice → second is a no-op) +- [ ] **[BE]** Server boots cleanly on `INTERNAL_PORT=3001` + `PUBLIC_PORT=3000` +- [ ] **[BE]** Missing `AUTH_JWT_SECRET` (or <32 chars) → server refuses to start / logs a clear error +- [ ] **[BE]** CORS config on internal listener allows `CC_ORIGIN` with `credentials: true` + +### 1.2 Backend: Token & Session Service (curl) + +- [ ] **[BE]** Access token claims: `{ sub, user_type, session_id, iat, exp }` — HS256, 1h TTL +- [ ] **[BE]** Refresh token is opaque (uuid.random), 30d TTL, bcrypt-hashed in `auth_sessions.refresh_token_hash` +- [ ] **[BE]** Refresh rotates: old refresh after one use → `REFRESH_INVALID`, same `session_id` persists, new access token issued +- [ ] **[BE]** Logout deletes the `auth_sessions` row → subsequent refresh → `REFRESH_INVALID` +- [ ] **[BE]** Multi-device: same user signs in twice → two `auth_sessions` rows; logout on one doesn't kill the other +- [ ] **[BE]** Tampered JWT (signature broken) → 401 `TOKEN_INVALID` +- [ ] **[BE]** Expired JWT → 401; api_client auto-refreshes and retries once +- [ ] **[BE]** `device_info` JSONB populated with `user_agent` + `ip` on each session +- [ ] **[BE]** Session fingerprint survives refresh (no churn) +- [ ] **[BE]** Documented 1h access-token revocation window: post-logout access token still verifies until natural expiry (expected — Valkey revoked_sessions pre-wired but not active) + +### 1.3 Backend: OTP Service (stub mode) + +> Fazpass is stubbed. Dev code is logged to backend console as `[OTP STUB] phone=… code=… ref=…`. + +- [ ] **[BE]** `POST /api/client/auth/otp/request` with valid phone → 200 + `otp_request_id`, stub logs code +- [ ] **[BE]** `POST /api/mitra/auth/otp/request` same, separate user_type +- [ ] **[BE]** Invalid phone format (not E.164) → 422 `PHONE_INVALID` +- [ ] **[BE]** Resend within `otp_resend_cooldown_seconds` (default 60) → 429 `OTP_COOLDOWN` +- [ ] **[BE]** 4th request in an hour from same phone → 429 `OTP_RATE_LIMIT_PHONE` +- [ ] **[BE]** 11th request in an hour from same IP → 429 `OTP_RATE_LIMIT_IP` +- [ ] **[BE]** OTP verify with wrong code → 401 `CODE_MISMATCH`, `attempts` incremented +- [ ] **[BE]** 6th verify attempt (after 5 wrongs) → 429 `OTP_ATTEMPTS_EXCEEDED` +- [ ] **[BE]** OTP verify after `expires_at` (default 5min) → 410 `OTP_EXPIRED` +- [ ] **[BE]** OTP verify twice with correct code → second call → 409 `OTP_USED` +- [ ] **[BE]** Mitra OTP verified via `/api/client/auth/otp/verify` → 400 `WRONG_FLOW` +- [ ] **[BE]** Customer OTP verified via `/api/mitra/auth/otp/verify` → 400 `WRONG_FLOW` + +### 1.4 Backend: Social Identity (post-creds) + +> Deferred until Google/Apple OAuth credentials land. Run this block after setting `GOOGLE_OAUTH_CLIENT_IDS` + `APPLE_*` in `.env` and flipping `ENABLE_SOCIAL_AUTH=true` on client_app. + +- [ ] **[BE]** Google: valid id_token → customer created / upgraded, tokens issued +- [ ] **[BE]** Google: wrong audience (id_token from different client) → 401 `INVALID_ID_TOKEN` +- [ ] **[BE]** Google: tampered signature → 401 `INVALID_ID_TOKEN` +- [ ] **[BE]** Google: `google_sub` already linked to another customer → 409 `IDENTITY_CONFLICT` (merge deferred) +- [ ] **[BE]** Apple: valid id_token → customer created / upgraded +- [ ] **[BE]** Apple: invalid signature (JWKS mismatch) → 401 `INVALID_ID_TOKEN` +- [ ] **[BE]** Apple: missing email (user opted out) → account still created, `email` NULL +- [ ] **[BE]** Apple: `apple_sub` already linked elsewhere → 409 `IDENTITY_CONFLICT` + +### 1.5 Backend: Anonymous + Upgrade + +- [ ] **[BE]** `POST /api/shared/auth/anonymous` → creates customer with auto-generated `display_name`, returns tokens + profile +- [ ] **[BE]** Anonymous customer row: `phone=NULL`, `google_sub=NULL`, `apple_sub=NULL` +- [ ] **[BE]** OTP verify with `anonymous_customer_id` → upgrades SAME customer row; customer UUID unchanged; display_name preserved +- [ ] **[BE]** Google verify with `anonymous_customer_id` → upgrades SAME customer row; google_sub added; display_name preserved if present, else backfilled from Google profile +- [ ] **[BE]** Apple verify with `anonymous_customer_id` → upgrades SAME row +- [ ] **[BE]** Upgrade with `anonymous_customer_id` when identity is ALREADY taken by a different customer → 409 `IDENTITY_CONFLICT` (anonymous row is NOT deleted) +- [ ] **[BE]** Upgrade issues a NEW auth_sessions row. Old anonymous refresh still works (separate session) and reflects the upgraded profile on subsequent calls — **intentional for multi-device UX; do not "fix" without discussion** + +### 1.6 Backend: Auth Middleware + Cross-User-Type Guards + +- [ ] **[BE]** Protected route with no `Authorization` header → 401 `AUTH_MISSING` +- [ ] **[BE]** Expired access token → 401 → api_client refreshes → retry succeeds +- [ ] **[BE]** Customer JWT calling `/api/mitra/auth/me` → 403 `FORBIDDEN` +- [ ] **[BE]** Mitra JWT calling `/api/client/auth/me` → 403 `FORBIDDEN` +- [ ] **[BE]** Customer JWT calling `/internal/*` → 403 `FORBIDDEN` +- [ ] **[BE]** `request.auth = { userType, userId, sessionId }` populated correctly on every protected route (spot-check in logs) + +### 1.7 Backend: Mitra Activation + Inactive Flow + +- [ ] **[BE]** New phone number → auto-creates `mitras` row with `is_active=false`; OTP verify returns 403 `ACCOUNT_INACTIVE` +- [ ] **[BE]** Admin activates mitra via CC (or direct DB `UPDATE mitras SET is_active=true`) → re-request OTP → verify succeeds, tokens returned +- [ ] **[BE]** Mitra re-OTP after deactivation → 403 `ACCOUNT_INACTIVE` on next verify; existing sessions keep working until natural JWT expiry (documented 1h window) + +### 1.8 Backend: Password / CC Login + +- [ ] **[BE]** `POST /internal/auth/login` with correct creds → sets `cc_refresh_token` httpOnly cookie, returns access token + profile + role/permissions +- [ ] **[BE]** Wrong password → 401 `INVALID_CREDENTIALS`, `failed_login_count` incremented +- [ ] **[BE]** 5th wrong password → `lockout_until = NOW() + 15min`; subsequent attempts (even with right password) → 423 `ACCOUNT_LOCKED` +- [ ] **[BE]** Successful login resets `failed_login_count` + `lockout_until` +- [ ] **[BE]** `PATCH /internal/control-center-users/me/password` with wrong current → 401 `INVALID_CREDENTIALS` +- [ ] **[BE]** Password complexity rejected: <8 chars → `PASSWORD_TOO_SHORT`; no digit → `PASSWORD_MISSING_DIGIT`; no upper → `PASSWORD_MISSING_UPPERCASE`; no lower → `PASSWORD_MISSING_LOWERCASE` +- [ ] **[BE]** Admin-forced reset via `PATCH /internal/control-center-users/:id/password` requires `control_center_users:update` permission +- [ ] **[BE]** Create CC user `POST /internal/control-center-users` requires `control_center_users:create`; stores bcrypt hash; rejects weak password with same complexity codes +- [ ] **[BE]** Seed script does NOT enforce complexity (intentional bootstrap loophole — dev `admin123` works) + +### 1.9 Backend: WebSocket Auth + +- [ ] **[BE]** WS handshake with valid JWT in first `{type:"auth", token}` frame → `{type:"auth_ok"}` +- [ ] **[BE]** WS handshake with missing token → connection closed with code 4401 (or equivalent) +- [ ] **[BE]** WS handshake with expired/tampered token → closed +- [ ] **[BE]** `request.auth` equivalent available on WS connection (userId + userType + sessionId) +- [ ] **[BE]** Old Firebase-era token on WS → rejected (documented expected failure from pre-3.4 builds) + +### 1.10 Control Center: UI + Auth + +- [ ] **[CC]** `firebase` dep is gone from `package.json`; bundle is ~280KB not ~800KB +- [ ] **[CC]** Login page: right creds → redirects to `/dashboard` +- [ ] **[CC]** Login page: wrong creds → inline error "Email atau password salah." +- [ ] **[CC]** Login page: account-locked → shows backend message (or translated Indonesian) +- [ ] **[CC]** Refresh cookie persists across a hard browser reload (F5) → user stays logged in +- [ ] **[CC]** Close tab + reopen → `AuthContext.bootstrap` calls `/internal/auth/refresh` with the cookie → user stays logged in +- [ ] **[CC]** Logout clears access token in memory + calls `/logout` + clears cookie → next action redirects to login +- [ ] **[CC]** Access token expires mid-session → next api call 401 → bridge auto-refreshes → request succeeds transparently (simulate by breaking the access token in devtools) +- [ ] **[CC]** Refresh fails (cookie expired / revoked) → `onUnauthenticated` → user bounced to login +- [ ] **[CC]** Two concurrent 401s (fire two API calls in parallel with a broken token) → only one refresh fires; both retries succeed +- [ ] **[CC]** Sidebar "Ganti password" form: wrong current → "Password saat ini salah." +- [ ] **[CC]** Sidebar "Ganti password" form: weak new password → shows backend complexity message +- [ ] **[CC]** Sidebar "Ganti password" form: success → "Password berhasil diubah." (and next login uses the new password) +- [ ] **[CC]** UsersPage create: all fields required; Generate button produces a compliant password; submit creates user +- [ ] **[CC]** UsersPage create: duplicate email → inline error ("Email sudah digunakan.") +- [ ] **[CC]** UsersPage per-row "Reset password": success → inline "Tersimpan."; the target user can then log in with that password +- [ ] **[CC]** All API calls send `credentials: 'include'` (verify via devtools network tab) + +### 1.11 mitra_app: UI + Auth + +- [ ] **[M]** `firebase_auth` dep is gone from `pubspec.yaml`; app builds debug APK clean +- [ ] **[M]** First launch with no stored session → lands on `/login` +- [ ] **[M]** Phone OTP request → OTP screen; backend console shows `[OTP STUB] phone=… code=…` +- [ ] **[M]** Enter stub code → `/home` +- [ ] **[M]** Kill app, relaunch → bootstrap reads refresh from secure storage → `/home` (no login screen flash) +- [ ] **[M]** Inactive mitra → snackbar "Akun tidak aktif. Hubungi administrator." +- [ ] **[M]** Wrong OTP code → snackbar "Kode OTP salah." Fields cleared, focus returns to first digit +- [ ] **[M]** OTP expired (wait 5min) → "Kode OTP kedaluwarsa. Minta kode baru." +- [ ] **[M]** Resend within cooldown → "Tunggu sebentar..." message +- [ ] **[M]** Logout button → storage cleared; next launch hits `/login` +- [ ] **[M]** Long session: access token expires mid-chat → api_client auto-refreshes; user sees no disruption +- [ ] **[M]** Refresh token expires / revoked → `onUnauthenticated` → app returns to `/login` with current screen cleared +- [ ] **[M]** WebSocket handshake uses JWT from `AuthBridge` — verify by logging the token frame in dev +- [ ] **[M]** iOS: keychain persists the refresh token across app updates (reinstall DOES wipe it, but update keeps it) +- [ ] **[M]** Android: encrypted SharedPrefs persists across app updates + +### 1.12 client_app: UI + Auth + +- [ ] **[C]** `firebase_auth` dep is gone; app builds debug APK clean +- [ ] **[C]** First launch → onboarding → welcome screen +- [ ] **[C]** "Lanjut sebagai Tamu" → DisplayNameScreen → enter name → `/home` (anonymous customer created, display_name PATCHed) +- [ ] **[C]** Kill app, relaunch → bootstrap succeeds → `/home` (anonymous session restored) +- [ ] **[C]** "Daftar / Masuk" → phone field → OTP → home (customer created with phone) +- [ ] **[C]** From anonymous state, tap "Daftar / Masuk" → OTP upgrade → **same customer_id**, display_name preserved, phone added (verify via `/me`) +- [ ] **[C]** OTP error codes all render correct Indonesian messages (`CODE_MISMATCH`, `OTP_EXPIRED`, `IDENTITY_CONFLICT`, etc.) +- [ ] **[C]** Anonymity config `anonymity_enabled=false` → after anonymous sign-in, router sends to `/auth/force-register` +- [ ] **[C]** From ForceRegister, complete phone OTP → lands in `/home` with upgraded profile +- [ ] **[C]** Google/Apple buttons are **hidden** by default (ENABLE_SOCIAL_AUTH=false) +- [ ] **[C]** `--dart-define=ENABLE_SOCIAL_AUTH=true` → buttons appear; tapping without backend creds returns clear error (no crash) +- [ ] **[C]** After Google/Apple creds are live: sign-in with fresh account works; sign-in with existing-elsewhere account → `IDENTITY_CONFLICT` surfaced as "Akun ini sudah terhubung..." +- [ ] **[C]** Logout from home → returns to welcome (storage cleared) +- [ ] **[C]** Set display name screen: empty input disabled; success → home +- [ ] **[C]** Long session: access token expires → auto-refresh transparent +- [ ] **[C]** Refresh expires → unauthenticated → welcome screen, not a broken home + +### 1.13 Cross-App / WebSocket / Regression After Auth Change + +- [ ] **[BE][M][C]** WS auth in chat (`chat_notifier` client + `mitra_chat_notifier`) works with the new JWT +- [ ] **[BE][M][C]** WS auth in pairing (`pairing_notifier` client + `chat_request_notifier` mitra) works with the new JWT +- [ ] **[BE][M][C]** Full chat flow: pair → chat → extension → closure — using the new JWT end-to-end +- [ ] **[BE]** FCM device-token registration endpoint (`/api/shared/device-token`) still works; Authorization header is the new JWT + +--- + +## Part 2 — Phase 3.3 Carry-Over: Topic Sensitivity + +### 2.1 Database / Migration + +- [ ] Migration runs cleanly on an existing dev DB (no errors, all `IF NOT EXISTS` / `ON CONFLICT` paths hit) +- [ ] `chat_sessions.topic_sensitivity` column exists with default `'regular'` and NOT NULL +- [ ] Existing sessions (created before the migration) have `topic_sensitivity = 'regular'` after migration +- [ ] `session_sensitivity_log` table exists with correct FKs (sessions, mitras) +- [ ] `idx_chat_sessions_topic_sensitivity` index created +- [ ] `idx_session_sensitivity_log_session` index created +- [ ] `app_config` has `sensitive_flip_confirmation_enabled = true` by default +- [ ] `app_config` has `sensitive_flag_one_way_latch = false` by default + +### 2.2 Customer Flow (client_app) + +**Topic selection bottom sheet** +- [ ] Tap "Mulai Curhat" → topic selection bottom sheet appears +- [ ] Sheet **cannot** be dismissed by tapping outside +- [ ] Sheet **cannot** be dismissed by swiping down +- [ ] System back button cancels entire "Mulai Curhat" flow (does NOT open pricing) +- [ ] Copy matches PRD (title, body, sub-question, helper line) +- [ ] "Topik umum" button styling is primary +- [ ] "Topik sensitif" button styling is secondary but equal weight (not de-emphasized) + +**Request submission** +- [ ] Tap "Topik umum" → pricing sheet opens with topic pre-selected as regular +- [ ] Tap "Topik sensitif" → pricing sheet opens with topic pre-selected as sensitive +- [ ] After pricing confirm → `POST /api/client/chat/request` body includes `topic_sensitivity: regular|sensitive` +- [ ] Backend rejects request with missing `topic_sensitivity` (400 BAD_REQUEST) +- [ ] Backend rejects request with invalid `topic_sensitivity` value (e.g., `"other"`) +- [ ] Created `chat_sessions` row has correct `topic_sensitivity` value + +**Customer UI after request** +- [ ] Chat screen stays pink (no yellow), regardless of flag +- [ ] Customer history screen: no badge on any row regardless of flag +- [ ] Customer transcript screen: stays pink +- [ ] Customer receives `session_topic_updated` WS message after mitra flip → **no UI change**, no error, no crash + +### 2.3 Mitra Flow — Incoming Request (mitra_app) + +- [ ] Regular request: overlay has no badge, no yellow accent +- [ ] Sensitive request: overlay shows "Topik sensitif" badge + yellow accent +- [ ] Overlay payload from WS includes `topic_sensitivity` +- [ ] Overlay payload from FCM fallback includes `topic_sensitivity` (or app fetches on open) +- [ ] Overlay payload from `getPendingRequestsForMitra` (app-resume path) includes `topic_sensitivity` +- [ ] Mitra accepts sensitive request → lands in active chat screen with correct flag + +### 2.4 Mitra Flow — Active Chat Screen + +- [ ] Sensitive active session: yellow doodle background +- [ ] Regular active session: pink doodle (unchanged behavior) +- [ ] Header banner shows "Topik sensitif" label only when sensitive +- [ ] App-bar toggle icon visible (flag / flag_outlined depending on state) + +**Flip toggle — confirmation enabled (default)** +- [ ] Tap toggle regular → sensitive → dialog appears: "Tandai sesi ini sebagai sensitif?" +- [ ] "Batal" cancels, no state change, no audit log entry, no WS broadcast +- [ ] "Tandai" flips, background turns yellow instantly, log entry created +- [ ] Tap toggle sensitive → regular → dialog: "Tandai sesi ini sebagai topik umum?" + +**Flip toggle — confirmation disabled (via CC config)** +- [ ] Toggle `sensitive_flip_confirmation_enabled` to `false` in CC +- [ ] Flip happens immediately, toast "Sesi ditandai sensitif" / "Sesi ditandai topik umum" +- [ ] No dialog appears + +**One-way latch — disabled (default)** +- [ ] Can flip regular → sensitive → regular → sensitive freely + +**One-way latch — enabled (via CC config)** +- [ ] Toggle `sensitive_flag_one_way_latch` to `true` in CC +- [ ] Session that was regular: can flip to sensitive; toggle then disabled +- [ ] Session that was already sensitive at latch-enable time: toggle disabled with tooltip +- [ ] Attempt to flip sensitive → regular with latch on: API returns 409 `SENSITIVITY_LATCHED` +- [ ] Error dialog shown: "Sesi sudah ditandai sensitif dan tidak bisa diubah kembali." + +**Audit trail** +- [ ] Every successful flip creates a `session_sensitivity_log` row with correct `from_value`, `to_value`, `changed_by_mitra_id` +- [ ] No-op flip (e.g., tap confirm but value didn't actually change) does NOT create a log row +- [ ] Log entries ordered correctly (ascending `created_at`) + +### 2.5 Mitra Flow — Extension + +- [ ] Customer requests extension on regular session → mitra extension card has no badge +- [ ] Customer requests extension on sensitive session → mitra extension card shows "Topik sensitif" badge + yellow accent +- [ ] Mitra flipped session mid-chat regular → sensitive, then customer requests extension → extension card reflects **current** sensitive flag +- [ ] Extension accepted → flag carries over unchanged to extended session + +### 2.6 Mitra Flow — History & Transcript + +- [ ] Mitra history list row shows "Topik sensitif" badge for sensitive sessions +- [ ] Mitra history list row has no badge for regular sessions +- [ ] Mitra transcript view: sensitive session → yellow doodle background +- [ ] Mitra transcript view: regular session → pink doodle +- [ ] Customer-side history and transcript: always pink, no badge + +### 2.7 Mitra Flow — Edge Cases + +- [ ] Mitra tries to flip flag on a session they don't own → 403 FORBIDDEN +- [ ] Mitra tries to flip flag on a `CLOSING` session → 409 SESSION_NOT_ACTIVE +- [ ] Mitra tries to flip flag on a `COMPLETED` session → 409 SESSION_NOT_ACTIVE +- [ ] Mitra tries to flip flag on an `EXPIRED` session → 409 SESSION_NOT_ACTIVE +- [ ] Invalid `topic_sensitivity` value sent to PATCH endpoint → 400 BAD_REQUEST +- [ ] Customer tries to call PATCH endpoint → 403 FORBIDDEN (only mitra allowed) + +### 2.8 Control Center — Settings + +- [ ] Settings page has new "Sensitivitas Topik" section +- [ ] `sensitive_flip_confirmation_enabled` checkbox reflects current backend value +- [ ] `sensitive_flag_one_way_latch` checkbox reflects current backend value +- [ ] PATCH `/internal/config/sensitivity` persists changes +- [ ] Changes take effect immediately on next mitra flip (no app restart needed on mitra side, if mitra fetches config dynamically) + +### 2.9 Control Center — Sessions Page + +- [ ] Sessions list has new filter dropdown (All / Umum / Sensitif) +- [ ] Filter "Sensitif" returns only sessions with `topic_sensitivity = 'sensitive'` +- [ ] Filter "Umum" returns only `regular` +- [ ] Filter "All" returns everything (backward-compatible) +- [ ] Filter works combined with existing status filter +- [ ] Session list has new "Topik" column showing badge (green "Umum" / yellow "Sensitif") + +### 2.10 Control Center — Session Detail + +- [ ] Session detail page shows current `topic_sensitivity` +- [ ] Session detail shows sensitivity audit trail timeline: "Mitra {name} menandai topik sebagai {from→to} pada {timestamp}" +- [ ] Timeline ordered ascending +- [ ] Sessions with no flips show empty timeline (no error) + +### 2.11 Control Center — Dashboard + +- [ ] Dashboard shows "Sesi Sensitif" card with total count + 30-day % breakdown +- [ ] Percentage math correct (sensitive / total × 100, rounded to 1 decimal) +- [ ] Edge case: 0 sessions in last 30 days → shows `0%` not `NaN` + +### 2.12 Control Center — Mitra Activity + +- [ ] Summary table has new columns: Sensitive Total, Sensitive Accepted, Sensitive Rate (%) +- [ ] Mitra with 0 sensitive requests shows `—` (not `0%`) +- [ ] Sensitive rate computed correctly (sensitive_accepted / sensitive_total × 100) +- [ ] Detail log table: optional new "Topik" column with badge +- [ ] Date range filter still works with new columns + +### 2.13 Control Center — Integration Regression + +- [ ] Existing settings (anonymity, free-trial, extension-timeout, early-end, mitra-ping, price-tiers) still work under the new auth +- [ ] Existing sessions filter (by status) still works +- [ ] Existing dashboard cards still render + +--- + +## Part 3 — Phase 3.2 Carry-Over + +### 3.1 Chat Request Overlay + +- [ ] **Multiple concurrent chat requests** — verify queue behavior (one shown at a time, next appears when current resolved) +- [ ] Stale request: "cancelled by customer" message shown + requires acknowledge (no auto-dismiss) +- [ ] Stale request: "accepted by other bestie" message shown + requires acknowledge +- [ ] Stale request: "expired" message shown + requires acknowledge +- [ ] Swipe-to-dismiss (ignore) does NOT send reject to backend +- [ ] Ignored request eventually logs as `ignored` in `chat_request_notifications` after 60s timeout +- [ ] Request `missed` (another mitra accepted first) logs correctly +- [ ] `active_session_count` captured correctly at notification creation + +### 3.2 End-to-End Flows + +- [ ] Full chat flow: pair → chat → extension → closure (customer + mitra) +- [ ] Goodbye flow: session expires → closing → both submit goodbye → completed +- [ ] Extension accepted mid-flow → session resumes, timer extends, no grace timer lingering +- [ ] Extension rejected → session moves to closing, both see closure UI +- [ ] Extension timeout (no mitra response) → closing + +### 3.3 iOS Coverage + +- [ ] OTP login on iOS (customer) — **update:** flow now uses stub Fazpass OTP from backend console, no native reCAPTCHA +- [ ] OTP login on iOS (mitra) — same as above +- [ ] Push notifications on iOS (customer + mitra) +- [ ] FCM token registration on iOS +- [ ] Chat screen rendering on iOS +- [ ] Back button behavior on iOS (deep-link pop fallback) +- [ ] Overlay on iOS (iOS setup started but incomplete per prior memory) +- [ ] Splash screen on iOS +- [ ] Onboarding carousel on iOS +- [ ] Keyboard handling on iOS (chat input, goodbye form) + +--- + +## Part 4 — Phase 3 / 3.1 Carry-Over + +### 4.1 Session Lifecycle + +- [ ] Server restart mid-session: session timer is restored from DB (`restoreActiveTimers`) +- [ ] Stale active sessions auto-complete on restart +- [ ] Closing sessions with stale grace timers auto-complete on restart +- [ ] Session expired from customer side (5-min countdown display) +- [ ] Abandoned session during closure grace period → auto-completes +- [ ] **Known limitation**: multi-instance backend sessions not supported until Valkey keyspace notifications implemented (out of scope — see `project_session_timer_scaling` in memory) + +### 4.2 Chat Mechanics + +- [ ] Message status transitions (sent → delivered → read) work correctly +- [ ] Typing indicator shows/hides correctly on both sides +- [ ] Messages received while backgrounded are marked `delivered` on foreground resume +- [ ] Messages viewed are marked `read` and the read receipt propagates back to sender +- [ ] Unread badge on home screen updates correctly (client_app + mitra_app) + +### 4.3 Navigation / UI + +- [ ] All navigation uses `GoRouter.context.push/go` (no leftover `Navigator.pushNamed`) +- [ ] Deep-linked screens work with `canPop` fallback + `PopScope` +- [ ] `notification_service` uses `go` (not `push`) for terminal states +- [ ] Splash screen hides auth loading flash on both apps +- [ ] Goodbye views use `SingleChildScrollView` (no keyboard overflow) + +### 4.4 Control Center Settings + +- [ ] Free trial config: toggle + duration edit +- [ ] Extension timeout: edit seconds +- [ ] Early end: toggle mitra / customer independently +- [ ] Mitra ping: toggle require + interval +- [ ] Price tiers: add / edit / remove tiers and verify client_app pricing sheet reflects changes + +--- + +## Part 5 — Cross-Cutting / Pre-Release + +### 5.1 Regression Checks (after Phase 3.4 merge) + +- [ ] Existing customer auth flow still works (welcome → OTP → register → home) — under new JWT +- [ ] Existing mitra auth flow still works — under new JWT +- [ ] Existing control center login still works (admin with rotated password) — under new cookie-based refresh +- [ ] Pairing flow (mulai curhat → matched) still works end-to-end under new auth +- [ ] All existing WS messages still processed (no regressions from `session_topic_updated` handler or JWT handshake) + +### 5.2 Platform Coverage + +- [ ] Android: client_app on emulator (Medium_Phone_API_36.1) +- [ ] Android: mitra_app on physical device (SM-A530F, 52002a5db8e0c46b) +- [ ] iOS: client_app (Mac + simulator / physical) +- [ ] iOS: mitra_app (Mac + simulator / physical) +- [ ] Control center: Chrome latest +- [ ] Control center: Firefox / Safari (if required) + +### 5.3 Load / Concurrency (sanity) + +- [ ] 2 concurrent customers requesting chat at the same time — both find a mitra (or one waits) +- [ ] 1 customer, 5 mitras online — blast notification reaches all 5 +- [ ] Mitra accepts after another mitra already accepted → receives `missed` with `accepted_by_other` +- [ ] Backend restart with active sessions → timers restored, no data loss +- [ ] 3 concurrent CC admins logging in → each gets its own `auth_sessions` row; logout on one doesn't affect others + +### 5.4 Config Flag Interactions + +- [ ] `sensitive_flag_one_way_latch = true` + existing sensitive session → toggle disabled +- [ ] `sensitive_flip_confirmation_enabled = false` + rapid flips → no race, all logged in order +- [ ] Anonymity toggle flip (enabled → disabled) while a user has an active anonymous session → next bootstrap routes to ForceRegister +- [ ] Anonymity toggle flip (disabled → enabled) → no-op for already-identified users; new users can go anonymous again + +### 5.5 Security / Negative + +- [ ] JWT secret leak simulation: a manually-signed token with the wrong secret → 401 across all surfaces +- [ ] Token from a deleted `auth_sessions` row still verifies for up to 1h (documented window); revoke immediately with future Valkey `revoked_sessions` set +- [ ] Refresh token used twice in parallel → only one succeeds (rotation-on-use); second → `REFRESH_INVALID` +- [ ] Refresh token stolen + used from a different IP → still works (documented design — rotation + bcrypt is the defense); `device_info` logs the new IP for audit +- [ ] Control center cookie `SameSite` / `Secure` / `HttpOnly` flags correct in prod (verify via devtools in staging) + +### 5.6 Known Blockers / Deferred + +Not tests — tracked so they don't get forgotten: + +- [ ] **Valkey keyspace notifications** — required for multi-instance session timers + real token revocation +- [ ] **Mitra QC auto-flag** — auto-flagging high-rejection mitras on CC dashboard +- [ ] **Merge-on-link** for social login (currently reject-on-existing with `IDENTITY_CONFLICT`) +- [ ] **Drop `firebase_uid` columns** — cleanup migration once no code path references them +- [ ] **Real Fazpass integration** — replace stub in `otp.service.js` when API docs + creds arrive +- [ ] **Google + Apple OAuth creds** — provision then flip `ENABLE_SOCIAL_AUTH=true` and run §1.4 + client_app social blocks +- [ ] **Apple Developer setup** — Services ID + `.p8` + Team ID + Key ID before iOS Apple-sign-in E2E +- [ ] **JWT secret rotation procedure** — documented but not implemented (dual-secret window plan in `phase3.4-plan.md`)