Files
stack/apps/api/src/federation/command.service.spec.ts
Jason Woltje 9501aa3867 feat(#89): implement COMMAND message type for federation
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>
2026-02-03 13:30:16 -06:00

575 lines
18 KiB
TypeScript

/**
* Command Service Tests
*/
import { describe, it, expect, beforeEach, vi } from "vitest";
import { Test, TestingModule } from "@nestjs/testing";
import { HttpService } from "@nestjs/axios";
import { CommandService } from "./command.service";
import { PrismaService } from "../prisma/prisma.service";
import { FederationService } from "./federation.service";
import { SignatureService } from "./signature.service";
import {
FederationConnectionStatus,
FederationMessageType,
FederationMessageStatus,
} from "@prisma/client";
import { of } from "rxjs";
import type { CommandMessage, CommandResponse } from "./types/message.types";
describe("CommandService", () => {
let service: CommandService;
let prisma: PrismaService;
let federationService: FederationService;
let signatureService: SignatureService;
let httpService: HttpService;
const mockWorkspaceId = "workspace-123";
const mockConnectionId = "connection-123";
const mockInstanceId = "instance-456";
const mockRemoteUrl = "https://remote.example.com";
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
CommandService,
{
provide: PrismaService,
useValue: {
federationConnection: {
findUnique: vi.fn(),
findFirst: vi.fn(),
},
federationMessage: {
create: vi.fn(),
update: vi.fn(),
findMany: vi.fn(),
findUnique: vi.fn(),
findFirst: vi.fn(),
},
},
},
{
provide: FederationService,
useValue: {
getInstanceIdentity: vi.fn(),
},
},
{
provide: SignatureService,
useValue: {
signMessage: vi.fn(),
verifyMessage: vi.fn(),
validateTimestamp: vi.fn(),
},
},
{
provide: HttpService,
useValue: {
post: vi.fn(),
},
},
],
}).compile();
service = module.get<CommandService>(CommandService);
prisma = module.get<PrismaService>(PrismaService);
federationService = module.get<FederationService>(FederationService);
signatureService = module.get<SignatureService>(SignatureService);
httpService = module.get<HttpService>(HttpService);
});
describe("sendCommand", () => {
it("should send a command to a remote instance", async () => {
const commandType = "spawn_agent";
const payload = { agentType: "task_executor" };
const mockConnection = {
id: mockConnectionId,
workspaceId: mockWorkspaceId,
status: FederationConnectionStatus.ACTIVE,
remoteUrl: mockRemoteUrl,
remoteInstanceId: mockInstanceId,
};
const mockIdentity = {
instanceId: "local-instance",
displayName: "Local Instance",
};
const mockMessage = {
id: "msg-123",
workspaceId: mockWorkspaceId,
connectionId: mockConnectionId,
messageType: FederationMessageType.COMMAND,
messageId: expect.any(String),
correlationId: null,
query: null,
commandType,
payload,
response: {},
status: FederationMessageStatus.PENDING,
error: null,
signature: "signature-123",
createdAt: new Date(),
updatedAt: new Date(),
deliveredAt: null,
};
vi.spyOn(prisma.federationConnection, "findUnique").mockResolvedValue(
mockConnection as never
);
vi.spyOn(federationService, "getInstanceIdentity").mockResolvedValue(mockIdentity as never);
vi.spyOn(signatureService, "signMessage").mockResolvedValue("signature-123");
vi.spyOn(prisma.federationMessage, "create").mockResolvedValue(mockMessage as never);
vi.spyOn(httpService, "post").mockReturnValue(of({} as never));
const result = await service.sendCommand(
mockWorkspaceId,
mockConnectionId,
commandType,
payload
);
expect(result).toMatchObject({
workspaceId: mockWorkspaceId,
connectionId: mockConnectionId,
messageType: FederationMessageType.COMMAND,
commandType,
status: FederationMessageStatus.PENDING,
});
expect(httpService.post).toHaveBeenCalledWith(
`${mockRemoteUrl}/api/v1/federation/incoming/command`,
expect.objectContaining({
messageId: expect.any(String),
instanceId: "local-instance",
commandType,
payload,
timestamp: expect.any(Number),
signature: "signature-123",
})
);
});
it("should throw error if connection not found", async () => {
vi.spyOn(prisma.federationConnection, "findUnique").mockResolvedValue(null);
await expect(
service.sendCommand(mockWorkspaceId, mockConnectionId, "test", {})
).rejects.toThrow("Connection not found");
});
it("should throw error if connection is not active", async () => {
const mockConnection = {
id: mockConnectionId,
workspaceId: mockWorkspaceId,
status: FederationConnectionStatus.SUSPENDED,
};
vi.spyOn(prisma.federationConnection, "findUnique").mockResolvedValue(
mockConnection as never
);
await expect(
service.sendCommand(mockWorkspaceId, mockConnectionId, "test", {})
).rejects.toThrow("Connection is not active");
});
it("should mark command as failed if sending fails", async () => {
const mockConnection = {
id: mockConnectionId,
workspaceId: mockWorkspaceId,
status: FederationConnectionStatus.ACTIVE,
remoteUrl: mockRemoteUrl,
};
const mockIdentity = {
instanceId: "local-instance",
displayName: "Local Instance",
};
const mockMessage = {
id: "msg-123",
workspaceId: mockWorkspaceId,
connectionId: mockConnectionId,
messageType: FederationMessageType.COMMAND,
messageId: "test-msg-id",
correlationId: null,
query: null,
commandType: "test",
payload: {},
response: {},
status: FederationMessageStatus.PENDING,
error: null,
signature: "signature-123",
createdAt: new Date(),
updatedAt: new Date(),
deliveredAt: null,
};
vi.spyOn(prisma.federationConnection, "findUnique").mockResolvedValue(
mockConnection as never
);
vi.spyOn(federationService, "getInstanceIdentity").mockResolvedValue(mockIdentity as never);
vi.spyOn(signatureService, "signMessage").mockResolvedValue("signature-123");
vi.spyOn(prisma.federationMessage, "create").mockResolvedValue(mockMessage as never);
vi.spyOn(httpService, "post").mockReturnValue(
new (class {
subscribe(handlers: { error: (err: Error) => void }) {
handlers.error(new Error("Network error"));
}
})() as never
);
vi.spyOn(prisma.federationMessage, "update").mockResolvedValue(mockMessage as never);
await expect(
service.sendCommand(mockWorkspaceId, mockConnectionId, "test", {})
).rejects.toThrow("Failed to send command");
expect(prisma.federationMessage.update).toHaveBeenCalledWith({
where: { id: "msg-123" },
data: {
status: FederationMessageStatus.FAILED,
error: "Network error",
},
});
});
});
describe("handleIncomingCommand", () => {
it("should process a valid incoming command", async () => {
const commandMessage: CommandMessage = {
messageId: "cmd-123",
instanceId: mockInstanceId,
commandType: "spawn_agent",
payload: { agentType: "task_executor" },
timestamp: Date.now(),
signature: "signature-123",
};
const mockConnection = {
id: mockConnectionId,
remoteInstanceId: mockInstanceId,
status: FederationConnectionStatus.ACTIVE,
};
const mockIdentity = {
instanceId: "local-instance",
displayName: "Local Instance",
};
vi.spyOn(signatureService, "validateTimestamp").mockReturnValue(true);
vi.spyOn(prisma.federationConnection, "findFirst").mockResolvedValue(mockConnection as never);
vi.spyOn(signatureService, "verifyMessage").mockResolvedValue({
valid: true,
error: null,
} as never);
vi.spyOn(federationService, "getInstanceIdentity").mockResolvedValue(mockIdentity as never);
vi.spyOn(signatureService, "signMessage").mockResolvedValue("response-signature");
const response = await service.handleIncomingCommand(commandMessage);
expect(response).toMatchObject({
correlationId: "cmd-123",
instanceId: "local-instance",
success: true,
});
expect(signatureService.validateTimestamp).toHaveBeenCalledWith(commandMessage.timestamp);
expect(signatureService.verifyMessage).toHaveBeenCalledWith(
expect.objectContaining({
messageId: "cmd-123",
instanceId: mockInstanceId,
commandType: "spawn_agent",
}),
"signature-123",
mockInstanceId
);
});
it("should reject command with invalid timestamp", async () => {
const commandMessage: CommandMessage = {
messageId: "cmd-123",
instanceId: mockInstanceId,
commandType: "test",
payload: {},
timestamp: Date.now() - 1000000,
signature: "signature-123",
};
vi.spyOn(signatureService, "validateTimestamp").mockReturnValue(false);
await expect(service.handleIncomingCommand(commandMessage)).rejects.toThrow(
"Command timestamp is outside acceptable range"
);
});
it("should reject command if no connection found", async () => {
const commandMessage: CommandMessage = {
messageId: "cmd-123",
instanceId: mockInstanceId,
commandType: "test",
payload: {},
timestamp: Date.now(),
signature: "signature-123",
};
vi.spyOn(signatureService, "validateTimestamp").mockReturnValue(true);
vi.spyOn(prisma.federationConnection, "findFirst").mockResolvedValue(null);
await expect(service.handleIncomingCommand(commandMessage)).rejects.toThrow(
"No connection found for remote instance"
);
});
it("should reject command with invalid signature", async () => {
const commandMessage: CommandMessage = {
messageId: "cmd-123",
instanceId: mockInstanceId,
commandType: "test",
payload: {},
timestamp: Date.now(),
signature: "invalid-signature",
};
const mockConnection = {
id: mockConnectionId,
remoteInstanceId: mockInstanceId,
status: FederationConnectionStatus.ACTIVE,
};
vi.spyOn(signatureService, "validateTimestamp").mockReturnValue(true);
vi.spyOn(prisma.federationConnection, "findFirst").mockResolvedValue(mockConnection as never);
vi.spyOn(signatureService, "verifyMessage").mockResolvedValue({
valid: false,
error: "Invalid signature",
} as never);
await expect(service.handleIncomingCommand(commandMessage)).rejects.toThrow(
"Invalid signature"
);
});
});
describe("processCommandResponse", () => {
it("should process a successful command response", async () => {
const response: CommandResponse = {
messageId: "resp-123",
correlationId: "cmd-123",
instanceId: mockInstanceId,
success: true,
data: { result: "success" },
timestamp: Date.now(),
signature: "signature-123",
};
const mockMessage = {
id: "msg-123",
workspaceId: mockWorkspaceId,
connectionId: mockConnectionId,
messageType: FederationMessageType.COMMAND,
messageId: "cmd-123",
correlationId: null,
query: null,
commandType: "test",
payload: {},
response: {},
status: FederationMessageStatus.PENDING,
error: null,
signature: "signature-123",
createdAt: new Date(),
updatedAt: new Date(),
deliveredAt: null,
};
vi.spyOn(signatureService, "validateTimestamp").mockReturnValue(true);
vi.spyOn(prisma.federationMessage, "findFirst").mockResolvedValue(mockMessage as never);
vi.spyOn(signatureService, "verifyMessage").mockResolvedValue({
valid: true,
error: null,
} as never);
vi.spyOn(prisma.federationMessage, "update").mockResolvedValue(mockMessage as never);
await service.processCommandResponse(response);
expect(prisma.federationMessage.update).toHaveBeenCalledWith({
where: { id: "msg-123" },
data: {
status: FederationMessageStatus.DELIVERED,
deliveredAt: expect.any(Date),
response: { result: "success" },
},
});
});
it("should handle failed command response", async () => {
const response: CommandResponse = {
messageId: "resp-123",
correlationId: "cmd-123",
instanceId: mockInstanceId,
success: false,
error: "Command execution failed",
timestamp: Date.now(),
signature: "signature-123",
};
const mockMessage = {
id: "msg-123",
messageType: FederationMessageType.COMMAND,
messageId: "cmd-123",
};
vi.spyOn(signatureService, "validateTimestamp").mockReturnValue(true);
vi.spyOn(prisma.federationMessage, "findFirst").mockResolvedValue(mockMessage as never);
vi.spyOn(signatureService, "verifyMessage").mockResolvedValue({
valid: true,
error: null,
} as never);
vi.spyOn(prisma.federationMessage, "update").mockResolvedValue(mockMessage as never);
await service.processCommandResponse(response);
expect(prisma.federationMessage.update).toHaveBeenCalledWith({
where: { id: "msg-123" },
data: {
status: FederationMessageStatus.FAILED,
deliveredAt: expect.any(Date),
error: "Command execution failed",
},
});
});
it("should reject response with invalid timestamp", async () => {
const response: CommandResponse = {
messageId: "resp-123",
correlationId: "cmd-123",
instanceId: mockInstanceId,
success: true,
timestamp: Date.now() - 1000000,
signature: "signature-123",
};
vi.spyOn(signatureService, "validateTimestamp").mockReturnValue(false);
await expect(service.processCommandResponse(response)).rejects.toThrow(
"Response timestamp is outside acceptable range"
);
});
});
describe("getCommandMessages", () => {
it("should return all command messages for a workspace", async () => {
const mockMessages = [
{
id: "msg-1",
workspaceId: mockWorkspaceId,
connectionId: mockConnectionId,
messageType: FederationMessageType.COMMAND,
messageId: "cmd-1",
correlationId: null,
query: null,
commandType: "test",
payload: {},
response: {},
status: FederationMessageStatus.DELIVERED,
error: null,
signature: "sig-1",
createdAt: new Date(),
updatedAt: new Date(),
deliveredAt: new Date(),
},
];
vi.spyOn(prisma.federationMessage, "findMany").mockResolvedValue(mockMessages as never);
const result = await service.getCommandMessages(mockWorkspaceId);
expect(result).toHaveLength(1);
expect(result[0]).toMatchObject({
workspaceId: mockWorkspaceId,
messageType: FederationMessageType.COMMAND,
commandType: "test",
});
});
it("should filter command messages by status", async () => {
const mockMessages = [
{
id: "msg-1",
workspaceId: mockWorkspaceId,
connectionId: mockConnectionId,
messageType: FederationMessageType.COMMAND,
messageId: "cmd-1",
correlationId: null,
query: null,
commandType: "test",
payload: {},
response: {},
status: FederationMessageStatus.PENDING,
error: null,
signature: "sig-1",
createdAt: new Date(),
updatedAt: new Date(),
deliveredAt: null,
},
];
vi.spyOn(prisma.federationMessage, "findMany").mockResolvedValue(mockMessages as never);
await service.getCommandMessages(mockWorkspaceId, FederationMessageStatus.PENDING);
expect(prisma.federationMessage.findMany).toHaveBeenCalledWith({
where: {
workspaceId: mockWorkspaceId,
messageType: FederationMessageType.COMMAND,
status: FederationMessageStatus.PENDING,
},
orderBy: { createdAt: "desc" },
});
});
});
describe("getCommandMessage", () => {
it("should return a single command message", async () => {
const mockMessage = {
id: "msg-1",
workspaceId: mockWorkspaceId,
connectionId: mockConnectionId,
messageType: FederationMessageType.COMMAND,
messageId: "cmd-1",
correlationId: null,
query: null,
commandType: "test",
payload: { key: "value" },
response: {},
status: FederationMessageStatus.DELIVERED,
error: null,
signature: "sig-1",
createdAt: new Date(),
updatedAt: new Date(),
deliveredAt: new Date(),
};
vi.spyOn(prisma.federationMessage, "findUnique").mockResolvedValue(mockMessage as never);
const result = await service.getCommandMessage(mockWorkspaceId, "msg-1");
expect(result).toMatchObject({
id: "msg-1",
workspaceId: mockWorkspaceId,
commandType: "test",
payload: { key: "value" },
});
});
it("should throw error if command message not found", async () => {
vi.spyOn(prisma.federationMessage, "findUnique").mockResolvedValue(null);
await expect(service.getCommandMessage(mockWorkspaceId, "invalid-id")).rejects.toThrow(
"Command message not found"
);
});
});
});