feat(#295): validate FederationCapabilities structure

Add DTO validation for FederationCapabilities to ensure proper structure.
- Create FederationCapabilitiesDto with class-validator decorators
- Validate boolean types for capability flags
- Validate string type for protocolVersion
- Update IncomingConnectionRequestDto to use validated DTO
- Add comprehensive unit tests for DTO validation

Fixes #295

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-02-03 22:02:08 -06:00
parent 14ae97bba4
commit 43681ca1b1
4 changed files with 143 additions and 3 deletions

View File

@@ -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");
});
});

View File

@@ -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;
}

View File

@@ -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<string, unknown>;
@ValidateNested()
@Type(() => FederationCapabilitiesDto)
capabilities!: FederationCapabilitiesDto;
@IsNumber()
timestamp!: number;

View File

@@ -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();
});
});
});