From d373ce591fabe456aff69c76b1583e9f7bfdc88c Mon Sep 17 00:00:00 2001 From: Jason Woltje Date: Tue, 3 Feb 2026 21:58:24 -0600 Subject: [PATCH] test(#291): add test for connection limit per workspace Add test to verify workspace connection limit enforcement. Default limit is 100 connections per workspace. Co-Authored-By: Claude Sonnet 4.5 --- .../src/federation/connection.service.spec.ts | 18 ++++++++++++++++++ apps/api/src/federation/connection.service.ts | 12 ++++++++++++ 2 files changed, 30 insertions(+) diff --git a/apps/api/src/federation/connection.service.spec.ts b/apps/api/src/federation/connection.service.spec.ts index dcd7f7b..3a545f5 100644 --- a/apps/api/src/federation/connection.service.spec.ts +++ b/apps/api/src/federation/connection.service.spec.ts @@ -88,6 +88,7 @@ describe("ConnectionService", () => { findMany: vi.fn(), update: vi.fn(), delete: vi.fn(), + count: vi.fn(), }, }, }, @@ -136,6 +137,19 @@ describe("ConnectionService", () => { }); describe("initiateConnection", () => { + it("should throw error if workspace has reached connection limit", async () => { + const existingConnections = Array.from({ length: 100 }, (_, i) => ({ + ...mockConnection, + id: `conn-${i}`, + })); + + vi.spyOn(prismaService.federationConnection, "count").mockResolvedValue(100); + + await expect(service.initiateConnection(mockWorkspaceId, mockRemoteUrl)).rejects.toThrow( + "Connection limit reached for workspace. Maximum 100 connections allowed per workspace." + ); + }); + it("should create a pending connection", async () => { const mockAxiosResponse: AxiosResponse = { data: mockRemoteIdentity, @@ -145,6 +159,7 @@ describe("ConnectionService", () => { config: {} as never, }; + vi.spyOn(prismaService.federationConnection, "count").mockResolvedValue(5); vi.spyOn(httpService, "get").mockReturnValue(of(mockAxiosResponse)); vi.spyOn(httpService, "post").mockReturnValue( of({ data: { accepted: true } } as AxiosResponse) @@ -176,6 +191,7 @@ describe("ConnectionService", () => { config: {} as never, }; + vi.spyOn(prismaService.federationConnection, "count").mockResolvedValue(5); vi.spyOn(httpService, "get").mockReturnValue(of(mockAxiosResponse)); vi.spyOn(httpService, "post").mockReturnValue( of({ data: { accepted: true } } as AxiosResponse) @@ -202,6 +218,7 @@ describe("ConnectionService", () => { config: {} as never, }; + vi.spyOn(prismaService.federationConnection, "count").mockResolvedValue(5); const postSpy = vi .spyOn(httpService, "post") .mockReturnValue(of({ data: { accepted: true } } as AxiosResponse)); @@ -230,6 +247,7 @@ describe("ConnectionService", () => { config: {} as never, }; + vi.spyOn(prismaService.federationConnection, "count").mockResolvedValue(5); vi.spyOn(httpService, "get").mockReturnValue(of(mockAxiosResponse)); vi.spyOn(httpService, "post").mockReturnValue( throwError(() => new Error("Connection refused")) diff --git a/apps/api/src/federation/connection.service.ts b/apps/api/src/federation/connection.service.ts index 9668541..983d97d 100644 --- a/apps/api/src/federation/connection.service.ts +++ b/apps/api/src/federation/connection.service.ts @@ -25,6 +25,7 @@ import type { PublicInstanceIdentity } from "./types/instance.types"; @Injectable() export class ConnectionService { private readonly logger = new Logger(ConnectionService.name); + private readonly MAX_CONNECTIONS_PER_WORKSPACE = 100; constructor( private readonly prisma: PrismaService, @@ -40,6 +41,17 @@ export class ConnectionService { async initiateConnection(workspaceId: string, remoteUrl: string): Promise { this.logger.log(`Initiating connection to ${remoteUrl} for workspace ${workspaceId}`); + // Check connection limit for workspace + const connectionCount = await this.prisma.federationConnection.count({ + where: { workspaceId }, + }); + + if (connectionCount >= this.MAX_CONNECTIONS_PER_WORKSPACE) { + throw new BadRequestException( + `Connection limit reached for workspace. Maximum ${String(this.MAX_CONNECTIONS_PER_WORKSPACE)} connections allowed per workspace.` + ); + } + // Fetch remote instance identity const remoteIdentity = await this.fetchRemoteIdentity(remoteUrl);