Implement explicit deny-lists in QueryService and CommandService to prevent user credentials from leaking across federation boundaries. ## Changes ### Core Implementation - QueryService: Block all credential-related queries with keyword detection - CommandService: Block all credential operations (create/update/delete/read) - Case-insensitive keyword matching for both queries and commands ### Security Features - Deny-list includes: credential, api_key, secret, token, password, oauth - Errors returned for blocked operations - No impact on existing allowed operations (tasks, events, projects, agent commands) ### Testing - Added 2 unit tests to query.service.spec.ts - Added 3 unit tests to command.service.spec.ts - Added 8 integration tests in credential-isolation.integration.spec.ts - All 377 federation tests passing ### Documentation - Created comprehensive security doc at docs/security/federation-credential-isolation.md - Documents 4 security guarantees (G1-G4) - Includes testing strategy and incident response procedures ## Security Guarantees 1. G1: Credential Confidentiality - Credentials never leave instance in plaintext 2. G2: Cross-Instance Isolation - Compromised key on one instance doesn't affect others 3. G3: Query/Command Isolation - Federated instances cannot query/modify credentials 4. G4: Accidental Exposure Prevention - Credentials cannot leak via messages ## Defense-in-Depth This implementation adds application-layer protection on top of existing: - Transit key separation (mosaic-credentials vs mosaic-federation) - Per-instance OpenBao servers - Workspace-scoped credential access Fixes #360 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
901 lines
30 KiB
TypeScript
901 lines
30 KiB
TypeScript
/**
|
|
* Command Service Tests
|
|
*/
|
|
|
|
import { describe, it, expect, beforeEach, vi } from "vitest";
|
|
import { Test, TestingModule } from "@nestjs/testing";
|
|
import { ModuleRef } from "@nestjs/core";
|
|
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";
|
|
import { UnknownCommandTypeError } from "./errors/command.errors";
|
|
|
|
describe("CommandService", () => {
|
|
let service: CommandService;
|
|
let prisma: PrismaService;
|
|
let federationService: FederationService;
|
|
let signatureService: SignatureService;
|
|
let httpService: HttpService;
|
|
let moduleRef: ModuleRef;
|
|
|
|
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);
|
|
moduleRef = module.get<ModuleRef>(ModuleRef);
|
|
});
|
|
|
|
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",
|
|
})
|
|
);
|
|
|
|
// Verify status was checked in the query
|
|
expect(prisma.federationConnection.findUnique).toHaveBeenCalledWith({
|
|
where: {
|
|
id: mockConnectionId,
|
|
workspaceId: mockWorkspaceId,
|
|
status: FederationConnectionStatus.ACTIVE,
|
|
},
|
|
});
|
|
});
|
|
|
|
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 () => {
|
|
// Connection should not be found by query because it's not ACTIVE
|
|
vi.spyOn(prisma.federationConnection, "findUnique").mockResolvedValue(null);
|
|
|
|
await expect(
|
|
service.sendCommand(mockWorkspaceId, mockConnectionId, "test", {})
|
|
).rejects.toThrow("Connection not found");
|
|
|
|
// Verify status was checked in the query
|
|
expect(prisma.federationConnection.findUnique).toHaveBeenCalledWith({
|
|
where: {
|
|
id: mockConnectionId,
|
|
workspaceId: mockWorkspaceId,
|
|
status: FederationConnectionStatus.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 agent command", async () => {
|
|
const commandMessage: CommandMessage = {
|
|
messageId: "cmd-123",
|
|
instanceId: mockInstanceId,
|
|
commandType: "agent.spawn",
|
|
payload: { agentType: "task_executor", taskId: "task-123" },
|
|
timestamp: Date.now(),
|
|
signature: "signature-123",
|
|
};
|
|
|
|
const mockConnection = {
|
|
id: mockConnectionId,
|
|
remoteInstanceId: mockInstanceId,
|
|
status: FederationConnectionStatus.ACTIVE,
|
|
};
|
|
|
|
const mockIdentity = {
|
|
instanceId: "local-instance",
|
|
displayName: "Local Instance",
|
|
};
|
|
|
|
const mockFederationAgentService = {
|
|
handleAgentCommand: vi.fn().mockResolvedValue({
|
|
success: true,
|
|
data: { agentId: "agent-123", status: "spawning", spawnedAt: new Date().toISOString() },
|
|
}),
|
|
};
|
|
|
|
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");
|
|
vi.spyOn(moduleRef, "get").mockReturnValue(mockFederationAgentService as never);
|
|
|
|
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: "agent.spawn",
|
|
}),
|
|
"signature-123",
|
|
mockInstanceId
|
|
);
|
|
expect(mockFederationAgentService.handleAgentCommand).toHaveBeenCalledWith(
|
|
mockInstanceId,
|
|
"agent.spawn",
|
|
commandMessage.payload
|
|
);
|
|
});
|
|
|
|
it("should handle unknown command types and return error response", async () => {
|
|
const commandMessage: CommandMessage = {
|
|
messageId: "cmd-123",
|
|
instanceId: mockInstanceId,
|
|
commandType: "unknown.command",
|
|
payload: {},
|
|
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: false,
|
|
error: "Unknown command type: unknown.command",
|
|
});
|
|
});
|
|
|
|
it("should handle business logic errors from agent service and return error response", async () => {
|
|
const commandMessage: CommandMessage = {
|
|
messageId: "cmd-123",
|
|
instanceId: mockInstanceId,
|
|
commandType: "agent.spawn",
|
|
payload: { agentType: "invalid_type" },
|
|
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");
|
|
|
|
// Mock FederationAgentService to return error response
|
|
const mockFederationAgentService = {
|
|
handleAgentCommand: vi.fn().mockResolvedValue({
|
|
success: false,
|
|
error: "Invalid agent type: invalid_type",
|
|
}),
|
|
};
|
|
|
|
vi.spyOn(moduleRef, "get").mockReturnValue(mockFederationAgentService as never);
|
|
|
|
const response = await service.handleIncomingCommand(commandMessage);
|
|
|
|
expect(response).toMatchObject({
|
|
correlationId: "cmd-123",
|
|
instanceId: "local-instance",
|
|
success: false,
|
|
error: "Invalid agent type: invalid_type",
|
|
});
|
|
});
|
|
|
|
it("should let system errors propagate (database connection failure)", async () => {
|
|
const commandMessage: CommandMessage = {
|
|
messageId: "cmd-123",
|
|
instanceId: mockInstanceId,
|
|
commandType: "agent.spawn",
|
|
payload: { agentType: "task_executor" },
|
|
timestamp: Date.now(),
|
|
signature: "signature-123",
|
|
};
|
|
|
|
vi.spyOn(signatureService, "validateTimestamp").mockReturnValue(true);
|
|
|
|
// Simulate database connection failure (system error)
|
|
const dbError = new Error("Connection pool exhausted");
|
|
dbError.name = "PoolExhaustedError";
|
|
vi.spyOn(prisma.federationConnection, "findFirst").mockRejectedValue(dbError);
|
|
|
|
// System errors should propagate
|
|
await expect(service.handleIncomingCommand(commandMessage)).rejects.toThrow(
|
|
"Connection pool exhausted"
|
|
);
|
|
});
|
|
|
|
it("should let system errors propagate from agent service (not wrapped)", async () => {
|
|
const commandMessage: CommandMessage = {
|
|
messageId: "cmd-123",
|
|
instanceId: mockInstanceId,
|
|
commandType: "agent.spawn",
|
|
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",
|
|
};
|
|
|
|
// Simulate a system error (not a CommandProcessingError) from agent service
|
|
const systemError = new Error("Database connection failed");
|
|
systemError.name = "DatabaseError";
|
|
|
|
const mockFederationAgentService = {
|
|
handleAgentCommand: vi.fn().mockRejectedValue(systemError),
|
|
};
|
|
|
|
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(moduleRef, "get").mockReturnValue(mockFederationAgentService as never);
|
|
|
|
// System errors should propagate (not caught by business logic handler)
|
|
await expect(service.handleIncomingCommand(commandMessage)).rejects.toThrow(
|
|
"Database connection failed"
|
|
);
|
|
});
|
|
|
|
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"
|
|
);
|
|
});
|
|
});
|
|
|
|
describe("handleIncomingCommand - Credential Isolation", () => {
|
|
it("should reject credential.create commands", async () => {
|
|
const commandMessage: CommandMessage = {
|
|
messageId: "cmd-123",
|
|
instanceId: "remote-instance-1",
|
|
commandType: "credential.create",
|
|
payload: {
|
|
name: "test-credential",
|
|
value: "secret-value",
|
|
},
|
|
timestamp: Date.now(),
|
|
signature: "signature-123",
|
|
};
|
|
|
|
const mockConnection = {
|
|
id: "connection-1",
|
|
workspaceId: mockWorkspaceId,
|
|
remoteInstanceId: "remote-instance-1",
|
|
status: FederationConnectionStatus.ACTIVE,
|
|
};
|
|
|
|
const mockIdentity = {
|
|
instanceId: "local-instance-1",
|
|
};
|
|
|
|
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 result = await service.handleIncomingCommand(commandMessage);
|
|
|
|
expect(result.success).toBe(false);
|
|
expect(result.error).toContain("Credential operations are not allowed");
|
|
});
|
|
|
|
it("should reject all credential operations", async () => {
|
|
const credentialCommands = [
|
|
"credential.create",
|
|
"credential.update",
|
|
"credential.delete",
|
|
"credential.read",
|
|
"credential.list",
|
|
"credentials.sync",
|
|
];
|
|
|
|
const mockConnection = {
|
|
id: "connection-1",
|
|
workspaceId: mockWorkspaceId,
|
|
remoteInstanceId: "remote-instance-1",
|
|
status: FederationConnectionStatus.ACTIVE,
|
|
};
|
|
|
|
const mockIdentity = {
|
|
instanceId: "local-instance-1",
|
|
};
|
|
|
|
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");
|
|
|
|
for (const commandType of credentialCommands) {
|
|
const commandMessage: CommandMessage = {
|
|
messageId: `cmd-${Math.random()}`,
|
|
instanceId: "remote-instance-1",
|
|
commandType,
|
|
payload: {},
|
|
timestamp: Date.now(),
|
|
signature: "signature-123",
|
|
};
|
|
|
|
const result = await service.handleIncomingCommand(commandMessage);
|
|
|
|
expect(result.success).toBe(false);
|
|
expect(result.error).toContain("Credential operations are not allowed");
|
|
}
|
|
});
|
|
|
|
it("should allow agent commands (existing functionality)", async () => {
|
|
const commandMessage: CommandMessage = {
|
|
messageId: "cmd-123",
|
|
instanceId: "remote-instance-1",
|
|
commandType: "agent.spawn",
|
|
payload: {
|
|
agentType: "task-executor",
|
|
},
|
|
timestamp: Date.now(),
|
|
signature: "signature-123",
|
|
};
|
|
|
|
const mockConnection = {
|
|
id: "connection-1",
|
|
workspaceId: mockWorkspaceId,
|
|
remoteInstanceId: "remote-instance-1",
|
|
status: FederationConnectionStatus.ACTIVE,
|
|
};
|
|
|
|
const mockIdentity = {
|
|
instanceId: "local-instance-1",
|
|
};
|
|
|
|
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");
|
|
|
|
// Mock FederationAgentService
|
|
const mockAgentService = {
|
|
handleAgentCommand: vi.fn().mockResolvedValue({
|
|
success: true,
|
|
data: { agentId: "agent-123" },
|
|
}),
|
|
};
|
|
|
|
const moduleRef = {
|
|
get: vi.fn().mockReturnValue(mockAgentService),
|
|
};
|
|
|
|
// Inject moduleRef into service
|
|
(service as never)["moduleRef"] = moduleRef;
|
|
|
|
const result = await service.handleIncomingCommand(commandMessage);
|
|
|
|
expect(result.success).toBe(true);
|
|
expect(result.data).toEqual({ agentId: "agent-123" });
|
|
});
|
|
});
|
|
});
|