mirror of
https://github.com/BigBodyCobain/Shadowbroker.git
synced 2026-06-10 08:13:58 +02:00
8d3c7a51b7
Render the app shell dynamically so Next can attach per-request CSP nonces to its production scripts, preventing Docker from serving a static shell that cannot hydrate. Also gives the first-contact warmup test enough time in CI.
570 lines
23 KiB
TypeScript
570 lines
23 KiB
TypeScript
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<string, any> = {};
|
|
|
|
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<string, unknown>)
|
|
? (((error as Record<string, unknown>).result as Record<string, unknown>) || 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(<MessagesView onBack={() => {}} 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();
|
|
});
|
|
});
|