import { Test, TestingModule } from "@nestjs/testing"; import { BridgeModule } from "./bridge.module"; import { DiscordService } from "./discord/discord.service"; import { MatrixService } from "./matrix/matrix.service"; import { StitcherService } from "../stitcher/stitcher.service"; import { PrismaService } from "../prisma/prisma.service"; import { BullMqService } from "../bullmq/bullmq.service"; import { CHAT_PROVIDERS } from "./bridge.constants"; import type { IChatProvider } from "./interfaces"; import { describe, it, expect, beforeEach, afterEach, vi } from "vitest"; // Mock discord.js const mockReadyCallbacks: Array<() => void> = []; const mockClient = { login: vi.fn().mockImplementation(async () => { mockReadyCallbacks.forEach((cb) => cb()); return Promise.resolve(); }), destroy: vi.fn().mockResolvedValue(undefined), on: vi.fn(), once: vi.fn().mockImplementation((event: string, callback: () => void) => { if (event === "ready") { mockReadyCallbacks.push(callback); } }), user: { tag: "TestBot#1234" }, channels: { fetch: vi.fn(), }, guilds: { fetch: vi.fn(), }, }; vi.mock("discord.js", () => { return { Client: class MockClient { login = mockClient.login; destroy = mockClient.destroy; on = mockClient.on; once = mockClient.once; user = mockClient.user; channels = mockClient.channels; guilds = mockClient.guilds; }, Events: { ClientReady: "ready", MessageCreate: "messageCreate", Error: "error", }, GatewayIntentBits: { Guilds: 1 << 0, GuildMessages: 1 << 9, MessageContent: 1 << 15, }, }; }); // Mock matrix-bot-sdk vi.mock("matrix-bot-sdk", () => { return { MatrixClient: class MockMatrixClient { start = vi.fn().mockResolvedValue(undefined); stop = vi.fn(); on = vi.fn(); sendMessage = vi.fn().mockResolvedValue("$mock-event-id"); }, SimpleFsStorageProvider: class MockStorage { constructor(_path: string) { // no-op } }, AutojoinRoomsMixin: { setupOnClient: vi.fn(), }, }; }); /** * Saved environment variables to restore after each test */ interface SavedEnvVars { DISCORD_BOT_TOKEN?: string; DISCORD_GUILD_ID?: string; DISCORD_CONTROL_CHANNEL_ID?: string; MATRIX_ACCESS_TOKEN?: string; MATRIX_HOMESERVER_URL?: string; MATRIX_BOT_USER_ID?: string; MATRIX_CONTROL_ROOM_ID?: string; MATRIX_WORKSPACE_ID?: string; ENCRYPTION_KEY?: string; } describe("BridgeModule", () => { let savedEnv: SavedEnvVars; beforeEach(() => { // Save current env vars savedEnv = { DISCORD_BOT_TOKEN: process.env.DISCORD_BOT_TOKEN, DISCORD_GUILD_ID: process.env.DISCORD_GUILD_ID, DISCORD_CONTROL_CHANNEL_ID: process.env.DISCORD_CONTROL_CHANNEL_ID, MATRIX_ACCESS_TOKEN: process.env.MATRIX_ACCESS_TOKEN, MATRIX_HOMESERVER_URL: process.env.MATRIX_HOMESERVER_URL, MATRIX_BOT_USER_ID: process.env.MATRIX_BOT_USER_ID, MATRIX_CONTROL_ROOM_ID: process.env.MATRIX_CONTROL_ROOM_ID, MATRIX_WORKSPACE_ID: process.env.MATRIX_WORKSPACE_ID, ENCRYPTION_KEY: process.env.ENCRYPTION_KEY, }; // Clear all bridge env vars delete process.env.DISCORD_BOT_TOKEN; delete process.env.DISCORD_GUILD_ID; delete process.env.DISCORD_CONTROL_CHANNEL_ID; delete process.env.MATRIX_ACCESS_TOKEN; delete process.env.MATRIX_HOMESERVER_URL; delete process.env.MATRIX_BOT_USER_ID; delete process.env.MATRIX_CONTROL_ROOM_ID; delete process.env.MATRIX_WORKSPACE_ID; // Set encryption key (needed by StitcherService) process.env.ENCRYPTION_KEY = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"; // Clear ready callbacks mockReadyCallbacks.length = 0; vi.clearAllMocks(); }); afterEach(() => { // Restore env vars for (const [key, value] of Object.entries(savedEnv)) { if (value === undefined) { delete process.env[key]; } else { process.env[key] = value; } } }); /** * Helper to compile a test module with BridgeModule */ async function compileModule(): Promise { return Test.createTestingModule({ imports: [BridgeModule], }) .overrideProvider(PrismaService) .useValue({}) .overrideProvider(BullMqService) .useValue({}) .compile(); } /** * Helper to set Discord env vars */ function setDiscordEnv(): void { process.env.DISCORD_BOT_TOKEN = "test-discord-token"; process.env.DISCORD_GUILD_ID = "test-guild-id"; process.env.DISCORD_CONTROL_CHANNEL_ID = "test-channel-id"; } /** * Helper to set Matrix env vars */ function setMatrixEnv(): void { process.env.MATRIX_ACCESS_TOKEN = "test-matrix-token"; process.env.MATRIX_HOMESERVER_URL = "https://matrix.example.com"; process.env.MATRIX_BOT_USER_ID = "@bot:example.com"; process.env.MATRIX_CONTROL_ROOM_ID = "!room:example.com"; process.env.MATRIX_WORKSPACE_ID = "test-workspace-id"; } describe("with both Discord and Matrix configured", () => { let module: TestingModule; beforeEach(async () => { setDiscordEnv(); setMatrixEnv(); module = await compileModule(); }); it("should compile the module", () => { expect(module).toBeDefined(); }); it("should provide DiscordService", () => { const discordService = module.get(DiscordService); expect(discordService).toBeDefined(); expect(discordService).toBeInstanceOf(DiscordService); }); it("should provide MatrixService", () => { const matrixService = module.get(MatrixService); expect(matrixService).toBeDefined(); expect(matrixService).toBeInstanceOf(MatrixService); }); it("should provide CHAT_PROVIDERS with both providers", () => { const chatProviders = module.get(CHAT_PROVIDERS); expect(chatProviders).toBeDefined(); expect(chatProviders).toHaveLength(2); expect(chatProviders[0]).toBeInstanceOf(DiscordService); expect(chatProviders[1]).toBeInstanceOf(MatrixService); }); it("should provide StitcherService via StitcherModule", () => { const stitcherService = module.get(StitcherService); expect(stitcherService).toBeDefined(); expect(stitcherService).toBeInstanceOf(StitcherService); }); }); describe("with only Discord configured", () => { let module: TestingModule; beforeEach(async () => { setDiscordEnv(); module = await compileModule(); }); it("should compile the module", () => { expect(module).toBeDefined(); }); it("should provide DiscordService", () => { const discordService = module.get(DiscordService); expect(discordService).toBeDefined(); expect(discordService).toBeInstanceOf(DiscordService); }); it("should provide CHAT_PROVIDERS with only Discord", () => { const chatProviders = module.get(CHAT_PROVIDERS); expect(chatProviders).toBeDefined(); expect(chatProviders).toHaveLength(1); expect(chatProviders[0]).toBeInstanceOf(DiscordService); }); }); describe("with only Matrix configured", () => { let module: TestingModule; beforeEach(async () => { setMatrixEnv(); module = await compileModule(); }); it("should compile the module", () => { expect(module).toBeDefined(); }); it("should provide MatrixService", () => { const matrixService = module.get(MatrixService); expect(matrixService).toBeDefined(); expect(matrixService).toBeInstanceOf(MatrixService); }); it("should provide CHAT_PROVIDERS with only Matrix", () => { const chatProviders = module.get(CHAT_PROVIDERS); expect(chatProviders).toBeDefined(); expect(chatProviders).toHaveLength(1); expect(chatProviders[0]).toBeInstanceOf(MatrixService); }); }); describe("with neither bridge configured", () => { let module: TestingModule; beforeEach(async () => { // No env vars set for either bridge module = await compileModule(); }); it("should compile the module without errors", () => { expect(module).toBeDefined(); }); it("should provide CHAT_PROVIDERS as an empty array", () => { const chatProviders = module.get(CHAT_PROVIDERS); expect(chatProviders).toBeDefined(); expect(chatProviders).toHaveLength(0); expect(Array.isArray(chatProviders)).toBe(true); }); }); describe("CHAT_PROVIDERS token", () => { it("should be a string constant", () => { expect(CHAT_PROVIDERS).toBe("CHAT_PROVIDERS"); expect(typeof CHAT_PROVIDERS).toBe("string"); }); }); });