Files
stack/apps/api/src/federation/query.controller.spec.ts
Jason Woltje 1159ca42a7 feat(#88): implement QUERY message type for federation
Implement complete QUERY message protocol for federated queries between
Mosaic Stack instances, building on existing connection infrastructure.

Database Changes:
- Add FederationMessageType enum (QUERY, COMMAND, EVENT)
- Add FederationMessageStatus enum (PENDING, DELIVERED, FAILED, TIMEOUT)
- Add FederationMessage model for tracking all federation messages
- Add workspace and connection relations

Types & DTOs:
- QueryMessage: Signed query request payload
- QueryResponse: Signed query response payload
- QueryMessageDetails: API response type
- SendQueryDto: Client request DTO
- IncomingQueryDto: Validated incoming query DTO

QueryService:
- sendQuery: Send signed query to remote instance via ACTIVE connection
- handleIncomingQuery: Process and validate incoming queries
- processQueryResponse: Handle and verify query responses
- getQueryMessages: List workspace queries with optional status filter
- getQueryMessage: Get single query message details
- Message deduplication via unique messageId
- Signature verification using SignatureService
- Timestamp validation (5-minute window)

QueryController:
- POST /api/v1/federation/query: Send query (authenticated)
- POST /api/v1/federation/incoming/query: Receive query (public, signature-verified)
- GET /api/v1/federation/queries: List queries (authenticated)
- GET /api/v1/federation/queries/🆔 Get query details (authenticated)

Security:
- All messages signed with instance private key
- All responses verified with remote public key
- Timestamp validation prevents replay attacks
- Connection status validation (must be ACTIVE)
- Workspace isolation enforced via RLS

Testing:
- 15 QueryService tests (100% coverage)
- 9 QueryController tests (100% coverage)
- All tests passing with proper mocking
- TypeScript strict mode compliance

Refs #88

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-02-03 13:12:12 -06:00

239 lines
7.0 KiB
TypeScript

/**
* Query Controller Tests
*
* Tests for federated query API endpoints.
*/
import { describe, it, expect, beforeEach, vi } from "vitest";
import { Test, TestingModule } from "@nestjs/testing";
import { FederationMessageType, FederationMessageStatus } from "@prisma/client";
import { QueryController } from "./query.controller";
import { QueryService } from "./query.service";
import { AuthGuard } from "../auth/guards/auth.guard";
import type { AuthenticatedRequest } from "../common/types/user.types";
import type { SendQueryDto, IncomingQueryDto } from "./dto/query.dto";
describe("QueryController", () => {
let controller: QueryController;
let queryService: QueryService;
const mockQueryService = {
sendQuery: vi.fn(),
handleIncomingQuery: vi.fn(),
getQueryMessages: vi.fn(),
getQueryMessage: vi.fn(),
};
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
controllers: [QueryController],
providers: [{ provide: QueryService, useValue: mockQueryService }],
})
.overrideGuard(AuthGuard)
.useValue({ canActivate: () => true })
.compile();
controller = module.get<QueryController>(QueryController);
queryService = module.get<QueryService>(QueryService);
vi.clearAllMocks();
});
describe("sendQuery", () => {
it("should send query to remote instance", async () => {
const req = {
user: {
id: "user-1",
workspaceId: "workspace-1",
},
} as AuthenticatedRequest;
const dto: SendQueryDto = {
connectionId: "connection-1",
query: "SELECT * FROM tasks",
context: { userId: "user-1" },
};
const mockResult = {
id: "msg-1",
workspaceId: "workspace-1",
connectionId: "connection-1",
messageType: FederationMessageType.QUERY,
messageId: "unique-msg-1",
query: dto.query,
status: FederationMessageStatus.PENDING,
createdAt: new Date(),
updatedAt: new Date(),
};
mockQueryService.sendQuery.mockResolvedValue(mockResult);
const result = await controller.sendQuery(req, dto);
expect(result).toBeDefined();
expect(result.messageType).toBe(FederationMessageType.QUERY);
expect(mockQueryService.sendQuery).toHaveBeenCalledWith(
"workspace-1",
dto.connectionId,
dto.query,
dto.context
);
});
it("should throw error if user not authenticated", async () => {
const req = {} as AuthenticatedRequest;
const dto: SendQueryDto = {
connectionId: "connection-1",
query: "SELECT * FROM tasks",
};
await expect(controller.sendQuery(req, dto)).rejects.toThrow(
"Workspace ID not found in request"
);
});
});
describe("handleIncomingQuery", () => {
it("should process incoming query", async () => {
const dto: IncomingQueryDto = {
messageId: "msg-1",
instanceId: "remote-instance-1",
query: "SELECT * FROM tasks",
timestamp: Date.now(),
signature: "valid-signature",
};
const mockResponse = {
messageId: "response-1",
correlationId: dto.messageId,
instanceId: "local-instance-1",
success: true,
data: { tasks: [] },
timestamp: Date.now(),
signature: "response-signature",
};
mockQueryService.handleIncomingQuery.mockResolvedValue(mockResponse);
const result = await controller.handleIncomingQuery(dto);
expect(result).toBeDefined();
expect(result.correlationId).toBe(dto.messageId);
expect(mockQueryService.handleIncomingQuery).toHaveBeenCalledWith(dto);
});
it("should return error response for invalid query", async () => {
const dto: IncomingQueryDto = {
messageId: "msg-1",
instanceId: "remote-instance-1",
query: "SELECT * FROM tasks",
timestamp: Date.now(),
signature: "invalid-signature",
};
mockQueryService.handleIncomingQuery.mockRejectedValue(new Error("Invalid signature"));
await expect(controller.handleIncomingQuery(dto)).rejects.toThrow("Invalid signature");
});
});
describe("getQueries", () => {
it("should return query messages for workspace", async () => {
const req = {
user: {
id: "user-1",
workspaceId: "workspace-1",
},
} as AuthenticatedRequest;
const mockMessages = [
{
id: "msg-1",
workspaceId: "workspace-1",
connectionId: "connection-1",
messageType: FederationMessageType.QUERY,
messageId: "unique-msg-1",
query: "SELECT * FROM tasks",
status: FederationMessageStatus.DELIVERED,
createdAt: new Date(),
updatedAt: new Date(),
},
];
mockQueryService.getQueryMessages.mockResolvedValue(mockMessages);
const result = await controller.getQueries(req, undefined);
expect(result).toHaveLength(1);
expect(mockQueryService.getQueryMessages).toHaveBeenCalledWith("workspace-1", undefined);
});
it("should filter by status when provided", async () => {
const req = {
user: {
id: "user-1",
workspaceId: "workspace-1",
},
} as AuthenticatedRequest;
const status = FederationMessageStatus.PENDING;
mockQueryService.getQueryMessages.mockResolvedValue([]);
await controller.getQueries(req, status);
expect(mockQueryService.getQueryMessages).toHaveBeenCalledWith("workspace-1", status);
});
it("should throw error if user not authenticated", async () => {
const req = {} as AuthenticatedRequest;
await expect(controller.getQueries(req, undefined)).rejects.toThrow(
"Workspace ID not found in request"
);
});
});
describe("getQuery", () => {
it("should return query message by ID", async () => {
const req = {
user: {
id: "user-1",
workspaceId: "workspace-1",
},
} as AuthenticatedRequest;
const messageId = "msg-1";
const mockMessage = {
id: messageId,
workspaceId: "workspace-1",
connectionId: "connection-1",
messageType: FederationMessageType.QUERY,
messageId: "unique-msg-1",
query: "SELECT * FROM tasks",
status: FederationMessageStatus.DELIVERED,
createdAt: new Date(),
updatedAt: new Date(),
};
mockQueryService.getQueryMessage.mockResolvedValue(mockMessage);
const result = await controller.getQuery(req, messageId);
expect(result).toBeDefined();
expect(result.id).toBe(messageId);
expect(mockQueryService.getQueryMessage).toHaveBeenCalledWith("workspace-1", messageId);
});
it("should throw error if user not authenticated", async () => {
const req = {} as AuthenticatedRequest;
await expect(controller.getQuery(req, "msg-1")).rejects.toThrow(
"Workspace ID not found in request"
);
});
});
});