Skip to content

Block Sever (#144 Phase 1 + 2) - Manual + Emulator Test Plan โ€‹

FieldValue
Branch / PRfeat/144-block-enforcement / #776
EnvironmentFirestore emulator (rules + Admin-SDK integration) for the safety layers; local full stack (web :5173 + lanterns-api :8083 + emulators) for the UI E2E
Build flags / confignone (behavior is default-on)
Build (commit)see PR head
Testeragent (emulator scenarios) + operator (browser E2E, post-deploy)
Date2026-08-02

What this feature does โ€‹

Blocking someone now SEVERS the relationship instead of hiding it: the chat connection is closed and its messages are destroyed server-side, pending waves and beacon invites between the two are deleted, and the security rules refuse any further read/send across a block in either direction. Before the chat is destroyed, the blocker can save a local copy (capture-then-sever). Unblock stays possible but does not restore the destroyed thread.

Who verifies what (honest split) โ€‹

  • Agent, via the Firestore emulator (automated, recorded here): the security-rule gate (real rules, real emulator) and the server sever/purge (real Admin SDK against the emulator, real deletes). These are the safety-critical layers.
  • Operator, via the browser (manual): the UI flow (block button -> save-a-copy download -> chat closes). Left manual because it needs two real accounts with an established E2EE chat; not worth standing up headlessly for marginal gain over the layer tests.
  • Post-deploy pass: re-confirm on the deployed dev build that the behavior is live (the code is default-on, so this is a smoke check, not an activation-flag check).

Setup / preconditions โ€‹

  • Emulator scenarios: npm run test:rules (Firestore emulator, rules) and npx vitest run services/api/lanterns/test/block.sever.integration.test.js (Admin SDK against a firebase emulators:exec Firestore). No live project touched.
  • Browser E2E: two dev test accounts (see ~/.lantern-agent-probe.env for the admin probe; a second phone+PIN test account per the browser-testing memory). Run the app against emulators (npm run emulators + npm run dev), or against deployed dev AFTER this PR merges. Data to verify lives in the lantern-app-dev Firestore (or the emulator): connections/{id} (blockClosed), connections/{id}/messages (gone), waves, beaconInvites, users/{uid}/blocks/{blockedUid}.
  • Automated browser E2E (headless, deployed dev): npm run e2e:block-sever drives the full two-account flow (light โ†’ wave โ†’ accept โ†’ message โ†’ block+save-copy) and asserts the sever in Firestore. Setup, test-account creation, and gotchas are documented in docs/engineering/testing/HEADLESS_E2E.md.
  • Guardrail: never run the destructive block against a real account you care about; use throwaway/test accounts. The sever really deletes messages.

Summary โ€‹

#ScenarioLayerResultNotes
1Blocked party cannot SEND into the connection (rule)emulator/rules[x] passnpm run test:rules
2Blocked party cannot READ the thread (rule)emulator/rules[x] passnpm run test:rules
3Reverse-direction block also denies (rule)emulator/rules[x] passnpm run test:rules
4blockClosed connection denies read+send even with no block doc (rule)emulator/rules[x] passnpm run test:rules
5blockClosed is server-write-only; participant cannot set/clear (rule)emulator/rules[x] passnpm run test:rules
6beaconInvite create is denied across a block (rule)emulator/rules[x] passnpm run test:rules
7severBetween closes connection + deletes messages (Admin SDK)emulator/integration[x] passnpm run test:sever:emulator
8severBetween purges pending waves both directions (Admin SDK)emulator/integration[x] passnpm run test:sever:emulator
9severBetween purges beacon invites both directions (Admin SDK)emulator/integration[x] passnpm run test:sever:emulator
10Unrelated connections/waves/invites untouched (Admin SDK)emulator/integration[x] passnpm run test:sever:emulator
11UI: block from a live chat closes it; save-a-copy downloadsbrowser[ ] pass [ ] fail [ ] blockedoperator
12UI: warning states permanent deletion; unblock does not restorebrowser[ ] pass [ ] fail [ ] blockedoperator
13Post-deploy: behavior live on dev builddeployed[ ] pass [ ] fail [ ] blockedafter merge
14UI: a blocked peer disappears from the Archived connections list (Phase 3)browser[ ] pass [ ] fail [ ] blockedoperator; unit-covered in waveService.test.js

Deploy verified 2026-08-02 (agent): PR #776 merged to dev (squash cb4fc137) and deployed green. dev.ourlantern.app = 200; lanterns-api /health = 200; the DEPLOYED lanterns-api openapi carries /lanterns/block/sever with responses [200, 400, 401, 429], confirming the exact merged code (incl. the review fixes) shipped. Scenarios 11/12/14 (browser) could NOT be automated headlessly: deployed dev enforces App Check (reCAPTCHA Enterprise), so a headless browser can't sign in (Missing X-Firebase-AppCheck header). These are operator-run on a real browser/device (or a local run with a registered App Check debug token). See the browser-testing memory.

Emulator run 2026-08-02 (agent): scenarios 1-10 PASS. npm run test:rules = 206 rules tests green (incl. the 10 Phase 1 + 3 Phase 2 block cases); npm run test:sever:emulator = 5 integration tests green against a real Firestore emulator. Scenarios 11-12 (browser E2E) and 13 (post-deploy) remain for the operator, since they need two real accounts with an established E2EE chat.

Scenarios โ€‹

1. Blocked party cannot SEND into the connection โ€‹

  • Goal: A block in either direction refuses a message create.
  • Steps:
    1. Seed a connection (ALICE, BOB) and a block users/ALICE/blocks/BOB.
    2. As BOB, attempt to create a message under the connection.
  • Expected: Permission denied.
  • Verify: rules test after ALICE blocks BOB, BOB (the blocked party) CANNOT send.
Result:    [ ] pass   [ ] fail   [ ] blocked
Actual:
Evidence:
Follow-up:

2. Blocked party cannot READ the thread โ€‹

  • Goal: The blocked party's message read is denied.
  • Steps:
    1. Seed connection (ALICE, BOB), a message, and block users/ALICE/blocks/BOB.
    2. As BOB, query the messages subcollection.
  • Expected: Permission denied.
  • Verify: rules test the blocked party CANNOT read the thread.
Result:    [ ] pass   [ ] fail   [ ] blocked
Actual:
Evidence:
Follow-up:

3. Reverse-direction block also denies โ€‹

  • Goal: BOB blocking ALICE denies ALICE just as ALICE blocking BOB does.
  • Steps: Seed block users/BOB/blocks/ALICE; as ALICE, attempt a message send.
  • Expected: Permission denied.
  • Verify: rules test a reverse-direction block (BOB blocks ALICE) also denies ALICE sending.
Result:    [ ] pass   [ ] fail   [ ] blocked
Actual:
Evidence:
Follow-up:

4. blockClosed connection denies read+send even with no block doc โ€‹

  • Goal: After the server severs (sets blockClosed, deletes the thread, and the block doc may later be removed on unblock), the connection stays closed.
  • Steps: Seed connection with blockClosed: true and NO block doc; as a participant, attempt read and send.
  • Expected: Both denied.
  • Verify: rules tests a blockClosed connection denies reads... and ...denies sends.
Result:    [ ] pass   [ ] fail   [ ] blocked
Actual:
Evidence:
Follow-up:

5. blockClosed is server-write-only โ€‹

  • Goal: A participant cannot forge a sever or clear one to reopen.
  • Steps: As a participant, attempt update {blockClosed:true} on an open connection, and update {blockClosed:false} on a severed one.
  • Expected: Both denied.
  • Verify: rules tests a participant CANNOT set blockClosed... / ...CANNOT clear blockClosed....
Result:    [ ] pass   [ ] fail   [ ] blocked
Actual:
Evidence:
Follow-up:

6. beaconInvite create is denied across a block โ€‹

  • Goal: A blocked user cannot create a location-leaking beacon invite (Phase 2 rule gate).
  • Steps: Seed block users/BOB/blocks/ALICE; as ALICE, attempt to create a beaconInvite with recipientId: BOB.
  • Expected: Permission denied.
  • Verify: rules test (Phase 2) beaconInvite create denied across a block.
Result:    [ ] pass   [ ] fail   [ ] blocked
Actual:
Evidence:
Follow-up:

7. severBetween closes connection + deletes messages (Admin SDK) โ€‹

  • Goal: The server sever actually closes and destroys against a real Firestore emulator.
  • Steps: Seed a connection + messages in the emulator; call severBetween(A, B).
  • Expected: connection blockClosed == true; messages subcollection empty; counts returned.
  • Verify: integration test asserts emulator state after the call.
Result:    [ ] pass   [ ] fail   [ ] blocked
Actual:
Evidence:
Follow-up:

8. severBetween purges pending waves both directions โ€‹

  • Goal: Pre-block waves (the location leak) are destroyed, both orderings.
  • Steps: Seed waves (A->B and B->A) in the emulator; call severBetween(A, B).
  • Expected: both wave docs gone; wavesDeleted == 2.
  • Verify: integration test.
Result:    [ ] pass   [ ] fail   [ ] blocked
Actual:
Evidence:
Follow-up:

9. severBetween purges beacon invites both directions โ€‹

  • Goal: Pre-block beacon invites (venue + time leak) are destroyed, both orderings.
  • Steps: Seed beaconInvites (A->B and B->A); call severBetween(A, B).
  • Expected: both invite docs gone; beaconInvitesDeleted == 2.
  • Verify: integration test.
Result:    [ ] pass   [ ] fail   [ ] blocked
Actual:
Evidence:
Follow-up:

10. Unrelated connections/waves/invites untouched โ€‹

  • Goal: The sever is precisely scoped to the pair.
  • Steps: Seed an unrelated connection (A, C), wave (A->C), invite (A->C); call severBetween(A, B).
  • Expected: none of the A-C docs are closed or deleted.
  • Verify: integration test.
Result:    [ ] pass   [ ] fail   [ ] blocked
Actual:
Evidence:
Follow-up:

11. UI: block from a live chat closes it; save-a-copy downloads (operator) โ€‹

  • Goal: End-to-end user flow.
  • Preconditions: Two accounts with an active connection and a few exchanged messages.
  • Steps:
    1. Open the chat as account A. Tap Block.
    2. In the modal, check "Save a copy of this conversation before blocking".
    3. Confirm the block.
  • Expected: a .txt transcript downloads; the chat closes; A no longer sees the conversation.
  • Verify (data): connections/{id}.blockClosed == true; connections/{id}/messages empty; users/A/blocks/B exists. As account B, the chat can no longer be read or sent to.
Result:    [ ] pass   [ ] fail   [ ] blocked
Actual:
Evidence:
Follow-up:

12. UI: warning copy + unblock does not restore (operator) โ€‹

  • Goal: The user is warned the deletion is permanent, and unblock confirms it.
  • Steps:
    1. Open the block modal from a chat; read the warning.
    2. Block, then unblock the same user in Settings.
    3. Start a new connection with them (new wave/accept) if possible.
  • Expected: warning states the chat is permanently deleted and unrecoverable even on unblock; after unblock the old messages do NOT reappear; a fresh conversation starts empty.
  • Verify: old connections/{id}/messages still empty; any new conversation is a new connection doc.
Result:    [ ] pass   [ ] fail   [ ] blocked
Actual:
Evidence:
Follow-up:

14. UI: a blocked peer disappears from the Archived connections list (operator) โ€‹

  • Goal: Phase 3 - blocked peers vanish from the archived list too (the active list already filtered them).
  • Steps: Have an archived (met/expired) connection with a peer, then block that peer; open the Archived connections view.
  • Expected: the blocked peer's archived connection is not shown.
  • Verify: unit-covered in apps/web/src/lib/__tests__/waveService.test.js (getArchivedConnections + subscribeToArchivedConnections block cases); this is the browser confirmation.
Result:    [ ] pass   [ ] fail   [ ] blocked
Actual:
Evidence:
Follow-up:

13. Post-deploy: behavior live on dev build โ€‹

  • Goal: Confirm the shipped dev build severs (no activation flag; this is a smoke check).
  • Steps: After the PR merges and dev deploys, repeat scenario 11 on dev.ourlantern.app with test accounts.
  • Expected: same as scenario 11 on the deployed build.
  • Verify: dev Firestore shows the sever; note the deploy run id + commit.
Result:    [ ] pass   [ ] fail   [ ] blocked
Actual:
Evidence:
Follow-up:

Notes โ€‹

(Agent and operator both edit this file over the run. Agent records emulator scenario results as they run; operator fills the browser + post-deploy scenarios.)

Built with VitePress