feat(#379): Register MatrixService in BridgeModule with conditional loading
Some checks failed
ci/woodpecker/push/api Pipeline failed
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
This commit is contained in:
15
apps/api/src/bridge/bridge.constants.ts
Normal file
15
apps/api/src/bridge/bridge.constants.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
/**
|
||||
* Bridge Module Constants
|
||||
*
|
||||
* Injection tokens for the bridge module.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Injection token for the array of active IChatProvider instances.
|
||||
*
|
||||
* Use this token to inject all configured chat providers:
|
||||
* ```
|
||||
* @Inject(CHAT_PROVIDERS) private readonly chatProviders: IChatProvider[]
|
||||
* ```
|
||||
*/
|
||||
export const CHAT_PROVIDERS = "CHAT_PROVIDERS";
|
||||
@@ -1,10 +1,13 @@
|
||||
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 { describe, it, expect, beforeEach, vi } from "vitest";
|
||||
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> = [];
|
||||
@@ -53,20 +56,93 @@ vi.mock("discord.js", () => {
|
||||
};
|
||||
});
|
||||
|
||||
describe("BridgeModule", () => {
|
||||
let module: TestingModule;
|
||||
// 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(),
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
// Set environment variables
|
||||
process.env.DISCORD_BOT_TOKEN = "test-token";
|
||||
process.env.DISCORD_GUILD_ID = "test-guild-id";
|
||||
process.env.DISCORD_CONTROL_CHANNEL_ID = "test-channel-id";
|
||||
/**
|
||||
* 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;
|
||||
|
||||
module = await Test.createTestingModule({
|
||||
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)
|
||||
@@ -74,24 +150,144 @@ describe("BridgeModule", () => {
|
||||
.overrideProvider(BullMqService)
|
||||
.useValue({})
|
||||
.compile();
|
||||
}
|
||||
|
||||
// Clear all mocks
|
||||
vi.clearAllMocks();
|
||||
/**
|
||||
* 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);
|
||||
});
|
||||
});
|
||||
|
||||
it("should be defined", () => {
|
||||
expect(module).toBeDefined();
|
||||
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);
|
||||
});
|
||||
});
|
||||
|
||||
it("should provide DiscordService", () => {
|
||||
const discordService = module.get<DiscordService>(DiscordService);
|
||||
expect(discordService).toBeDefined();
|
||||
expect(discordService).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);
|
||||
});
|
||||
});
|
||||
|
||||
it("should provide StitcherService", () => {
|
||||
const stitcherService = module.get<StitcherService>(StitcherService);
|
||||
expect(stitcherService).toBeDefined();
|
||||
expect(stitcherService).toBeInstanceOf(StitcherService);
|
||||
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");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,16 +1,55 @@
|
||||
import { Module } from "@nestjs/common";
|
||||
import { Logger, Module } from "@nestjs/common";
|
||||
import { DiscordService } from "./discord/discord.service";
|
||||
import { MatrixService } from "./matrix/matrix.service";
|
||||
import { StitcherModule } from "../stitcher/stitcher.module";
|
||||
import { CHAT_PROVIDERS } from "./bridge.constants";
|
||||
import type { IChatProvider } from "./interfaces";
|
||||
|
||||
const logger = new Logger("BridgeModule");
|
||||
|
||||
/**
|
||||
* Bridge Module - Chat platform integrations
|
||||
*
|
||||
* Provides integration with chat platforms (Discord, Slack, Matrix, etc.)
|
||||
* Provides integration with chat platforms (Discord, Matrix, etc.)
|
||||
* for controlling Mosaic Stack via chat commands.
|
||||
*
|
||||
* Both services are always registered as providers, but the CHAT_PROVIDERS
|
||||
* injection token only includes bridges whose environment variables are set:
|
||||
* - Discord: included when DISCORD_BOT_TOKEN is set
|
||||
* - Matrix: included when MATRIX_ACCESS_TOKEN is set
|
||||
*
|
||||
* Both bridges can run simultaneously, and no error occurs if neither is configured.
|
||||
* Consumers should inject CHAT_PROVIDERS for bridge-agnostic access to all active providers.
|
||||
*/
|
||||
@Module({
|
||||
imports: [StitcherModule],
|
||||
providers: [DiscordService],
|
||||
exports: [DiscordService],
|
||||
providers: [
|
||||
DiscordService,
|
||||
MatrixService,
|
||||
{
|
||||
provide: CHAT_PROVIDERS,
|
||||
useFactory: (discord: DiscordService, matrix: MatrixService): IChatProvider[] => {
|
||||
const providers: IChatProvider[] = [];
|
||||
|
||||
if (process.env.DISCORD_BOT_TOKEN) {
|
||||
providers.push(discord);
|
||||
logger.log("Discord bridge enabled (DISCORD_BOT_TOKEN detected)");
|
||||
}
|
||||
|
||||
if (process.env.MATRIX_ACCESS_TOKEN) {
|
||||
providers.push(matrix);
|
||||
logger.log("Matrix bridge enabled (MATRIX_ACCESS_TOKEN detected)");
|
||||
}
|
||||
|
||||
if (providers.length === 0) {
|
||||
logger.warn("No chat bridges configured. Set DISCORD_BOT_TOKEN or MATRIX_ACCESS_TOKEN.");
|
||||
}
|
||||
|
||||
return providers;
|
||||
},
|
||||
inject: [DiscordService, MatrixService],
|
||||
},
|
||||
],
|
||||
exports: [DiscordService, MatrixService, CHAT_PROVIDERS],
|
||||
})
|
||||
export class BridgeModule {}
|
||||
|
||||
Reference in New Issue
Block a user