Files
stack/apps/api/src/federation/query.service.spec.ts
T
jason.woltjeandClaude Sonnet 4.5 4ac4219ce0 fix(#297): Implement actual query processing for federation
Added query processing to route federation queries to domain services:
- Created query parser to extract intent and parameters from query strings
- Route queries to TasksService, EventsService, and ProjectsService
- Return actual data instead of placeholder responses
- Added workspace context validation

Implemented query types:
- Tasks: "get tasks", "show tasks", etc.
- Events: "get events", "upcoming events", etc.
- Projects: "get projects", "show projects", etc.

Added 5 new tests for query processing (20 tests total, all passing):
- Process tasks/events/projects queries
- Handle unknown query types
- Enforce workspace context requirements

Updated FederationModule to import TasksModule, EventsModule, ProjectsModule.

Fixes #297

Co-Authored-By: Claude Sonnet 4.5 <[email protected]>
2026-02-03 22:48:59 -06:00

709 lines
23 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");
});
});
});