Files
halobestie-clone/requirement/phase3.4-testing.md
ramadhan sjamsani b59c66f7df Consolidate testing checklist into phase3.4-testing.md
Replaces phase3.3-testing.md. New doc covers:
- Part 1: Phase 3.4 self-managed auth — backend curl matrix, CC UI
  (cookie refresh + bridge), mitra_app + client_app (anonymous →
  upgrade, OTP stub codes, social behind flag), cross-app WS handshake
- Parts 2-4: Phase 3.3 topic sensitivity + 3.2 overlay/E2E/iOS + 3/3.1
  session lifecycle / chat mechanics / navigation — verbatim carry-over
- Part 5: Cross-cutting regression after 3.4 merge, platform coverage,
  security/negative (JWT leak, refresh rotation, cookie flags), and
  Known Blockers / Deferred updated for 3.4 reality (Valkey revocation,
  merge-on-link, firebase_uid drop, real Fazpass, social creds, Apple
  Dev prereqs, JWT rotation procedure)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-24 16:14:46 +08:00

29 KiB
Raw Blame History

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-plan.md, phase3.3.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)