Some checks failed
ci/woodpecker/push/api Pipeline failed
- Add CHAT_PROVIDERS injection token for bridge-agnostic access - Conditional loading based on env vars (DISCORD_BOT_TOKEN, MATRIX_ACCESS_TOKEN) - Both bridges can run simultaneously - No crash if neither bridge is configured - Tests verify all configuration combinations Refs #379
294 lines
8.7 KiB
TypeScript
294 lines
8.7 KiB
TypeScript
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<TestingModule> {
|
|
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>(DiscordService);
|
|
expect(discordService).toBeDefined();
|
|
expect(discordService).toBeInstanceOf(DiscordService);
|
|
});
|
|
|
|
it("should provide MatrixService", () => {
|
|
const matrixService = module.get<MatrixService>(MatrixService);
|
|
expect(matrixService).toBeDefined();
|
|
expect(matrixService).toBeInstanceOf(MatrixService);
|
|
});
|
|
|
|
it("should provide CHAT_PROVIDERS with both providers", () => {
|
|
const chatProviders = module.get<IChatProvider[]>(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>(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>(DiscordService);
|
|
expect(discordService).toBeDefined();
|
|
expect(discordService).toBeInstanceOf(DiscordService);
|
|
});
|
|
|
|
it("should provide CHAT_PROVIDERS with only Discord", () => {
|
|
const chatProviders = module.get<IChatProvider[]>(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>(MatrixService);
|
|
expect(matrixService).toBeDefined();
|
|
expect(matrixService).toBeInstanceOf(MatrixService);
|
|
});
|
|
|
|
it("should provide CHAT_PROVIDERS with only Matrix", () => {
|
|
const chatProviders = module.get<IChatProvider[]>(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<IChatProvider[]>(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");
|
|
});
|
|
});
|
|
});
|