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 { Test, TestingModule } from "@nestjs/testing";
|
||||||
import { BridgeModule } from "./bridge.module";
|
import { BridgeModule } from "./bridge.module";
|
||||||
import { DiscordService } from "./discord/discord.service";
|
import { DiscordService } from "./discord/discord.service";
|
||||||
|
import { MatrixService } from "./matrix/matrix.service";
|
||||||
import { StitcherService } from "../stitcher/stitcher.service";
|
import { StitcherService } from "../stitcher/stitcher.service";
|
||||||
import { PrismaService } from "../prisma/prisma.service";
|
import { PrismaService } from "../prisma/prisma.service";
|
||||||
import { BullMqService } from "../bullmq/bullmq.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
|
// Mock discord.js
|
||||||
const mockReadyCallbacks: Array<() => void> = [];
|
const mockReadyCallbacks: Array<() => void> = [];
|
||||||
@@ -53,20 +56,93 @@ vi.mock("discord.js", () => {
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("BridgeModule", () => {
|
// Mock matrix-bot-sdk
|
||||||
let module: TestingModule;
|
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
|
* Saved environment variables to restore after each test
|
||||||
process.env.DISCORD_BOT_TOKEN = "test-token";
|
*/
|
||||||
process.env.DISCORD_GUILD_ID = "test-guild-id";
|
interface SavedEnvVars {
|
||||||
process.env.DISCORD_CONTROL_CHANNEL_ID = "test-channel-id";
|
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";
|
process.env.ENCRYPTION_KEY = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef";
|
||||||
|
|
||||||
// Clear ready callbacks
|
// Clear ready callbacks
|
||||||
mockReadyCallbacks.length = 0;
|
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],
|
imports: [BridgeModule],
|
||||||
})
|
})
|
||||||
.overrideProvider(PrismaService)
|
.overrideProvider(PrismaService)
|
||||||
@@ -74,12 +150,38 @@ describe("BridgeModule", () => {
|
|||||||
.overrideProvider(BullMqService)
|
.overrideProvider(BullMqService)
|
||||||
.useValue({})
|
.useValue({})
|
||||||
.compile();
|
.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 be defined", () => {
|
it("should compile the module", () => {
|
||||||
expect(module).toBeDefined();
|
expect(module).toBeDefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -89,9 +191,103 @@ describe("BridgeModule", () => {
|
|||||||
expect(discordService).toBeInstanceOf(DiscordService);
|
expect(discordService).toBeInstanceOf(DiscordService);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should provide StitcherService", () => {
|
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);
|
const stitcherService = module.get<StitcherService>(StitcherService);
|
||||||
expect(stitcherService).toBeDefined();
|
expect(stitcherService).toBeDefined();
|
||||||
expect(stitcherService).toBeInstanceOf(StitcherService);
|
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");
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,16 +1,55 @@
|
|||||||
import { Module } from "@nestjs/common";
|
import { Logger, Module } from "@nestjs/common";
|
||||||
import { DiscordService } from "./discord/discord.service";
|
import { DiscordService } from "./discord/discord.service";
|
||||||
|
import { MatrixService } from "./matrix/matrix.service";
|
||||||
import { StitcherModule } from "../stitcher/stitcher.module";
|
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
|
* 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.
|
* 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({
|
@Module({
|
||||||
imports: [StitcherModule],
|
imports: [StitcherModule],
|
||||||
providers: [DiscordService],
|
providers: [
|
||||||
exports: [DiscordService],
|
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 {}
|
export class BridgeModule {}
|
||||||
|
|||||||
Reference in New Issue
Block a user