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:
80
apps/api/src/federation/dto/capabilities.dto.spec.ts
Normal file
80
apps/api/src/federation/dto/capabilities.dto.spec.ts
Normal 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");
|
||||||
|
});
|
||||||
|
});
|
||||||
32
apps/api/src/federation/dto/capabilities.dto.ts
Normal file
32
apps/api/src/federation/dto/capabilities.dto.ts
Normal 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;
|
||||||
|
}
|
||||||
@@ -4,8 +4,10 @@
|
|||||||
* Data Transfer Objects for federation connection API.
|
* 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 { Sanitize, SanitizeObject } from "../../common/decorators/sanitize.decorator";
|
||||||
|
import { FederationCapabilitiesDto } from "./capabilities.dto";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* DTO for initiating a connection
|
* DTO for initiating a connection
|
||||||
@@ -57,8 +59,9 @@ export class IncomingConnectionRequestDto {
|
|||||||
@IsString()
|
@IsString()
|
||||||
publicKey!: string;
|
publicKey!: string;
|
||||||
|
|
||||||
@IsObject()
|
@ValidateNested()
|
||||||
capabilities!: Record<string, unknown>;
|
@Type(() => FederationCapabilitiesDto)
|
||||||
|
capabilities!: FederationCapabilitiesDto;
|
||||||
|
|
||||||
@IsNumber()
|
@IsNumber()
|
||||||
timestamp!: number;
|
timestamp!: number;
|
||||||
|
|||||||
@@ -339,5 +339,30 @@ describe("FederationController", () => {
|
|||||||
});
|
});
|
||||||
expect(connectionService.handleIncomingConnectionRequest).toHaveBeenCalled();
|
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();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user