import '@testing-library/jest-dom/vitest'; import React from 'react'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { cleanup, fireEvent, render, screen } from '@testing-library/react'; let contactsState: Record = {}; const mocks = vi.hoisted(() => ({ buildMailboxClaims: vi.fn(async () => []), countDmMailboxes: vi.fn(async () => ({ ok: true, count: 0 })), ensureRegisteredDmKey: vi.fn(async () => ({ dhPubKey: 'local-dh', dhAlgo: 'X25519' })), fetchDmPublicKey: vi.fn(async () => ({ dh_pub_key: 'peer-dh', dh_algo: 'X25519' })), pollDmMailboxes: vi.fn(async () => ({ ok: true, messages: [] })), sendDmMessage: vi.fn(async () => ({ ok: true, transport: 'relay' })), sendOffLedgerConsentMessage: vi.fn(async () => ({ ok: true, transport: 'relay' })), sharedMailboxToken: vi.fn(async () => 'shared-token'), buildContactAcceptMessage: vi.fn(() => 'accept'), buildContactDenyMessage: vi.fn(() => 'deny'), buildContactOfferMessage: vi.fn(() => 'offer'), generateSharedAlias: vi.fn(() => 'alias-123'), mergeAliasHistory: vi.fn((history?: string[]) => history || []), parseAliasRotateMessage: vi.fn(() => null), parseDmConsentMessage: vi.fn(() => null), preferredDmPeerId: vi.fn((peerId: string) => peerId), allDmPeerIds: vi.fn(() => []), purgeBrowserDmState: vi.fn(async () => {}), ratchetDecryptDM: vi.fn(async () => { throw new Error('no_ratchet_state'); }), ratchetEncryptDM: vi.fn(async () => 'ratchet-ciphertext'), addContact: vi.fn(), blockContact: vi.fn(), decryptDM: vi.fn(async () => 'plaintext'), decryptSenderSealPayloadLocally: vi.fn(async () => ''), deriveSharedKey: vi.fn(async () => ({})), encryptDM: vi.fn(async () => 'ciphertext'), getContacts: vi.fn(() => contactsState), getDHAlgo: vi.fn(() => 'X25519'), getNodeIdentity: vi.fn(() => ({ nodeId: '!sb_local', publicKey: 'local-pub', privateKey: 'local-priv', })), hasSovereignty: vi.fn(() => true), hydrateWormholeContacts: vi.fn(async () => contactsState), purgeBrowserContactGraph: vi.fn(), purgeBrowserSigningMaterial: vi.fn(), removeContact: vi.fn(), unblockContact: vi.fn(), unwrapSenderSealPayload: vi.fn(() => ({ version: 'v2', ephemeralPub: '' })), updateContact: vi.fn(), verifyNodeIdBindingFromPublicKey: vi.fn(async () => true), verifyRawSignature: vi.fn(async () => true), getSenderRecoveryState: vi.fn(() => 'verified'), recoverSenderSealWithFallback: vi.fn(async () => null), requiresSenderRecovery: vi.fn(() => false), shouldKeepUnresolvedRequestVisible: vi.fn(() => false), shouldPromoteRecoveredSenderForBootstrap: vi.fn(() => false), shouldPromoteRecoveredSenderForKnownContact: vi.fn(() => false), bootstrapDecryptAccessRequest: vi.fn(async () => 'offer'), bootstrapEncryptAccessRequest: vi.fn(async () => 'x3dh1:bootstrap'), canUseWormholeBootstrap: vi.fn(async () => false), fetchWormholeStatus: vi.fn(async () => ({ ready: true, transport_tier: 'private_strong' })), fetchWormholeIdentity: vi.fn(async () => ({ node_id: '!sb_local', public_key: 'local-pub' })), prepareWormholeInteractiveLane: vi.fn(async () => ({ ready: true, settingsEnabled: true, transportTier: 'private_transitional', identity: { node_id: '!sb_local', public_key: 'local-pub' }, })), importWormholeDmInvite: vi.fn(async () => ({ ok: true, peer_id: '!sb_imported', trust_fingerprint: 'invitefp', trust_level: 'invite_pinned', })), isWormholeReady: vi.fn(async () => true), isWormholeSecureRequired: vi.fn(async () => false), issueWormholePairwiseAlias: vi.fn(async () => ({ ok: true, shared_alias: 'alias-123' })), openWormholeSenderSeal: vi.fn(async () => ({ sender_id: '!sb_peer', seal_verified: true })), })); vi.mock('@/lib/api', () => ({ API_BASE: 'http://localhost:8000', })); vi.mock('@/mesh/meshDmClient', () => ({ buildMailboxClaims: mocks.buildMailboxClaims, countDmMailboxes: mocks.countDmMailboxes, ensureRegisteredDmKey: mocks.ensureRegisteredDmKey, fetchDmPublicKey: mocks.fetchDmPublicKey, pollDmMailboxes: mocks.pollDmMailboxes, sendDmMessage: mocks.sendDmMessage, sendOffLedgerConsentMessage: mocks.sendOffLedgerConsentMessage, sharedMailboxToken: mocks.sharedMailboxToken, })); vi.mock('@/mesh/meshDmConsent', () => ({ allDmPeerIds: mocks.allDmPeerIds, buildContactAcceptMessage: mocks.buildContactAcceptMessage, buildContactDenyMessage: mocks.buildContactDenyMessage, buildContactOfferMessage: mocks.buildContactOfferMessage, generateSharedAlias: mocks.generateSharedAlias, mergeAliasHistory: mocks.mergeAliasHistory, parseAliasRotateMessage: mocks.parseAliasRotateMessage, parseDmConsentMessage: mocks.parseDmConsentMessage, preferredDmPeerId: mocks.preferredDmPeerId, })); vi.mock('@/mesh/meshDmWorkerClient', () => ({ purgeBrowserDmState: mocks.purgeBrowserDmState, ratchetDecryptDM: mocks.ratchetDecryptDM, ratchetEncryptDM: mocks.ratchetEncryptDM, })); vi.mock('@/mesh/meshIdentity', () => ({ addContact: mocks.addContact, blockContact: mocks.blockContact, decryptDM: mocks.decryptDM, decryptSenderSealPayloadLocally: mocks.decryptSenderSealPayloadLocally, deriveSharedKey: mocks.deriveSharedKey, encryptDM: mocks.encryptDM, getContacts: mocks.getContacts, getDHAlgo: mocks.getDHAlgo, getNodeIdentity: mocks.getNodeIdentity, hasSovereignty: mocks.hasSovereignty, hydrateWormholeContacts: mocks.hydrateWormholeContacts, purgeBrowserContactGraph: mocks.purgeBrowserContactGraph, purgeBrowserSigningMaterial: mocks.purgeBrowserSigningMaterial, removeContact: mocks.removeContact, unblockContact: mocks.unblockContact, unwrapSenderSealPayload: mocks.unwrapSenderSealPayload, updateContact: mocks.updateContact, verifyNodeIdBindingFromPublicKey: mocks.verifyNodeIdBindingFromPublicKey, verifyRawSignature: mocks.verifyRawSignature, })); vi.mock('@/mesh/requestSenderRecovery', () => ({ getSenderRecoveryState: mocks.getSenderRecoveryState, recoverSenderSealWithFallback: mocks.recoverSenderSealWithFallback, requiresSenderRecovery: mocks.requiresSenderRecovery, shouldKeepUnresolvedRequestVisible: mocks.shouldKeepUnresolvedRequestVisible, shouldPromoteRecoveredSenderForBootstrap: mocks.shouldPromoteRecoveredSenderForBootstrap, shouldPromoteRecoveredSenderForKnownContact: mocks.shouldPromoteRecoveredSenderForKnownContact, })); vi.mock('@/mesh/wormholeDmBootstrapClient', () => ({ bootstrapDecryptAccessRequest: mocks.bootstrapDecryptAccessRequest, bootstrapEncryptAccessRequest: mocks.bootstrapEncryptAccessRequest, canUseWormholeBootstrap: mocks.canUseWormholeBootstrap, })); vi.mock('@/mesh/wormholeIdentityClient', () => ({ fetchWormholeStatus: mocks.fetchWormholeStatus, fetchWormholeIdentity: mocks.fetchWormholeIdentity, prepareWormholeInteractiveLane: mocks.prepareWormholeInteractiveLane, getWormholeDmInviteImportErrorResult: (error: unknown) => error && typeof error === 'object' && 'result' in (error as Record) ? (((error as Record).result as Record) || null) : null, importWormholeDmInvite: mocks.importWormholeDmInvite, isWormholeReady: mocks.isWormholeReady, isWormholeSecureRequired: mocks.isWormholeSecureRequired, issueWormholePairwiseAlias: mocks.issueWormholePairwiseAlias, openWormholeSenderSeal: mocks.openWormholeSenderSeal, })); import MessagesView from '@/components/InfonetTerminal/MessagesView'; function renderMessagesView(options?: { onOpenDeadDrop?: (peerId: string, opts?: { showSas?: boolean }) => void; }) { return render( {}} onOpenDeadDrop={options?.onOpenDeadDrop} />); } async function openComposeForRecipient(recipient: string, body: string) { fireEvent.click(screen.getByRole('button', { name: 'COMPOSE' })); fireEvent.change(screen.getByLabelText(/Recipient agent ID/i), { target: { value: recipient }, }); fireEvent.change(screen.getByLabelText(/Message/i), { target: { value: body }, }); await screen.findByLabelText(/Recipient agent ID/i); } describe('MessagesView first-contact trust UX', () => { beforeEach(() => { cleanup(); localStorage.clear(); contactsState = {}; vi.clearAllMocks(); mocks.getContacts.mockImplementation(() => contactsState); mocks.hydrateWormholeContacts.mockImplementation(async () => contactsState); mocks.fetchWormholeStatus.mockResolvedValue({ ready: true, transport_tier: 'private_strong' }); mocks.prepareWormholeInteractiveLane.mockResolvedValue({ ready: true, settingsEnabled: true, transportTier: 'private_transitional', identity: { node_id: '!sb_local', public_key: 'local-pub' }, }); mocks.isWormholeSecureRequired.mockResolvedValue(false); mocks.getNodeIdentity.mockReturnValue({ nodeId: '!sb_local', publicKey: 'local-pub', privateKey: 'local-priv', }); mocks.hasSovereignty.mockReturnValue(true); mocks.buildMailboxClaims.mockResolvedValue([]); mocks.pollDmMailboxes.mockResolvedValue({ ok: true, messages: [] }); mocks.countDmMailboxes.mockResolvedValue({ ok: true, count: 0 }); mocks.ensureRegisteredDmKey.mockResolvedValue({ dhPubKey: 'local-dh', dhAlgo: 'X25519' }); mocks.fetchDmPublicKey.mockResolvedValue({ dh_pub_key: 'peer-dh', dh_algo: 'X25519' }); mocks.sendOffLedgerConsentMessage.mockResolvedValue({ ok: true, transport: 'relay' }); mocks.canUseWormholeBootstrap.mockResolvedValue(false); }); afterEach(() => { cleanup(); }); it('blocks unknown first contact until a signed invite is imported', async () => { renderMessagesView(); await openComposeForRecipient('!sb_unknown', 'hello from first contact'); expect(await screen.findByText('Verified First Contact Required')).toBeInTheDocument(); expect( screen.getByText(/Secure request bootstrap is blocked until you import a signed invite/i), ).toBeInTheDocument(); expect(screen.getByRole('button', { name: 'Send Secure Mail' })).toBeDisabled(); }); it('can jump directly from the downgrade warning into invite import flow', async () => { renderMessagesView(); await openComposeForRecipient('!sb_unknown', 'hello from first contact'); fireEvent.click(screen.getByRole('button', { name: 'Import Signed Invite' })); expect(await screen.findByText('Import Verified Invite')).toBeInTheDocument(); expect(screen.getByLabelText(/Local Alias/i)).toHaveValue('!sb_unknown'); }); it('does not expose a TOFU downgrade button for first contact anymore', async () => { renderMessagesView(); await openComposeForRecipient('!sb_unknown', 'hello from first contact'); expect(screen.queryByRole('button', { name: /Explicitly Allow TOFU/i })).not.toBeInTheDocument(); expect(mocks.sendOffLedgerConsentMessage).not.toHaveBeenCalled(); }); it('does not require the TOFU override when the contact is invite-pinned already', async () => { contactsState = { '!sb_invited': { alias: 'Pinned Peer', blocked: false, trust_level: 'invite_pinned', invitePinnedTrustFingerprint: 'abcdef123456', invitePinnedRootFingerprint: 'rootabcdef123456', invitePinnedRootManifestFingerprint: 'manifestabcdef123456', invitePinnedRootWitnessPolicyFingerprint: 'policyabcdef123456', invitePinnedRootWitnessThreshold: 2, invitePinnedRootWitnessCount: 3, invitePinnedRootManifestGeneration: 1, invitePinnedRootRotationProven: true, invitePinnedAt: 123, remotePrekeyFingerprint: 'abcdef123456', remotePrekeyRootFingerprint: 'rootabcdef123456', remotePrekeyRootManifestFingerprint: 'manifestabcdef123456', remotePrekeyRootWitnessPolicyFingerprint: 'policyabcdef123456', remotePrekeyRootWitnessThreshold: 2, remotePrekeyRootWitnessCount: 3, remotePrekeyRootManifestGeneration: 1, remotePrekeyRootRotationProven: true, }, }; renderMessagesView(); await openComposeForRecipient('!sb_invited', 'hello to pinned peer'); expect(screen.queryByText('Unverified First Contact')).not.toBeInTheDocument(); expect(await screen.findByText('ROOT LOCAL QUORUM')).toBeInTheDocument(); expect(await screen.findByText(/Local quorum root rootabcd\.\.123456/i)).toBeInTheDocument(); expect(screen.getByRole('button', { name: 'Send Secure Mail' })).toBeEnabled(); }); it('warms the private lane in the background before sending secure mail', async () => { contactsState = { '!sb_pinned': { alias: 'Pinned Peer', blocked: false, trust_level: 'invite_pinned', dhPubKey: 'peer-dh', remotePrekeyFingerprint: 'abcdef123456', }, }; mocks.fetchWormholeStatus.mockResolvedValue({ ready: false, transport_tier: 'public_degraded' }); renderMessagesView(); await openComposeForRecipient('!sb_pinned', 'hello after warmup'); fireEvent.click(screen.getByRole('button', { name: 'Send Secure Mail' })); await screen.findByText(/Mail delivered to Pinned Peer/i, {}, { timeout: 5000 }); expect(mocks.prepareWormholeInteractiveLane).toHaveBeenCalled(); expect(mocks.sendDmMessage).toHaveBeenCalled(); }, 10000); it('does not flatten witness policy not met into a generic witnessed root label', async () => { contactsState = { '!sb_policy': { alias: 'Policy Peer', blocked: false, trust_level: 'invite_pinned', invitePinnedTrustFingerprint: 'policyfingerprint123456', invitePinnedRootFingerprint: 'rootpolicyabcdef123456', invitePinnedRootManifestFingerprint: 'manifestpolicyabcdef123456', invitePinnedRootWitnessPolicyFingerprint: 'policyabcdef123456', invitePinnedRootWitnessThreshold: 2, invitePinnedRootWitnessCount: 1, invitePinnedRootManifestGeneration: 1, invitePinnedRootRotationProven: true, invitePinnedAt: 123, remotePrekeyFingerprint: 'policyfingerprint123456', remotePrekeyRootFingerprint: 'rootpolicyabcdef123456', remotePrekeyRootManifestFingerprint: 'manifestpolicyabcdef123456', remotePrekeyRootWitnessPolicyFingerprint: 'policyabcdef123456', remotePrekeyRootWitnessThreshold: 2, remotePrekeyRootWitnessCount: 1, remotePrekeyRootManifestGeneration: 1, remotePrekeyRootRotationProven: true, }, }; renderMessagesView(); fireEvent.click(screen.getByRole('button', { name: 'CONTACTS' })); expect(await screen.findByText(/Witness-policy root rootpoli\.\.123456/i)).toBeInTheDocument(); expect(screen.queryByText(/Witnessed root rootpoli\.\.123456/i)).not.toBeInTheDocument(); }); it('shows an import-invite shortcut for unpinned contacts in the contact list', async () => { contactsState = { '!sb_unpinned': { alias: 'Weak Peer', blocked: false, dhPubKey: 'peer-dh', trust_level: 'unpinned', }, }; renderMessagesView(); fireEvent.click(screen.getByRole('button', { name: 'CONTACTS' })); const importButton = await screen.findByRole('button', { name: 'Import Invite' }); fireEvent.click(importButton); expect(screen.getByLabelText(/Local Alias/i)).toHaveValue('!sb_unpinned'); }); it('routes continuity reverify from Secure Messages into Dead Drop with SAS visible', async () => { contactsState = { '!sb_reverify': { alias: 'Broken Root Peer', blocked: false, trust_level: 'continuity_broken', remotePrekeyObservedFingerprint: 'observed123456', remotePrekeyObservedRootFingerprint: 'rootobserved123456', remotePrekeyRootMismatch: true, }, }; const onOpenDeadDrop = vi.fn(); renderMessagesView({ onOpenDeadDrop }); fireEvent.click(screen.getByRole('button', { name: 'CONTACTS' })); const reverifyButton = await screen.findByRole('button', { name: 'REVERIFY NOW' }); fireEvent.click(reverifyButton); expect(onOpenDeadDrop).toHaveBeenCalledWith('!sb_reverify', { showSas: true }); }); it('still blocks first contact when legacy verified flags and a dh key are seeded on an unpinned contact', async () => { contactsState = { '!sb_seeded': { alias: 'Seeded Peer', blocked: false, dhPubKey: 'forged-dh', verify_inband: true, verify_registry: true, verified: true, trust_level: 'unpinned', trustSummary: { state: 'unpinned', label: 'UNVERIFIED', severity: 'warn', detail: 'invite required', verifiedFirstContact: false, recommendedAction: 'import_invite', legacyLookup: false, inviteAttested: false, registryMismatch: false, transparencyConflict: false, }, }, }; renderMessagesView(); await openComposeForRecipient('!sb_seeded', 'hello from forged first contact'); expect(await screen.findByText('Verified First Contact Required')).toBeInTheDocument(); expect( screen.getByText(/Secure request bootstrap is blocked until you import a signed invite/i), ).toBeInTheDocument(); expect(screen.getByRole('button', { name: 'Send Secure Mail' })).toBeDisabled(); }); it('blocks ambient legacy lookup for verified contacts that still lack an invite handle', async () => { contactsState = { '!sb_legacy': { alias: 'Legacy Peer', blocked: false, trust_level: 'sas_verified', remotePrekeyLookupMode: 'legacy_agent_id', trustSummary: { state: 'sas_verified', label: 'SAS VERIFIED', severity: 'good', detail: 'legacy lookup still active', verifiedFirstContact: true, recommendedAction: 'import_invite', legacyLookup: true, inviteAttested: false, registryMismatch: false, transparencyConflict: false, }, }, }; renderMessagesView(); await openComposeForRecipient('!sb_legacy', 'hello from a legacy lookup contact'); fireEvent.click(screen.getByRole('button', { name: 'Send Secure Mail' })); expect( await screen.findByText( /Import or re-import a signed invite before sending a contact request; legacy direct lookup is disabled\./i, ), ).toBeInTheDocument(); expect(mocks.fetchDmPublicKey).not.toHaveBeenCalled(); }); it('announces attested invite imports as INVITE PINNED', async () => { mocks.importWormholeDmInvite.mockResolvedValueOnce({ ok: true, peer_id: '!sb_attested', trust_fingerprint: 'invitefp-attested', trust_level: 'invite_pinned', contact: {}, }); renderMessagesView(); fireEvent.click(screen.getByRole('button', { name: 'CONTACTS' })); expect(await screen.findByText('Import Verified Invite')).toBeInTheDocument(); fireEvent.change(screen.getByLabelText(/Signed Invite JSON/i), { target: { value: JSON.stringify({ invite: { event_type: 'dm_invite', payload: {} } }) }, }); fireEvent.click(screen.getByRole('button', { name: 'Import Signed Invite' })); expect( await screen.findByText(/INVITE PINNED for !sb_attested \(invitefp\.\.tested\)\./i), ).toBeInTheDocument(); }); it('announces compat invite imports as TOFU PINNED with backend detail', async () => { mocks.importWormholeDmInvite.mockResolvedValueOnce({ ok: true, peer_id: '!sb_compat', trust_fingerprint: 'invitefp-compat', trust_level: 'tofu_pinned', detail: 'legacy invite imported as tofu_pinned; SAS verification required before first contact', contact: {}, }); renderMessagesView(); fireEvent.click(screen.getByRole('button', { name: 'CONTACTS' })); expect(await screen.findByText('Import Verified Invite')).toBeInTheDocument(); fireEvent.change(screen.getByLabelText(/Signed Invite JSON/i), { target: { value: JSON.stringify({ invite: { event_type: 'dm_invite', payload: {} } }) }, }); fireEvent.click(screen.getByRole('button', { name: 'Import Signed Invite' })); expect( await screen.findByText(/TOFU PINNED for !sb_compat \(invitefp\.\.compat\)\./i), ).toBeInTheDocument(); expect( screen.getByText(/legacy invite imported as tofu_pinned; SAS verification required before first contact/i), ).toBeInTheDocument(); }); it('surfaces stable root continuity breaks on invite re-import', async () => { contactsState = { '!sb_attested': { alias: 'Pinned Peer', blocked: false, trust_level: 'continuity_broken', invitePinnedTrustFingerprint: 'oldfingerprint123456', invitePinnedRootFingerprint: 'rootold123456', remotePrekeyFingerprint: 'newfingerprint654321', remotePrekeyObservedFingerprint: 'newfingerprint654321', remotePrekeyRootFingerprint: 'rootold123456', remotePrekeyObservedRootFingerprint: 'rootnew654321', remotePrekeyRootMismatch: true, }, }; const error = Object.assign( new Error( 'signed invite root continuity mismatch; re-verify SAS or replace the signed invite before trusting this root change', ), { result: { ok: false, peer_id: '!sb_attested', trust_level: 'continuity_broken', detail: 'signed invite root continuity mismatch; re-verify SAS or replace the signed invite before trusting this root change', contact: {}, }, }, ); mocks.importWormholeDmInvite.mockRejectedValueOnce(error); renderMessagesView(); fireEvent.click(screen.getByRole('button', { name: 'CONTACTS' })); expect(await screen.findByText('Import Verified Invite')).toBeInTheDocument(); fireEvent.change(screen.getByLabelText(/Signed Invite JSON/i), { target: { value: JSON.stringify({ invite: { event_type: 'dm_invite', payload: {} } }) }, }); fireEvent.click(screen.getByRole('button', { name: 'Import Signed Invite' })); expect( await screen.findByText(/CONTINUITY BROKEN for Pinned Peer\. Stable root continuity changed\./i), ).toBeInTheDocument(); expect( screen.getByText(/re-verify SAS in Dead Drop or replace the signed invite before trusting this contact again/i), ).toBeInTheDocument(); }); it('uses non-blocking secure-mail startup language while the DM lane warms', async () => { mocks.fetchWormholeStatus.mockResolvedValueOnce({ ready: false, transport_tier: 'public_degraded' }); mocks.prepareWormholeInteractiveLane.mockImplementation( () => new Promise(() => { /* keep background warm-up pending for this assertion */ }), ); renderMessagesView(); expect( await screen.findByText(/Preparing secure mail in the background/i), ).toBeInTheDocument(); expect(screen.queryByText(/LOCKED/i)).not.toBeInTheDocument(); expect(screen.queryByText(/enter the Wormhole/i)).not.toBeInTheDocument(); }); });