/** * Tests verifying the trusted contact flow is channel-agnostic. * * The access request -> guardian notification -> verification -> activation * flow should work identically across all channels. * These tests confirm no channel-specific assumptions leaked into the * trusted contact code paths. */ import { beforeEach, describe, expect, mock, test } from "bun:test"; // --------------------------------------------------------------------------- // Test isolation: in-memory SQLite via temp directory // --------------------------------------------------------------------------- mock.module("../config/env.js", () => ({ isHttpAuthDisabled: () => true, getGatewayInternalBaseUrl: () => "http://127.0.0.1:7830", })); const emitSignalCalls: Array> = []; mock.module("../notifications/emit-signal.js", () => ({ emitNotificationSignal: async (params: Record) => { emitSignalCalls.push(params); return { signalId: "mock-signal-id", deduplicated: false, dispatched: true, reason: "mock", deliveryResults: [], }; }, })); // Mock access-request-helper directly to capture notification calls. // Bun's mock.module does not intercept transitive imports reliably, so // mocking emit-signal.js alone is not sufficient — access-request-helper // imports emit-signal before the mock takes effect. const notifyGuardianCalls: Array> = []; mock.module("../runtime/access-request-helper.js", () => ({ notifyGuardianOfAccessRequest: (params: Record) => { notifyGuardianCalls.push(params); return { notified: true, created: true, requestId: `mock-req-${Date.now()}`, }; }, })); const deliverReplyCalls: Array<{ url: string; payload: Record; }> = []; mock.module("../runtime/gateway-client.js", () => ({ deliverChannelReply: async ( url: string, payload: Record, ) => { deliverReplyCalls.push({ url, payload }); }, })); mock.module("../runtime/approval-message-composer.js", () => ({ composeApprovalMessage: () => "mock approval message", composeApprovalMessageGenerative: async () => "mock generative message", })); import { findContactChannel } from "../contacts/contact-store.js"; import { getDb } from "../persistence/db-connection.js"; import { initializeDb } from "../persistence/db-init.js"; import { handleChannelInbound, seedContactChannel, } from "./helpers/channel-test-adapter.js"; import { createOutboundSession, createOutboundSessionGuarded, findActiveSession, getPendingSession, resetVerificationSessionsSim, validateAndConsumeVerification, } from "./helpers/verification-sessions-ipc-sim.js"; // The inbound stages read/write sessions via the gateway-backed IPC client; // delegate it to the in-memory sim so the challenge lanes keep running // without a live gateway. mock.module("../channels/gateway-verification-sessions.js", () => ({ createOutboundSession: async ( params: Parameters[0], ) => createOutboundSession(params), createOutboundSessionConditional: async ( params: Parameters[0], ) => createOutboundSessionGuarded(params), getPendingSession: async (channel: string) => getPendingSession(channel), findActiveSession: async ( channel: string, filter?: Parameters[1], ) => findActiveSession(channel, filter), })); import { createGuardianBinding } from "./helpers/create-guardian-binding.js"; import { gatewayAclByChannelId, resetGatewayAclStore, } from "./helpers/gateway-acl-store.js"; await initializeDb(); // --------------------------------------------------------------------------- // Helpers // --------------------------------------------------------------------------- const TEST_BEARER_TOKEN = "test-token"; function resetState(): void { resetVerificationSessionsSim(); const db = getDb(); db.run("DELETE FROM channel_inbound_events"); db.run("DELETE FROM conversations"); db.run("DELETE FROM notification_events"); db.run("DELETE FROM contact_channels"); db.run("DELETE FROM contacts"); resetGatewayAclStore(); emitSignalCalls.length = 0; notifyGuardianCalls.length = 0; deliverReplyCalls.length = 0; } interface ChannelTestConfig { channel: "telegram" | "slack"; deliverEndpoint: string; senderExternalUserId: string; externalChatId: string; guardianExternalUserId: string; guardianChatId: string; } const CHANNEL_CONFIGS: ChannelTestConfig[] = [ { channel: "telegram", deliverEndpoint: "/deliver/telegram", senderExternalUserId: "tg-user-456", externalChatId: "tg-chat-456", guardianExternalUserId: "tg-guardian-789", guardianChatId: "tg-guardian-chat-789", }, { channel: "slack", deliverEndpoint: "/deliver/slack", senderExternalUserId: "U0123ABCDEF", externalChatId: "C0123ABCDEF", guardianExternalUserId: "U9876ZYXWVU", guardianChatId: "C9876ZYXWVU", }, ]; function buildInboundRequest( config: ChannelTestConfig, overrides: Record = {}, ): Request { const body: Record = { sourceChannel: config.channel, interface: config.channel, conversationExternalId: config.externalChatId, externalMessageId: `msg-${Date.now()}-${Math.random() .toString(36) .slice(2, 8)}`, content: "Hello, can I use this assistant?", actorExternalId: config.senderExternalUserId, actorDisplayName: "Test Requester", actorUsername: "test_requester", replyCallbackUrl: `http://localhost:7830${config.deliverEndpoint}`, ...overrides, }; return new Request("http://localhost:8080/channels/inbound", { method: "POST", headers: { "Content-Type": "application/json", "X-Gateway-Origin": TEST_BEARER_TOKEN, }, body: JSON.stringify(body), }); } // --------------------------------------------------------------------------- // Parameterized tests for each channel // --------------------------------------------------------------------------- for (const config of CHANNEL_CONFIGS) { describe(`trusted contact flow on ${config.channel} channel`, () => { beforeEach(() => { resetState(); }); test("non-member message is denied with rejection reply", async () => { const req = buildInboundRequest(config); const resp = await handleChannelInbound( req, undefined, TEST_BEARER_TOKEN, ); const json = (await resp.json()) as Record; expect(json.denied).toBe(true); // Slack sends a verification challenge instead of a flat rejection if (config.channel === "slack") { expect(json.reason).toBe("verification_challenge_sent"); } else { expect(json.reason).toBe("not_a_member"); } expect(deliverReplyCalls.length).toBe(1); const replyText = ( deliverReplyCalls[0].payload as Record ).text as string; if (config.channel === "slack") { expect(replyText).toContain("verification code"); } else { expect( replyText.includes("you haven't been approved") || replyText.includes("you don't have access"), ).toBe(true); } }); test("guardian is notified when a non-member messages", async () => { createGuardianBinding({ channel: config.channel, guardianExternalUserId: config.guardianExternalUserId, guardianDeliveryChatId: config.guardianChatId, guardianPrincipalId: config.guardianExternalUserId, verifiedVia: "test", }); const req = buildInboundRequest(config); const resp = await handleChannelInbound( req, undefined, TEST_BEARER_TOKEN, ); const json = (await resp.json()) as Record; expect(json.denied).toBe(true); // Guardian notification helper was called for the correct channel expect(notifyGuardianCalls.length).toBe(1); expect(notifyGuardianCalls[0].sourceChannel).toBe(config.channel); expect(notifyGuardianCalls[0].actorExternalId).toBe( config.senderExternalUserId, ); }); test("verification creates active member for channel", () => { const session = createOutboundSession({ channel: config.channel, expectedExternalUserId: config.senderExternalUserId, expectedChatId: config.externalChatId, identityBindingStatus: "bound", destinationAddress: config.externalChatId, verificationPurpose: "trusted_contact", }); const challengeResult = validateAndConsumeVerification( config.channel, session.secret, config.senderExternalUserId, config.externalChatId, ); expect(challengeResult.success).toBe(true); if (challengeResult.success) { expect(challengeResult.verificationType).toBe("trusted_contact"); } seedContactChannel({ sourceChannel: config.channel, externalUserId: config.senderExternalUserId, externalChatId: config.externalChatId, status: "active", policy: "allow", displayName: "Test Requester", username: "test_requester", }); const contactResult = findContactChannel({ channelType: config.channel, address: config.senderExternalUserId, }); expect(contactResult).not.toBeNull(); // Assert the gateway-resolved ACL landed in the gateway ACL store. const acl = gatewayAclByChannelId(contactResult!.channel.id); expect(acl!.status).toBe("active"); expect(acl!.policy).toBe("allow"); expect(contactResult!.channel.type).toBe(config.channel); }); test("no cross-channel leakage between member records", () => { // Create a member for this channel seedContactChannel({ sourceChannel: config.channel, externalUserId: config.senderExternalUserId, externalChatId: config.externalChatId, status: "active", policy: "allow", }); // Should be found on this channel const sameChanResult = findContactChannel({ channelType: config.channel, address: config.senderExternalUserId, }); expect(sameChanResult).not.toBeNull(); // Should NOT be found on a different channel const otherChannel = config.channel === "telegram" ? "slack" : "telegram"; const crossChanResult = findContactChannel({ channelType: otherChannel, address: config.senderExternalUserId, }); expect(crossChanResult).toBeNull(); }); }); } // --------------------------------------------------------------------------- // Voice-specific: phone E.164 identity binding // --------------------------------------------------------------------------- describe("voice identity binding with E.164 phone numbers", () => { beforeEach(() => { resetState(); }); test("voice verification session binds to phone E.164", () => { const phone = "+15551234567"; const session = createOutboundSession({ channel: "phone", expectedExternalUserId: phone, expectedPhoneE164: phone, expectedChatId: phone, identityBindingStatus: "bound", destinationAddress: phone, verificationPurpose: "trusted_contact", }); // Verify with matching phone identity const result = validateAndConsumeVerification( "phone", session.secret, phone, phone, ); expect(result.success).toBe(true); if (result.success) { expect(result.verificationType).toBe("trusted_contact"); } }); test("voice verification rejects mismatched phone identity", () => { const expectedPhone = "+15551234567"; const wrongPhone = "+15559999999"; const session = createOutboundSession({ channel: "phone", expectedExternalUserId: expectedPhone, expectedPhoneE164: expectedPhone, expectedChatId: expectedPhone, identityBindingStatus: "bound", destinationAddress: expectedPhone, }); // Try to verify with a different phone (anti-oracle: same error message) const result = validateAndConsumeVerification( "phone", session.secret, wrongPhone, wrongPhone, ); expect(result.success).toBe(false); }); }); // --------------------------------------------------------------------------- // Cross-channel: same user on different channels gets separate sessions // --------------------------------------------------------------------------- describe("cross-channel isolation", () => { beforeEach(() => { resetState(); }); test("verification sessions are scoped per channel", () => { // Create sessions on both channels const telegramSession = createOutboundSession({ channel: "telegram", expectedExternalUserId: "user-123", expectedChatId: "chat-123", identityBindingStatus: "bound", destinationAddress: "chat-123", }); const slackSession = createOutboundSession({ channel: "slack", expectedExternalUserId: "U0123ABCDEF", expectedChatId: "C0123ABCDEF", identityBindingStatus: "bound", destinationAddress: "C0123ABCDEF", }); // Telegram code should not work on Slack channel const wrongChannelResult = validateAndConsumeVerification( "slack", telegramSession.secret, "U0123ABCDEF", "C0123ABCDEF", ); expect(wrongChannelResult.success).toBe(false); // Slack code should work on Slack channel const correctChannelResult = validateAndConsumeVerification( "slack", slackSession.secret, "U0123ABCDEF", "C0123ABCDEF", ); expect(correctChannelResult.success).toBe(true); }); });