import { prisma } from "@langfuse/shared/src/db"; import type { Session } from "next-auth"; import { encrypt } from "@langfuse/shared/encryption"; import { createInnerTRPCContext } from "@/src/server/api/trpc"; import { appRouter } from "@/src/server/api/root"; import { createOrgProjectAndApiKey } from "@langfuse/shared/src/server"; import { TRPCError } from "@trpc/server"; // Mock SlackService jest.mock("@langfuse/shared/src/server", () => { const actual = jest.requireActual("@langfuse/shared/src/server"); return { ...actual, SlackService: { getInstance: jest.fn(), }, }; }); const __orgIds: string[] = []; let mockSlackService: any; const prepare = async () => { const { project, org } = await createOrgProjectAndApiKey(); const session: Session = { expires: "1", user: { id: "user-1", canCreateOrganizations: true, name: "Demo User", organizations: [ { id: org.id, name: org.name, role: "OWNER", plan: "cloud:hobby", cloudConfig: undefined, metadata: {}, projects: [ { id: project.id, role: "ADMIN", retentionDays: 30, deletedAt: null, name: project.name, metadata: {}, }, ], }, ], featureFlags: { excludeClickhouseRead: false, templateFlag: true, }, admin: true, }, environment: { enableExperimentalFeatures: false, selfHostedInstancePlan: "cloud:hobby", }, }; const ctx = createInnerTRPCContext({ session, headers: {} }); const caller = appRouter.createCaller({ ...ctx, prisma }); __orgIds.push(org.id); return { project, org, session, ctx, caller }; }; describe("Slack Integration", () => { beforeAll(async () => { // Import mocked SlackService const { SlackService } = await import("@langfuse/shared/src/server"); // Create mock service instance mockSlackService = { getWebClientForProject: jest.fn(), sendMessage: jest.fn(), getChannels: jest.fn(), validateClient: jest.fn(), deleteIntegration: jest.fn(), }; // Setup the getInstance mock to return our mock service (SlackService.getInstance as jest.Mock).mockReturnValue(mockSlackService); }); afterAll(async () => { await prisma.organization.deleteMany({ where: { id: { in: __orgIds }, }, }); }); describe("Slack tRPC Router", () => { describe("getIntegrationStatus", () => { it("should return connected status for valid integration", async () => { mockSlackService.validateClient.mockResolvedValue(true); const { caller, project } = await prepare(); // Create Slack integration await prisma.slackIntegration.create({ data: { projectId: project.id, teamId: "T123456", teamName: "Test Team", botToken: encrypt("xoxb-test-token-secret"), botUserId: "U123456", }, }); const result = await caller.slack.getIntegrationStatus({ projectId: project.id, }); expect(result).toMatchObject({ isConnected: true, teamId: "T123456", teamName: "Test Team", botUserId: "U123456", installUrl: null, }); // 🔒 CRITICAL: Ensure no bot token is exposed expect(JSON.stringify(result)).not.toContain("xoxb-test-token-secret"); expect(result).not.toHaveProperty("botToken"); }); it("should return disconnected status when no integration exists", async () => { const { caller, project } = await prepare(); const result = await caller.slack.getIntegrationStatus({ projectId: project.id, }); expect(result).toMatchObject({ isConnected: false, teamId: null, teamName: null, installUrl: expect.stringContaining( `/api/public/slack/install?projectId=${project.id}`, ), }); // 🔒 Ensure no sensitive data is present expect(JSON.stringify(result)).not.toContain("xoxb-"); }); it("should return disconnected status for invalid integration", async () => { mockSlackService.validateClient.mockResolvedValue(false); const { caller, project } = await prepare(); // Create Slack integration with invalid token await prisma.slackIntegration.create({ data: { projectId: project.id, teamId: "T123456", teamName: "Test Team", botToken: encrypt("xoxb-invalid-token"), botUserId: "U123456", }, }); const result = await caller.slack.getIntegrationStatus({ projectId: project.id, }); expect(result).toMatchObject({ isConnected: false, teamId: "T123456", teamName: "Test Team", error: "Integration is invalid. Please reconnect your Slack workspace.", }); // 🔒 CRITICAL: Ensure no bot token is exposed even for invalid integrations expect(JSON.stringify(result)).not.toContain("xoxb-invalid-token"); }); }); describe("getChannels", () => { it("should fetch channels for valid integration", async () => { const mockChannels = [ { id: "C123456", name: "general", isPrivate: false, isMember: true }, { id: "C789012", name: "random", isPrivate: false, isMember: true }, { id: "C345678", name: "private-channel", isPrivate: true, isMember: true, }, ]; mockSlackService.getChannels.mockResolvedValue(mockChannels); const { caller, project } = await prepare(); // Create Slack integration await prisma.slackIntegration.create({ data: { projectId: project.id, teamId: "T123456", teamName: "Test Team", botToken: encrypt("xoxb-valid-token"), botUserId: "U123456", }, }); const result = await caller.slack.getChannels({ projectId: project.id, }); expect(result).toMatchObject({ channels: mockChannels, teamId: "T123456", teamName: "Test Team", }); // 🔒 CRITICAL: Ensure no bot token is exposed in channel data expect(JSON.stringify(result)).not.toContain("xoxb-valid-token"); expect(result).not.toHaveProperty("botToken"); }); it("should throw NOT_FOUND for missing integration", async () => { const { caller, project } = await prepare(); await expect( caller.slack.getChannels({ projectId: project.id }), ).rejects.toThrow("Slack integration not found"); }); it("should handle Slack API failures gracefully", async () => { mockSlackService.getChannels.mockRejectedValue( new Error("Slack API error"), ); const { caller, project } = await prepare(); // Create Slack integration await prisma.slackIntegration.create({ data: { projectId: project.id, teamId: "T123456", teamName: "Test Team", botToken: encrypt("xoxb-test-token"), botUserId: "U123456", }, }); await expect( caller.slack.getChannels({ projectId: project.id }), ).rejects.toThrow( "Failed to fetch channels. Please check your Slack connection and try again.", ); }); }); describe("sendTestMessage", () => { it("should send test message successfully", async () => { const mockClient = { auth: { test: jest.fn() } }; mockSlackService.getWebClientForProject.mockResolvedValue(mockClient); mockSlackService.sendMessage.mockResolvedValue({ messageTs: "1234567890.123456", channel: "C123456", }); const { caller, project } = await prepare(); // Create Slack integration await prisma.slackIntegration.create({ data: { projectId: project.id, teamId: "T123456", teamName: "Test Team", botToken: encrypt("xoxb-test-token"), botUserId: "U123456", }, }); const result = await caller.slack.sendTestMessage({ projectId: project.id, channelId: "C123456", channelName: "general", }); expect(result).toMatchObject({ success: true, messageTs: "1234567890.123456", channel: "C123456", }); // Verify SlackService was called with proper parameters expect(mockSlackService.sendMessage).toHaveBeenCalledWith({ client: expect.any(Object), channelId: "C123456", blocks: expect.any(Array), text: "Test message from Langfuse", }); // 🔒 CRITICAL: Ensure no bot token is exposed in test results expect(JSON.stringify(result)).not.toContain("xoxb-test-token"); }); it("should create audit log entry", async () => { const mockClient = { auth: { test: jest.fn() } }; mockSlackService.getWebClientForProject.mockResolvedValue(mockClient); mockSlackService.sendMessage.mockResolvedValue({ messageTs: "1234567890.123456", channel: "C123456", }); const { caller, project } = await prepare(); // Create Slack integration const integration = await prisma.slackIntegration.create({ data: { projectId: project.id, teamId: "T123456", teamName: "Test Team", botToken: encrypt("xoxb-test-token"), botUserId: "U123456", }, }); await caller.slack.sendTestMessage({ projectId: project.id, channelId: "C123456", channelName: "general", }); // Verify audit log was created const auditLog = await prisma.auditLog.findFirst({ where: { projectId: project.id, resourceType: "slackIntegration", resourceId: integration.id, action: "create", }, }); expect(auditLog).toBeDefined(); const afterData = auditLog?.after ? JSON.parse(auditLog.after) : null; expect(afterData).toMatchObject({ action: "test_message_sent", channelId: "C123456", channelName: "general", messageTs: "1234567890.123456", }); // 🔒 Ensure audit log doesn't contain tokens expect(JSON.stringify(afterData)).not.toContain("xoxb-"); }); }); describe("disconnect", () => { it("should remove integration and audit log it", async () => { mockSlackService.deleteIntegration.mockResolvedValue(undefined); const { caller, project } = await prepare(); // Create Slack integration const integration = await prisma.slackIntegration.create({ data: { projectId: project.id, teamId: "T123456", teamName: "Test Team", botToken: encrypt("xoxb-test-token"), botUserId: "U123456", }, }); const result = await caller.slack.disconnect({ projectId: project.id, }); expect(result).toMatchObject({ success: true, }); // Verify SlackService was called expect(mockSlackService.deleteIntegration).toHaveBeenCalledWith( project.id, ); // Verify audit log was created const auditLog = await prisma.auditLog.findFirst({ where: { projectId: project.id, resourceType: "slackIntegration", resourceId: integration.id, action: "delete", }, }); expect(auditLog).toBeDefined(); const beforeData = auditLog?.before ? JSON.parse(auditLog.before) : null; expect(beforeData).toMatchObject({ projectId: project.id, teamId: "T123456", teamName: "Test Team", }); // 🔒 CRITICAL: Ensure audit log doesn't expose encrypted bot token expect(JSON.stringify(beforeData)).not.toContain("xoxb-"); // But encrypted token should be in the audit log for recovery purposes expect(beforeData).toHaveProperty("botToken"); }); it("should handle missing integration gracefully", async () => { const { caller, project } = await prepare(); await expect( caller.slack.disconnect({ projectId: project.id }), ).rejects.toThrow("Slack integration not found"); }); }); }); describe("Slack Security", () => { it("should encrypt bot tokens in database", async () => { const { project } = await prepare(); const originalToken = "xoxb-secret-bot-token-12345"; // Create Slack integration await prisma.slackIntegration.create({ data: { projectId: project.id, teamId: "T123456", teamName: "Test Team", botToken: encrypt(originalToken), botUserId: "U123456", }, }); // Verify token is encrypted in database const rawIntegration = await prisma.slackIntegration.findUnique({ where: { projectId: project.id }, }); expect(rawIntegration?.botToken).toBeDefined(); expect(rawIntegration?.botToken).not.toBe(originalToken); expect(rawIntegration?.botToken).not.toContain("xoxb-secret-bot-token"); // Verify the encrypted token can be decrypted back to original const { decrypt } = await import("@langfuse/shared/encryption"); const decryptedToken = decrypt(rawIntegration!.botToken); expect(decryptedToken).toBe(originalToken); }); it("should NEVER expose raw bot tokens in any API response", async () => { mockSlackService.validateClient.mockResolvedValue(true); mockSlackService.getChannels.mockResolvedValue([ { id: "C123456", name: "general", isPrivate: false, isMember: true }, ]); mockSlackService.sendMessage.mockResolvedValue({ messageTs: "1234567890.123456", channel: "C123456", }); const { caller, project } = await prepare(); const secretToken = "xoxb-extremely-secret-token-abcdef123456"; // Create Slack integration await prisma.slackIntegration.create({ data: { projectId: project.id, teamId: "T123456", teamName: "Test Team", botToken: encrypt(secretToken), botUserId: "U123456", }, }); // Test all tRPC endpoints const results = await Promise.all([ caller.slack.getIntegrationStatus({ projectId: project.id }), caller.slack.getChannels({ projectId: project.id }), caller.slack.sendTestMessage({ projectId: project.id, channelId: "C123456", channelName: "general", }), ]); results.forEach((result) => { const resultText = JSON.stringify(result); // 🔒 CRITICAL: Ensure no raw token appears anywhere in response expect(resultText).not.toContain(secretToken); expect(resultText).not.toContain("xoxb-extremely-secret-token"); }); }); it("should NEVER set unencrypted tokens in cookies", async () => { const { caller, project } = await prepare(); // Test tRPC calls (cookies would be set at HTTP layer, not tRPC layer) const result = await caller.slack.getIntegrationStatus({ projectId: project.id, }); // 🔒 Ensure tRPC result doesn't contain any token fields that could leak to cookies expect(JSON.stringify(result)).not.toContain("xoxb-"); expect(result).not.toHaveProperty("botToken"); expect(result).not.toHaveProperty("token"); }); it("should sanitize tokens from error messages", async () => { const secretToken = "xoxb-secret-error-token-999"; // Mock SlackService to throw error containing token mockSlackService.getChannels.mockRejectedValue( new Error(`Authentication failed for token ${secretToken}`), ); const { caller, project } = await prepare(); // Create Slack integration await prisma.slackIntegration.create({ data: { projectId: project.id, teamId: "T123456", teamName: "Test Team", botToken: encrypt(secretToken), botUserId: "U123456", }, }); try { await caller.slack.getChannels({ projectId: project.id }); throw new Error("Expected error to be thrown"); } catch (error) { const errorMessage = error instanceof TRPCError ? error.message : String(error); // 🔒 CRITICAL: Error messages should not contain raw tokens expect(errorMessage).not.toContain(secretToken); expect(errorMessage).not.toContain("xoxb-secret-error-token"); // Should still indicate there was an error, just sanitized expect(errorMessage).toContain("Failed to fetch channels."); } }); }); });