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>
785 lines
26 KiB
TypeScript
785 lines
26 KiB
TypeScript
/**
|
|
* Query Service Tests
|
|
*
|
|
* Tests for federated query message handling.
|
|
*/
|
|
|
|
import { describe, it, expect, beforeEach, vi } from "vitest";
|
|
import { Test, TestingModule } from "@nestjs/testing";
|
|
import { ConfigService } from "@nestjs/config";
|
|
import {
|
|
FederationConnectionStatus,
|
|
FederationMessageType,
|
|
FederationMessageStatus,
|
|
} from "@prisma/client";
|
|
import { QueryService } from "./query.service";
|
|
import { PrismaService } from "../prisma/prisma.service";
|
|
import { FederationService } from "./federation.service";
|
|
import { SignatureService } from "./signature.service";
|
|
import { TasksService } from "../tasks/tasks.service";
|
|
import { EventsService } from "../events/events.service";
|
|
import { ProjectsService } from "../projects/projects.service";
|
|
import { HttpService } from "@nestjs/axios";
|
|
import { of, throwError } from "rxjs";
|
|
import type { AxiosResponse } from "axios";
|
|
|
|
describe("QueryService", () => {
|
|
let service: QueryService;
|
|
let prisma: PrismaService;
|
|
let federationService: FederationService;
|
|
let signatureService: SignatureService;
|
|
let httpService: HttpService;
|
|
|
|
const mockPrisma = {
|
|
federationConnection: {
|
|
findUnique: vi.fn(),
|
|
findFirst: vi.fn(),
|
|
},
|
|
federationMessage: {
|
|
create: vi.fn(),
|
|
findMany: vi.fn(),
|
|
findUnique: vi.fn(),
|
|
findFirst: vi.fn(),
|
|
update: vi.fn(),
|
|
},
|
|
};
|
|
|
|
const mockFederationService = {
|
|
getInstanceIdentity: vi.fn(),
|
|
getPublicIdentity: vi.fn(),
|
|
};
|
|
|
|
const mockSignatureService = {
|
|
signMessage: vi.fn(),
|
|
verifyMessage: vi.fn(),
|
|
validateTimestamp: vi.fn(),
|
|
};
|
|
|
|
const mockHttpService = {
|
|
post: vi.fn(),
|
|
};
|
|
|
|
const mockConfig = {
|
|
get: vi.fn(),
|
|
};
|
|
|
|
const mockTasksService = {
|
|
findAll: vi.fn(),
|
|
};
|
|
|
|
const mockEventsService = {
|
|
findAll: vi.fn(),
|
|
};
|
|
|
|
const mockProjectsService = {
|
|
findAll: vi.fn(),
|
|
};
|
|
|
|
beforeEach(async () => {
|
|
const module: TestingModule = await Test.createTestingModule({
|
|
providers: [
|
|
QueryService,
|
|
{ provide: PrismaService, useValue: mockPrisma },
|
|
{ provide: FederationService, useValue: mockFederationService },
|
|
{ provide: SignatureService, useValue: mockSignatureService },
|
|
{ provide: HttpService, useValue: mockHttpService },
|
|
{ provide: ConfigService, useValue: mockConfig },
|
|
{ provide: TasksService, useValue: mockTasksService },
|
|
{ provide: EventsService, useValue: mockEventsService },
|
|
{ provide: ProjectsService, useValue: mockProjectsService },
|
|
],
|
|
}).compile();
|
|
|
|
service = module.get<QueryService>(QueryService);
|
|
prisma = module.get<PrismaService>(PrismaService);
|
|
federationService = module.get<FederationService>(FederationService);
|
|
signatureService = module.get<SignatureService>(SignatureService);
|
|
httpService = module.get<HttpService>(HttpService);
|
|
|
|
// Setup default mock return values for service queries
|
|
mockTasksService.findAll.mockResolvedValue({
|
|
data: [],
|
|
meta: { total: 0, page: 1, limit: 50, totalPages: 0 },
|
|
});
|
|
mockEventsService.findAll.mockResolvedValue({
|
|
data: [],
|
|
meta: { total: 0, page: 1, limit: 50, totalPages: 0 },
|
|
});
|
|
mockProjectsService.findAll.mockResolvedValue({
|
|
data: [],
|
|
meta: { total: 0, page: 1, limit: 50, totalPages: 0 },
|
|
});
|
|
|
|
vi.clearAllMocks();
|
|
});
|
|
|
|
describe("sendQuery", () => {
|
|
it("should send query to remote instance with signed message", async () => {
|
|
const workspaceId = "workspace-1";
|
|
const connectionId = "connection-1";
|
|
const query = "SELECT * FROM tasks";
|
|
const context = { userId: "user-1" };
|
|
|
|
const mockConnection = {
|
|
id: connectionId,
|
|
workspaceId,
|
|
remoteInstanceId: "remote-instance-1",
|
|
remoteUrl: "https://remote.example.com",
|
|
remotePublicKey: "mock-public-key",
|
|
status: FederationConnectionStatus.ACTIVE,
|
|
};
|
|
|
|
const mockIdentity = {
|
|
id: "identity-1",
|
|
instanceId: "local-instance-1",
|
|
name: "Local Instance",
|
|
url: "https://local.example.com",
|
|
publicKey: "local-public-key",
|
|
privateKey: "local-private-key",
|
|
};
|
|
|
|
const mockMessage = {
|
|
id: "message-1",
|
|
workspaceId,
|
|
connectionId,
|
|
messageType: FederationMessageType.QUERY,
|
|
messageId: expect.any(String),
|
|
correlationId: null,
|
|
query,
|
|
response: null,
|
|
status: FederationMessageStatus.PENDING,
|
|
error: null,
|
|
signature: "mock-signature",
|
|
createdAt: new Date(),
|
|
updatedAt: new Date(),
|
|
deliveredAt: null,
|
|
};
|
|
|
|
const mockResponse: AxiosResponse = {
|
|
data: { success: true },
|
|
status: 200,
|
|
statusText: "OK",
|
|
headers: {},
|
|
config: { headers: {} as never },
|
|
};
|
|
|
|
mockPrisma.federationConnection.findUnique.mockResolvedValue(mockConnection);
|
|
mockFederationService.getInstanceIdentity.mockResolvedValue(mockIdentity);
|
|
mockSignatureService.signMessage.mockResolvedValue("mock-signature");
|
|
mockPrisma.federationMessage.create.mockResolvedValue(mockMessage);
|
|
mockHttpService.post.mockReturnValue(of(mockResponse));
|
|
|
|
const result = await service.sendQuery(workspaceId, connectionId, query, context);
|
|
|
|
expect(result).toBeDefined();
|
|
expect(result.messageType).toBe(FederationMessageType.QUERY);
|
|
expect(result.query).toBe(query);
|
|
expect(mockPrisma.federationConnection.findUnique).toHaveBeenCalledWith({
|
|
where: {
|
|
id: connectionId,
|
|
workspaceId,
|
|
status: FederationConnectionStatus.ACTIVE,
|
|
},
|
|
});
|
|
expect(mockPrisma.federationMessage.create).toHaveBeenCalled();
|
|
expect(mockHttpService.post).toHaveBeenCalledWith(
|
|
`${mockConnection.remoteUrl}/api/v1/federation/incoming/query`,
|
|
expect.objectContaining({
|
|
messageId: expect.any(String),
|
|
instanceId: mockIdentity.instanceId,
|
|
query,
|
|
context,
|
|
timestamp: expect.any(Number),
|
|
signature: "mock-signature",
|
|
})
|
|
);
|
|
});
|
|
|
|
it("should throw error if connection not found", async () => {
|
|
mockPrisma.federationConnection.findUnique.mockResolvedValue(null);
|
|
|
|
await expect(
|
|
service.sendQuery("workspace-1", "connection-1", "SELECT * FROM tasks")
|
|
).rejects.toThrow("Connection not found");
|
|
});
|
|
|
|
it("should throw error if connection not active", async () => {
|
|
// Connection should not be found by query because it's not ACTIVE
|
|
mockPrisma.federationConnection.findUnique.mockResolvedValue(null);
|
|
|
|
await expect(
|
|
service.sendQuery("workspace-1", "connection-1", "SELECT * FROM tasks")
|
|
).rejects.toThrow("Connection not found");
|
|
|
|
// Verify that findUnique was called with status: ACTIVE in the query
|
|
expect(mockPrisma.federationConnection.findUnique).toHaveBeenCalledWith({
|
|
where: {
|
|
id: "connection-1",
|
|
workspaceId: "workspace-1",
|
|
status: FederationConnectionStatus.ACTIVE,
|
|
},
|
|
});
|
|
});
|
|
|
|
it("should handle network errors gracefully", async () => {
|
|
const mockConnection = {
|
|
id: "connection-1",
|
|
workspaceId: "workspace-1",
|
|
remoteInstanceId: "remote-instance-1",
|
|
remoteUrl: "https://remote.example.com",
|
|
status: FederationConnectionStatus.ACTIVE,
|
|
};
|
|
|
|
const mockIdentity = {
|
|
instanceId: "local-instance-1",
|
|
};
|
|
|
|
mockPrisma.federationConnection.findUnique.mockResolvedValue(mockConnection);
|
|
mockFederationService.getInstanceIdentity.mockResolvedValue(mockIdentity);
|
|
mockSignatureService.signMessage.mockResolvedValue("mock-signature");
|
|
mockPrisma.federationMessage.create.mockResolvedValue({
|
|
id: "message-1",
|
|
messageId: "msg-1",
|
|
});
|
|
mockHttpService.post.mockReturnValue(throwError(() => new Error("Network error")));
|
|
|
|
await expect(
|
|
service.sendQuery("workspace-1", "connection-1", "SELECT * FROM tasks")
|
|
).rejects.toThrow("Failed to send query");
|
|
});
|
|
});
|
|
|
|
describe("handleIncomingQuery", () => {
|
|
it("should process valid incoming query", async () => {
|
|
const queryMessage = {
|
|
messageId: "msg-1",
|
|
instanceId: "remote-instance-1",
|
|
query: "SELECT * FROM tasks",
|
|
context: {},
|
|
timestamp: Date.now(),
|
|
signature: "valid-signature",
|
|
};
|
|
|
|
const mockConnection = {
|
|
id: "connection-1",
|
|
workspaceId: "workspace-1",
|
|
remoteInstanceId: "remote-instance-1",
|
|
status: FederationConnectionStatus.ACTIVE,
|
|
};
|
|
|
|
const mockIdentity = {
|
|
instanceId: "local-instance-1",
|
|
};
|
|
|
|
mockPrisma.federationConnection.findFirst.mockResolvedValue(mockConnection);
|
|
mockSignatureService.validateTimestamp.mockReturnValue(true);
|
|
mockSignatureService.verifyMessage.mockResolvedValue({ valid: true });
|
|
mockFederationService.getInstanceIdentity.mockResolvedValue(mockIdentity);
|
|
mockSignatureService.signMessage.mockResolvedValue("response-signature");
|
|
|
|
const result = await service.handleIncomingQuery(queryMessage);
|
|
|
|
expect(result).toBeDefined();
|
|
expect(result.messageId).toBeDefined();
|
|
expect(result.correlationId).toBe(queryMessage.messageId);
|
|
expect(result.instanceId).toBe(mockIdentity.instanceId);
|
|
expect(result.signature).toBe("response-signature");
|
|
});
|
|
|
|
it("should reject query with invalid signature", async () => {
|
|
const queryMessage = {
|
|
messageId: "msg-1",
|
|
instanceId: "remote-instance-1",
|
|
query: "SELECT * FROM tasks",
|
|
timestamp: Date.now(),
|
|
signature: "invalid-signature",
|
|
};
|
|
|
|
const mockConnection = {
|
|
id: "connection-1",
|
|
workspaceId: "workspace-1",
|
|
remoteInstanceId: "remote-instance-1",
|
|
status: FederationConnectionStatus.ACTIVE,
|
|
};
|
|
|
|
mockPrisma.federationConnection.findFirst.mockResolvedValue(mockConnection);
|
|
mockSignatureService.validateTimestamp.mockReturnValue(true);
|
|
mockSignatureService.verifyMessage.mockResolvedValue({
|
|
valid: false,
|
|
error: "Invalid signature",
|
|
});
|
|
|
|
await expect(service.handleIncomingQuery(queryMessage)).rejects.toThrow("Invalid signature");
|
|
});
|
|
|
|
it("should reject query with expired timestamp", async () => {
|
|
const queryMessage = {
|
|
messageId: "msg-1",
|
|
instanceId: "remote-instance-1",
|
|
query: "SELECT * FROM tasks",
|
|
timestamp: Date.now() - 10 * 60 * 1000, // 10 minutes ago
|
|
signature: "valid-signature",
|
|
};
|
|
|
|
const mockConnection = {
|
|
id: "connection-1",
|
|
workspaceId: "workspace-1",
|
|
remoteInstanceId: "remote-instance-1",
|
|
status: FederationConnectionStatus.ACTIVE,
|
|
};
|
|
|
|
mockPrisma.federationConnection.findFirst.mockResolvedValue(mockConnection);
|
|
mockSignatureService.validateTimestamp.mockReturnValue(false);
|
|
|
|
await expect(service.handleIncomingQuery(queryMessage)).rejects.toThrow(
|
|
"Query timestamp is outside acceptable range"
|
|
);
|
|
});
|
|
|
|
it("should reject query from inactive connection", async () => {
|
|
const queryMessage = {
|
|
messageId: "msg-1",
|
|
instanceId: "remote-instance-1",
|
|
query: "SELECT * FROM tasks",
|
|
timestamp: Date.now(),
|
|
signature: "valid-signature",
|
|
};
|
|
|
|
// Connection should not be found because status filter in query excludes non-ACTIVE
|
|
mockPrisma.federationConnection.findFirst.mockResolvedValue(null);
|
|
mockSignatureService.validateTimestamp.mockReturnValue(true);
|
|
|
|
await expect(service.handleIncomingQuery(queryMessage)).rejects.toThrow(
|
|
"No connection found for remote instance"
|
|
);
|
|
|
|
// Verify the findFirst was called with status: ACTIVE filter
|
|
expect(mockPrisma.federationConnection.findFirst).toHaveBeenCalledWith({
|
|
where: {
|
|
remoteInstanceId: "remote-instance-1",
|
|
status: FederationConnectionStatus.ACTIVE,
|
|
},
|
|
});
|
|
});
|
|
|
|
it("should reject query from unknown instance", async () => {
|
|
const queryMessage = {
|
|
messageId: "msg-1",
|
|
instanceId: "unknown-instance",
|
|
query: "SELECT * FROM tasks",
|
|
timestamp: Date.now(),
|
|
signature: "valid-signature",
|
|
};
|
|
|
|
mockPrisma.federationConnection.findFirst.mockResolvedValue(null);
|
|
mockSignatureService.validateTimestamp.mockReturnValue(true);
|
|
|
|
await expect(service.handleIncomingQuery(queryMessage)).rejects.toThrow(
|
|
"No connection found for remote instance"
|
|
);
|
|
});
|
|
});
|
|
|
|
describe("getQueryMessages", () => {
|
|
it("should return query messages for workspace", async () => {
|
|
const workspaceId = "workspace-1";
|
|
const mockMessages = [
|
|
{
|
|
id: "msg-1",
|
|
workspaceId,
|
|
connectionId: "connection-1",
|
|
messageType: FederationMessageType.QUERY,
|
|
messageId: "unique-msg-1",
|
|
query: "SELECT * FROM tasks",
|
|
status: FederationMessageStatus.DELIVERED,
|
|
createdAt: new Date(),
|
|
updatedAt: new Date(),
|
|
},
|
|
];
|
|
|
|
mockPrisma.federationMessage.findMany.mockResolvedValue(mockMessages);
|
|
|
|
const result = await service.getQueryMessages(workspaceId);
|
|
|
|
expect(result).toHaveLength(1);
|
|
expect(result[0].id).toBe("msg-1");
|
|
expect(mockPrisma.federationMessage.findMany).toHaveBeenCalledWith({
|
|
where: {
|
|
workspaceId,
|
|
messageType: FederationMessageType.QUERY,
|
|
},
|
|
orderBy: { createdAt: "desc" },
|
|
});
|
|
});
|
|
|
|
it("should filter by status when provided", async () => {
|
|
const workspaceId = "workspace-1";
|
|
const status = FederationMessageStatus.PENDING;
|
|
|
|
mockPrisma.federationMessage.findMany.mockResolvedValue([]);
|
|
|
|
await service.getQueryMessages(workspaceId, status);
|
|
|
|
expect(mockPrisma.federationMessage.findMany).toHaveBeenCalledWith({
|
|
where: {
|
|
workspaceId,
|
|
messageType: FederationMessageType.QUERY,
|
|
status,
|
|
},
|
|
orderBy: { createdAt: "desc" },
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("getQueryMessage", () => {
|
|
it("should return query message by ID", async () => {
|
|
const workspaceId = "workspace-1";
|
|
const messageId = "msg-1";
|
|
const mockMessage = {
|
|
id: "msg-1",
|
|
workspaceId,
|
|
messageType: FederationMessageType.QUERY,
|
|
messageId: "unique-msg-1",
|
|
query: "SELECT * FROM tasks",
|
|
status: FederationMessageStatus.DELIVERED,
|
|
createdAt: new Date(),
|
|
updatedAt: new Date(),
|
|
};
|
|
|
|
mockPrisma.federationMessage.findUnique.mockResolvedValue(mockMessage);
|
|
|
|
const result = await service.getQueryMessage(workspaceId, messageId);
|
|
|
|
expect(result).toBeDefined();
|
|
expect(result.id).toBe(messageId);
|
|
expect(mockPrisma.federationMessage.findUnique).toHaveBeenCalledWith({
|
|
where: { id: messageId, workspaceId },
|
|
});
|
|
});
|
|
|
|
it("should throw error if message not found", async () => {
|
|
mockPrisma.federationMessage.findUnique.mockResolvedValue(null);
|
|
|
|
await expect(service.getQueryMessage("workspace-1", "msg-1")).rejects.toThrow(
|
|
"Query message not found"
|
|
);
|
|
});
|
|
});
|
|
|
|
describe("processQueryResponse", () => {
|
|
it("should update message with response", async () => {
|
|
const response = {
|
|
messageId: "response-1",
|
|
correlationId: "original-msg-1",
|
|
instanceId: "remote-instance-1",
|
|
success: true,
|
|
data: { tasks: [] },
|
|
timestamp: Date.now(),
|
|
signature: "valid-signature",
|
|
};
|
|
|
|
const mockMessage = {
|
|
id: "msg-1",
|
|
messageId: "original-msg-1",
|
|
workspaceId: "workspace-1",
|
|
status: FederationMessageStatus.PENDING,
|
|
};
|
|
|
|
mockPrisma.federationMessage.findFirst.mockResolvedValue(mockMessage);
|
|
mockSignatureService.validateTimestamp.mockReturnValue(true);
|
|
mockSignatureService.verifyMessage.mockResolvedValue({ valid: true });
|
|
mockPrisma.federationMessage.update.mockResolvedValue({
|
|
...mockMessage,
|
|
status: FederationMessageStatus.DELIVERED,
|
|
response: response.data,
|
|
});
|
|
|
|
await service.processQueryResponse(response);
|
|
|
|
expect(mockPrisma.federationMessage.update).toHaveBeenCalledWith({
|
|
where: { id: mockMessage.id },
|
|
data: {
|
|
status: FederationMessageStatus.DELIVERED,
|
|
response: response.data,
|
|
deliveredAt: expect.any(Date),
|
|
},
|
|
});
|
|
});
|
|
|
|
it("should reject response with invalid signature", async () => {
|
|
const response = {
|
|
messageId: "response-1",
|
|
correlationId: "original-msg-1",
|
|
instanceId: "remote-instance-1",
|
|
success: true,
|
|
timestamp: Date.now(),
|
|
signature: "invalid-signature",
|
|
};
|
|
|
|
const mockMessage = {
|
|
id: "msg-1",
|
|
messageId: "original-msg-1",
|
|
workspaceId: "workspace-1",
|
|
};
|
|
|
|
mockPrisma.federationMessage.findFirst.mockResolvedValue(mockMessage);
|
|
mockSignatureService.validateTimestamp.mockReturnValue(true);
|
|
mockSignatureService.verifyMessage.mockResolvedValue({
|
|
valid: false,
|
|
error: "Invalid signature",
|
|
});
|
|
|
|
await expect(service.processQueryResponse(response)).rejects.toThrow("Invalid signature");
|
|
});
|
|
});
|
|
|
|
describe("query processing with actual data", () => {
|
|
it("should process 'get tasks' query and return tasks data", async () => {
|
|
const queryMessage = {
|
|
messageId: "msg-1",
|
|
instanceId: "remote-instance-1",
|
|
query: "get tasks",
|
|
context: { workspaceId: "workspace-1" },
|
|
timestamp: Date.now(),
|
|
signature: "valid-signature",
|
|
};
|
|
|
|
const mockConnection = {
|
|
id: "connection-1",
|
|
workspaceId: "workspace-1",
|
|
remoteInstanceId: "remote-instance-1",
|
|
status: FederationConnectionStatus.ACTIVE,
|
|
};
|
|
|
|
const mockIdentity = {
|
|
instanceId: "local-instance-1",
|
|
};
|
|
|
|
mockPrisma.federationConnection.findFirst.mockResolvedValue(mockConnection);
|
|
mockSignatureService.validateTimestamp.mockReturnValue(true);
|
|
mockSignatureService.verifyMessage.mockResolvedValue({ valid: true });
|
|
mockFederationService.getInstanceIdentity.mockResolvedValue(mockIdentity);
|
|
mockSignatureService.signMessage.mockResolvedValue("response-signature");
|
|
|
|
const result = await service.handleIncomingQuery(queryMessage);
|
|
|
|
expect(result).toBeDefined();
|
|
expect(result.success).toBe(true);
|
|
expect(result.data).toBeDefined();
|
|
expect(result.correlationId).toBe(queryMessage.messageId);
|
|
});
|
|
|
|
it("should process 'get events' query and return events data", async () => {
|
|
const queryMessage = {
|
|
messageId: "msg-2",
|
|
instanceId: "remote-instance-1",
|
|
query: "get events",
|
|
context: { workspaceId: "workspace-1" },
|
|
timestamp: Date.now(),
|
|
signature: "valid-signature",
|
|
};
|
|
|
|
const mockConnection = {
|
|
id: "connection-1",
|
|
workspaceId: "workspace-1",
|
|
remoteInstanceId: "remote-instance-1",
|
|
status: FederationConnectionStatus.ACTIVE,
|
|
};
|
|
|
|
const mockIdentity = {
|
|
instanceId: "local-instance-1",
|
|
};
|
|
|
|
mockPrisma.federationConnection.findFirst.mockResolvedValue(mockConnection);
|
|
mockSignatureService.validateTimestamp.mockReturnValue(true);
|
|
mockSignatureService.verifyMessage.mockResolvedValue({ valid: true });
|
|
mockFederationService.getInstanceIdentity.mockResolvedValue(mockIdentity);
|
|
mockSignatureService.signMessage.mockResolvedValue("response-signature");
|
|
|
|
const result = await service.handleIncomingQuery(queryMessage);
|
|
|
|
expect(result).toBeDefined();
|
|
expect(result.success).toBe(true);
|
|
expect(result.data).toBeDefined();
|
|
});
|
|
|
|
it("should process 'get projects' query and return projects data", async () => {
|
|
const queryMessage = {
|
|
messageId: "msg-3",
|
|
instanceId: "remote-instance-1",
|
|
query: "get projects",
|
|
context: { workspaceId: "workspace-1" },
|
|
timestamp: Date.now(),
|
|
signature: "valid-signature",
|
|
};
|
|
|
|
const mockConnection = {
|
|
id: "connection-1",
|
|
workspaceId: "workspace-1",
|
|
remoteInstanceId: "remote-instance-1",
|
|
status: FederationConnectionStatus.ACTIVE,
|
|
};
|
|
|
|
const mockIdentity = {
|
|
instanceId: "local-instance-1",
|
|
};
|
|
|
|
mockPrisma.federationConnection.findFirst.mockResolvedValue(mockConnection);
|
|
mockSignatureService.validateTimestamp.mockReturnValue(true);
|
|
mockSignatureService.verifyMessage.mockResolvedValue({ valid: true });
|
|
mockFederationService.getInstanceIdentity.mockResolvedValue(mockIdentity);
|
|
mockSignatureService.signMessage.mockResolvedValue("response-signature");
|
|
|
|
const result = await service.handleIncomingQuery(queryMessage);
|
|
|
|
expect(result).toBeDefined();
|
|
expect(result.success).toBe(true);
|
|
expect(result.data).toBeDefined();
|
|
});
|
|
|
|
it("should handle unknown query type gracefully", async () => {
|
|
const queryMessage = {
|
|
messageId: "msg-4",
|
|
instanceId: "remote-instance-1",
|
|
query: "unknown command",
|
|
context: { workspaceId: "workspace-1" },
|
|
timestamp: Date.now(),
|
|
signature: "valid-signature",
|
|
};
|
|
|
|
const mockConnection = {
|
|
id: "connection-1",
|
|
workspaceId: "workspace-1",
|
|
remoteInstanceId: "remote-instance-1",
|
|
status: FederationConnectionStatus.ACTIVE,
|
|
};
|
|
|
|
const mockIdentity = {
|
|
instanceId: "local-instance-1",
|
|
};
|
|
|
|
mockPrisma.federationConnection.findFirst.mockResolvedValue(mockConnection);
|
|
mockSignatureService.validateTimestamp.mockReturnValue(true);
|
|
mockSignatureService.verifyMessage.mockResolvedValue({ valid: true });
|
|
mockFederationService.getInstanceIdentity.mockResolvedValue(mockIdentity);
|
|
mockSignatureService.signMessage.mockResolvedValue("response-signature");
|
|
|
|
const result = await service.handleIncomingQuery(queryMessage);
|
|
|
|
expect(result).toBeDefined();
|
|
expect(result.success).toBe(false);
|
|
expect(result.error).toContain("Unknown query type");
|
|
});
|
|
|
|
it("should enforce workspace context in queries", async () => {
|
|
const queryMessage = {
|
|
messageId: "msg-5",
|
|
instanceId: "remote-instance-1",
|
|
query: "get tasks",
|
|
context: {}, // Missing workspaceId
|
|
timestamp: Date.now(),
|
|
signature: "valid-signature",
|
|
};
|
|
|
|
const mockConnection = {
|
|
id: "connection-1",
|
|
workspaceId: "workspace-1",
|
|
remoteInstanceId: "remote-instance-1",
|
|
status: FederationConnectionStatus.ACTIVE,
|
|
};
|
|
|
|
const mockIdentity = {
|
|
instanceId: "local-instance-1",
|
|
};
|
|
|
|
mockPrisma.federationConnection.findFirst.mockResolvedValue(mockConnection);
|
|
mockSignatureService.validateTimestamp.mockReturnValue(true);
|
|
mockSignatureService.verifyMessage.mockResolvedValue({ valid: true });
|
|
mockFederationService.getInstanceIdentity.mockResolvedValue(mockIdentity);
|
|
mockSignatureService.signMessage.mockResolvedValue("response-signature");
|
|
|
|
const result = await service.handleIncomingQuery(queryMessage);
|
|
|
|
expect(result).toBeDefined();
|
|
expect(result.success).toBe(false);
|
|
expect(result.error).toContain("workspaceId");
|
|
});
|
|
|
|
it("should reject queries for UserCredential entity type", async () => {
|
|
const queryMessage = {
|
|
messageId: "msg-1",
|
|
instanceId: "remote-instance-1",
|
|
query: "credentials",
|
|
context: { workspaceId: "workspace-1" },
|
|
timestamp: Date.now(),
|
|
signature: "valid-signature",
|
|
};
|
|
|
|
const mockConnection = {
|
|
id: "connection-1",
|
|
workspaceId: "workspace-1",
|
|
remoteInstanceId: "remote-instance-1",
|
|
status: FederationConnectionStatus.ACTIVE,
|
|
};
|
|
|
|
const mockIdentity = {
|
|
instanceId: "local-instance-1",
|
|
};
|
|
|
|
mockPrisma.federationConnection.findFirst.mockResolvedValue(mockConnection);
|
|
mockSignatureService.validateTimestamp.mockReturnValue(true);
|
|
mockSignatureService.verifyMessage.mockResolvedValue({ valid: true });
|
|
mockFederationService.getInstanceIdentity.mockResolvedValue(mockIdentity);
|
|
mockSignatureService.signMessage.mockResolvedValue("response-signature");
|
|
|
|
const result = await service.handleIncomingQuery(queryMessage);
|
|
|
|
expect(result).toBeDefined();
|
|
expect(result.success).toBe(false);
|
|
expect(result.error).toContain("Credential queries are not allowed");
|
|
});
|
|
|
|
it("should reject queries containing credential-related keywords", async () => {
|
|
const credentialQueries = [
|
|
"SELECT * FROM user_credentials",
|
|
"get all credentials",
|
|
"show my api keys",
|
|
"list oauth tokens",
|
|
];
|
|
|
|
const mockConnection = {
|
|
id: "connection-1",
|
|
workspaceId: "workspace-1",
|
|
remoteInstanceId: "remote-instance-1",
|
|
status: FederationConnectionStatus.ACTIVE,
|
|
};
|
|
|
|
const mockIdentity = {
|
|
instanceId: "local-instance-1",
|
|
};
|
|
|
|
mockPrisma.federationConnection.findFirst.mockResolvedValue(mockConnection);
|
|
mockSignatureService.validateTimestamp.mockReturnValue(true);
|
|
mockSignatureService.verifyMessage.mockResolvedValue({ valid: true });
|
|
mockFederationService.getInstanceIdentity.mockResolvedValue(mockIdentity);
|
|
mockSignatureService.signMessage.mockResolvedValue("response-signature");
|
|
|
|
for (const query of credentialQueries) {
|
|
const queryMessage = {
|
|
messageId: `msg-${Math.random()}`,
|
|
instanceId: "remote-instance-1",
|
|
query,
|
|
context: { workspaceId: "workspace-1" },
|
|
timestamp: Date.now(),
|
|
signature: "valid-signature",
|
|
};
|
|
|
|
const result = await service.handleIncomingQuery(queryMessage);
|
|
|
|
expect(result.success).toBe(false);
|
|
expect(result.error).toContain("Credential queries are not allowed");
|
|
}
|
|
});
|
|
});
|
|
});
|