diff --git a/apps/api/src/federation/dto/capabilities.dto.spec.ts b/apps/api/src/federation/dto/capabilities.dto.spec.ts new file mode 100644 index 0000000..ded956c --- /dev/null +++ b/apps/api/src/federation/dto/capabilities.dto.spec.ts @@ -0,0 +1,80 @@ +/** + * Capabilities DTO Tests + * + * Tests for FederationCapabilities validation. + */ + +import { describe, it, expect } from "vitest"; +import { validate } from "class-validator"; +import { plainToInstance } from "class-transformer"; +import { FederationCapabilitiesDto } from "./capabilities.dto"; + +describe("FederationCapabilitiesDto", () => { + it("should accept valid capabilities", async () => { + const plain = { + supportsQuery: true, + supportsCommand: false, + supportsEvent: true, + supportsAgentSpawn: false, + protocolVersion: "1.0", + }; + + const dto = plainToInstance(FederationCapabilitiesDto, plain); + const errors = await validate(dto); + + expect(errors).toHaveLength(0); + }); + + it("should accept minimal valid capabilities", async () => { + const plain = {}; + + const dto = plainToInstance(FederationCapabilitiesDto, plain); + const errors = await validate(dto); + + expect(errors).toHaveLength(0); + }); + + it("should reject invalid boolean for supportsQuery", async () => { + const plain = { + supportsQuery: "yes", // Should be boolean + }; + + const dto = plainToInstance(FederationCapabilitiesDto, plain); + const errors = await validate(dto); + + expect(errors.length).toBeGreaterThan(0); + expect(errors[0].property).toBe("supportsQuery"); + }); + + it("should reject invalid type for protocolVersion", async () => { + const plain = { + protocolVersion: 1.0, // Should be string + }; + + const dto = plainToInstance(FederationCapabilitiesDto, plain); + const errors = await validate(dto); + + expect(errors.length).toBeGreaterThan(0); + expect(errors[0].property).toBe("protocolVersion"); + }); + + it("should accept only specified fields", async () => { + const plain = { + supportsQuery: true, + supportsCommand: true, + supportsEvent: false, + supportsAgentSpawn: true, + protocolVersion: "1.0", + }; + + const dto = plainToInstance(FederationCapabilitiesDto, plain); + const errors = await validate(dto); + + expect(errors).toHaveLength(0); + expect(dto.supportsQuery).toBe(true); + expect(dto.supportsCommand).toBe(true); + expect(dto.supportsEvent).toBe(false); + expect(dto.supportsAgentSpawn).toBe(true); + expect(dto.protocolVersion).toBe("1.0"); + }); +}); diff --git a/apps/api/src/federation/dto/capabilities.dto.ts b/apps/api/src/federation/dto/capabilities.dto.ts new file mode 100644 index 0000000..da606b5 --- /dev/null +++ b/apps/api/src/federation/dto/capabilities.dto.ts @@ -0,0 +1,32 @@ +/** + * Capabilities DTO + * + * Data Transfer Object for federation capabilities validation. + */ + +import { IsBoolean, IsOptional, IsString } from "class-validator"; + +/** + * DTO for validating FederationCapabilities structure + */ +export class FederationCapabilitiesDto { + @IsOptional() + @IsBoolean() + supportsQuery?: boolean; + + @IsOptional() + @IsBoolean() + supportsCommand?: boolean; + + @IsOptional() + @IsBoolean() + supportsEvent?: boolean; + + @IsOptional() + @IsBoolean() + supportsAgentSpawn?: boolean; + + @IsOptional() + @IsString() + protocolVersion?: string; +} diff --git a/apps/api/src/federation/dto/connection.dto.ts b/apps/api/src/federation/dto/connection.dto.ts index 3a15765..e65ac79 100644 --- a/apps/api/src/federation/dto/connection.dto.ts +++ b/apps/api/src/federation/dto/connection.dto.ts @@ -4,8 +4,10 @@ * Data Transfer Objects for federation connection API. */ -import { IsString, IsUrl, IsOptional, IsObject, IsNumber } from "class-validator"; +import { IsString, IsUrl, IsOptional, IsObject, IsNumber, ValidateNested } from "class-validator"; +import { Type } from "class-transformer"; import { Sanitize, SanitizeObject } from "../../common/decorators/sanitize.decorator"; +import { FederationCapabilitiesDto } from "./capabilities.dto"; /** * DTO for initiating a connection @@ -57,8 +59,9 @@ export class IncomingConnectionRequestDto { @IsString() publicKey!: string; - @IsObject() - capabilities!: Record; + @ValidateNested() + @Type(() => FederationCapabilitiesDto) + capabilities!: FederationCapabilitiesDto; @IsNumber() timestamp!: number; diff --git a/apps/api/src/federation/federation.controller.spec.ts b/apps/api/src/federation/federation.controller.spec.ts index 48b682f..320cbc1 100644 --- a/apps/api/src/federation/federation.controller.spec.ts +++ b/apps/api/src/federation/federation.controller.spec.ts @@ -339,5 +339,30 @@ describe("FederationController", () => { }); expect(connectionService.handleIncomingConnectionRequest).toHaveBeenCalled(); }); + + it("should validate capabilities structure with valid data", async () => { + const dto = { + instanceId: "remote-instance-456", + instanceUrl: "https://remote.example.com", + publicKey: "PUBLIC_KEY", + capabilities: { + supportsQuery: true, + supportsCommand: false, + supportsEvent: true, + supportsAgentSpawn: false, + protocolVersion: "1.0", + }, + timestamp: Date.now(), + signature: "valid-signature", + }; + vi.spyOn(connectionService, "handleIncomingConnectionRequest").mockResolvedValue( + mockConnection + ); + + const result = await controller.handleIncomingConnection(dto); + + expect(result.status).toBe("pending"); + expect(connectionService.handleIncomingConnectionRequest).toHaveBeenCalled(); + }); }); });