Implements federated command messages following TDD principles and mirroring the QueryService pattern for consistency. ## Implementation ### Schema Changes - Added commandType and payload fields to FederationMessage model - Supports COMMAND message type (already defined in enum) - Applied schema changes with prisma db push ### Type Definitions - CommandMessage: Request structure with commandType and payload - CommandResponse: Response structure with correlation - CommandMessageDetails: Full message details for API responses ### CommandService - sendCommand(): Send command to remote instance with signature - handleIncomingCommand(): Process incoming commands with verification - processCommandResponse(): Handle command responses - getCommandMessages(): List commands for workspace - getCommandMessage(): Get single command details - Full signature verification and timestamp validation - Error handling and status tracking ### CommandController - POST /api/v1/federation/command - Send command (authenticated) - POST /api/v1/federation/incoming/command - Handle incoming (public) - GET /api/v1/federation/commands - List commands (authenticated) - GET /api/v1/federation/commands/:id - Get command (authenticated) ## Testing - CommandService: 15 tests, 90.21% coverage - CommandController: 8 tests, 100% coverage - All 23 tests passing - Exceeds 85% coverage requirement - Total 47 tests passing (includes command tests) ## Security - RSA signature verification for all incoming commands - Timestamp validation to prevent replay attacks - Connection status validation - Authorization checks on command types ## Quality Checks - TypeScript compilation: PASSED - All tests: 47 PASSED - Code coverage: >85% (90.21% for CommandService, 100% for CommandController) - Linting: PASSED Fixes #89 Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
237 lines
7.1 KiB
TypeScript
237 lines
7.1 KiB
TypeScript
/**
|
|
* Command Controller Tests
|
|
*/
|
|
|
|
import { describe, it, expect, beforeEach, vi } from "vitest";
|
|
import { Test, TestingModule } from "@nestjs/testing";
|
|
import { CommandController } from "./command.controller";
|
|
import { CommandService } from "./command.service";
|
|
import { AuthGuard } from "../auth/guards/auth.guard";
|
|
import { FederationMessageType, FederationMessageStatus } from "@prisma/client";
|
|
import type { AuthenticatedRequest } from "../common/types/user.types";
|
|
import type { CommandMessage, CommandResponse } from "./types/message.types";
|
|
|
|
describe("CommandController", () => {
|
|
let controller: CommandController;
|
|
let commandService: CommandService;
|
|
|
|
const mockWorkspaceId = "workspace-123";
|
|
const mockUserId = "user-123";
|
|
|
|
beforeEach(async () => {
|
|
const module: TestingModule = await Test.createTestingModule({
|
|
controllers: [CommandController],
|
|
providers: [
|
|
{
|
|
provide: CommandService,
|
|
useValue: {
|
|
sendCommand: vi.fn(),
|
|
handleIncomingCommand: vi.fn(),
|
|
getCommandMessages: vi.fn(),
|
|
getCommandMessage: vi.fn(),
|
|
},
|
|
},
|
|
],
|
|
})
|
|
.overrideGuard(AuthGuard)
|
|
.useValue({ canActivate: () => true })
|
|
.compile();
|
|
|
|
controller = module.get<CommandController>(CommandController);
|
|
commandService = module.get<CommandService>(CommandService);
|
|
});
|
|
|
|
describe("sendCommand", () => {
|
|
it("should send a command", async () => {
|
|
const req = {
|
|
user: { id: mockUserId, workspaceId: mockWorkspaceId },
|
|
} as AuthenticatedRequest;
|
|
|
|
const dto = {
|
|
connectionId: "conn-123",
|
|
commandType: "spawn_agent",
|
|
payload: { agentType: "task_executor" },
|
|
};
|
|
|
|
const mockResult = {
|
|
id: "msg-123",
|
|
workspaceId: mockWorkspaceId,
|
|
connectionId: "conn-123",
|
|
messageType: FederationMessageType.COMMAND,
|
|
messageId: "cmd-123",
|
|
commandType: "spawn_agent",
|
|
payload: { agentType: "task_executor" },
|
|
status: FederationMessageStatus.PENDING,
|
|
createdAt: new Date(),
|
|
updatedAt: new Date(),
|
|
};
|
|
|
|
vi.spyOn(commandService, "sendCommand").mockResolvedValue(mockResult as never);
|
|
|
|
const result = await controller.sendCommand(req, dto);
|
|
|
|
expect(result).toEqual(mockResult);
|
|
expect(commandService.sendCommand).toHaveBeenCalledWith(
|
|
mockWorkspaceId,
|
|
"conn-123",
|
|
"spawn_agent",
|
|
{ agentType: "task_executor" }
|
|
);
|
|
});
|
|
|
|
it("should throw error if workspace ID not found", async () => {
|
|
const req = {
|
|
user: { id: mockUserId },
|
|
} as AuthenticatedRequest;
|
|
|
|
const dto = {
|
|
connectionId: "conn-123",
|
|
commandType: "test",
|
|
payload: {},
|
|
};
|
|
|
|
await expect(controller.sendCommand(req, dto)).rejects.toThrow(
|
|
"Workspace ID not found in request"
|
|
);
|
|
});
|
|
});
|
|
|
|
describe("handleIncomingCommand", () => {
|
|
it("should handle an incoming command", async () => {
|
|
const dto: CommandMessage = {
|
|
messageId: "cmd-123",
|
|
instanceId: "remote-instance",
|
|
commandType: "spawn_agent",
|
|
payload: { agentType: "task_executor" },
|
|
timestamp: Date.now(),
|
|
signature: "signature-123",
|
|
};
|
|
|
|
const mockResponse: CommandResponse = {
|
|
messageId: "resp-123",
|
|
correlationId: "cmd-123",
|
|
instanceId: "local-instance",
|
|
success: true,
|
|
data: { result: "success" },
|
|
timestamp: Date.now(),
|
|
signature: "response-signature",
|
|
};
|
|
|
|
vi.spyOn(commandService, "handleIncomingCommand").mockResolvedValue(mockResponse);
|
|
|
|
const result = await controller.handleIncomingCommand(dto);
|
|
|
|
expect(result).toEqual(mockResponse);
|
|
expect(commandService.handleIncomingCommand).toHaveBeenCalledWith(dto);
|
|
});
|
|
});
|
|
|
|
describe("getCommands", () => {
|
|
it("should return all commands for workspace", async () => {
|
|
const req = {
|
|
user: { id: mockUserId, workspaceId: mockWorkspaceId },
|
|
} as AuthenticatedRequest;
|
|
|
|
const mockCommands = [
|
|
{
|
|
id: "msg-1",
|
|
workspaceId: mockWorkspaceId,
|
|
connectionId: "conn-123",
|
|
messageType: FederationMessageType.COMMAND,
|
|
messageId: "cmd-1",
|
|
commandType: "test",
|
|
payload: {},
|
|
status: FederationMessageStatus.DELIVERED,
|
|
createdAt: new Date(),
|
|
updatedAt: new Date(),
|
|
},
|
|
];
|
|
|
|
vi.spyOn(commandService, "getCommandMessages").mockResolvedValue(mockCommands as never);
|
|
|
|
const result = await controller.getCommands(req);
|
|
|
|
expect(result).toEqual(mockCommands);
|
|
expect(commandService.getCommandMessages).toHaveBeenCalledWith(mockWorkspaceId, undefined);
|
|
});
|
|
|
|
it("should filter commands by status", async () => {
|
|
const req = {
|
|
user: { id: mockUserId, workspaceId: mockWorkspaceId },
|
|
} as AuthenticatedRequest;
|
|
|
|
const mockCommands = [
|
|
{
|
|
id: "msg-1",
|
|
workspaceId: mockWorkspaceId,
|
|
connectionId: "conn-123",
|
|
messageType: FederationMessageType.COMMAND,
|
|
messageId: "cmd-1",
|
|
commandType: "test",
|
|
payload: {},
|
|
status: FederationMessageStatus.PENDING,
|
|
createdAt: new Date(),
|
|
updatedAt: new Date(),
|
|
},
|
|
];
|
|
|
|
vi.spyOn(commandService, "getCommandMessages").mockResolvedValue(mockCommands as never);
|
|
|
|
await controller.getCommands(req, FederationMessageStatus.PENDING);
|
|
|
|
expect(commandService.getCommandMessages).toHaveBeenCalledWith(
|
|
mockWorkspaceId,
|
|
FederationMessageStatus.PENDING
|
|
);
|
|
});
|
|
|
|
it("should throw error if workspace ID not found", async () => {
|
|
const req = {
|
|
user: { id: mockUserId },
|
|
} as AuthenticatedRequest;
|
|
|
|
await expect(controller.getCommands(req)).rejects.toThrow(
|
|
"Workspace ID not found in request"
|
|
);
|
|
});
|
|
});
|
|
|
|
describe("getCommand", () => {
|
|
it("should return a single command", async () => {
|
|
const req = {
|
|
user: { id: mockUserId, workspaceId: mockWorkspaceId },
|
|
} as AuthenticatedRequest;
|
|
|
|
const mockCommand = {
|
|
id: "msg-1",
|
|
workspaceId: mockWorkspaceId,
|
|
connectionId: "conn-123",
|
|
messageType: FederationMessageType.COMMAND,
|
|
messageId: "cmd-1",
|
|
commandType: "test",
|
|
payload: { key: "value" },
|
|
status: FederationMessageStatus.DELIVERED,
|
|
createdAt: new Date(),
|
|
updatedAt: new Date(),
|
|
};
|
|
|
|
vi.spyOn(commandService, "getCommandMessage").mockResolvedValue(mockCommand as never);
|
|
|
|
const result = await controller.getCommand(req, "msg-1");
|
|
|
|
expect(result).toEqual(mockCommand);
|
|
expect(commandService.getCommandMessage).toHaveBeenCalledWith(mockWorkspaceId, "msg-1");
|
|
});
|
|
|
|
it("should throw error if workspace ID not found", async () => {
|
|
const req = {
|
|
user: { id: mockUserId },
|
|
} as AuthenticatedRequest;
|
|
|
|
await expect(controller.getCommand(req, "msg-1")).rejects.toThrow(
|
|
"Workspace ID not found in request"
|
|
);
|
|
});
|
|
});
|
|
});
|