Files
stack/apps/api/src/federation/event.service.spec.ts
Jason Woltje ca4f5ec011 feat(#90): implement EVENT subscriptions for federation
Implement event pub/sub messaging for federation to enable real-time
event streaming between federated instances.

Features:
- Event subscription management (subscribe/unsubscribe)
- Event publishing to subscribed instances
- Event acknowledgment protocol
- Server-side event filtering based on subscriptions
- Full signature verification and connection validation

Implementation:
- FederationEventSubscription model for storing subscriptions
- EventService with complete event lifecycle management
- EventController with authenticated and public endpoints
- EventMessage, EventAck, and SubscriptionDetails types
- Comprehensive DTOs for all event operations

API Endpoints:
- POST /api/v1/federation/events/subscribe
- POST /api/v1/federation/events/unsubscribe
- POST /api/v1/federation/events/publish
- GET /api/v1/federation/events/subscriptions
- GET /api/v1/federation/events/messages
- POST /api/v1/federation/incoming/event (public)
- POST /api/v1/federation/incoming/event/ack (public)

Testing:
- 18 unit tests for EventService (89.09% coverage)
- 11 unit tests for EventController (83.87% coverage)
- All 29 tests passing
- Follows TDD red-green-refactor cycle

Technical Notes:
- Reuses existing FederationMessage model with eventType field
- Follows patterns from QueryService and CommandService
- Uses existing signature and connection infrastructure
- Supports hierarchical event type naming (e.g., "task.created")

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

826 lines
26 KiB
TypeScript

/**
* EventService Tests
*
* Tests for federated event message handling.
*/
import { describe, it, expect, beforeEach, vi } from "vitest";
import { Test, TestingModule } from "@nestjs/testing";
import { EventService } from "./event.service";
import { PrismaService } from "../prisma/prisma.service";
import { FederationService } from "./federation.service";
import { SignatureService } from "./signature.service";
import { HttpService } from "@nestjs/axios";
import { of, throwError } from "rxjs";
import {
FederationConnectionStatus,
FederationMessageType,
FederationMessageStatus,
} from "@prisma/client";
import type { EventMessage, EventAck } from "./types/message.types";
import type { AxiosResponse } from "axios";
describe("EventService", () => {
let service: EventService;
let prisma: PrismaService;
let federationService: FederationService;
let signatureService: SignatureService;
let httpService: HttpService;
const mockWorkspaceId = "workspace-123";
const mockConnectionId = "connection-123";
const mockInstanceId = "instance-123";
const mockRemoteInstanceId = "remote-instance-123";
const mockMessageId = "message-123";
const mockEventType = "task.created";
const mockPrisma = {
federationConnection: {
findUnique: vi.fn(),
findFirst: vi.fn(),
},
federationEventSubscription: {
create: vi.fn(),
findMany: vi.fn(),
findUnique: vi.fn(),
findFirst: vi.fn(),
update: vi.fn(),
delete: vi.fn(),
},
federationMessage: {
create: vi.fn(),
findMany: vi.fn(),
findUnique: vi.fn(),
findFirst: vi.fn(),
update: vi.fn(),
},
};
const mockFederationService = {
getInstanceIdentity: vi.fn(),
};
const mockSignatureService = {
signMessage: vi.fn(),
verifyMessage: vi.fn(),
validateTimestamp: vi.fn(),
};
const mockHttpService = {
post: vi.fn(),
};
beforeEach(async () => {
vi.clearAllMocks();
const module: TestingModule = await Test.createTestingModule({
providers: [
EventService,
{
provide: PrismaService,
useValue: mockPrisma,
},
{
provide: FederationService,
useValue: mockFederationService,
},
{
provide: SignatureService,
useValue: mockSignatureService,
},
{
provide: HttpService,
useValue: mockHttpService,
},
],
}).compile();
service = module.get<EventService>(EventService);
prisma = module.get(PrismaService);
federationService = module.get(FederationService);
signatureService = module.get(SignatureService);
httpService = module.get(HttpService);
});
describe("subscribeToEventType", () => {
it("should create a new subscription", async () => {
const mockConnection = {
id: mockConnectionId,
workspaceId: mockWorkspaceId,
remoteInstanceId: mockRemoteInstanceId,
remoteUrl: "https://remote.example.com",
remotePublicKey: "public-key",
remoteCapabilities: {},
status: FederationConnectionStatus.ACTIVE,
metadata: {},
createdAt: new Date(),
updatedAt: new Date(),
connectedAt: new Date(),
disconnectedAt: null,
};
const mockSubscription = {
id: "subscription-123",
workspaceId: mockWorkspaceId,
connectionId: mockConnectionId,
eventType: mockEventType,
metadata: {},
isActive: true,
createdAt: new Date(),
updatedAt: new Date(),
};
prisma.federationConnection.findUnique.mockResolvedValue(mockConnection);
prisma.federationEventSubscription.create.mockResolvedValue(mockSubscription);
const result = await service.subscribeToEventType(
mockWorkspaceId,
mockConnectionId,
mockEventType
);
expect(result).toEqual({
id: mockSubscription.id,
workspaceId: mockSubscription.workspaceId,
connectionId: mockSubscription.connectionId,
eventType: mockSubscription.eventType,
metadata: mockSubscription.metadata,
isActive: mockSubscription.isActive,
createdAt: mockSubscription.createdAt,
updatedAt: mockSubscription.updatedAt,
});
expect(prisma.federationConnection.findUnique).toHaveBeenCalledWith({
where: { id: mockConnectionId, workspaceId: mockWorkspaceId },
});
expect(prisma.federationEventSubscription.create).toHaveBeenCalledWith({
data: {
workspaceId: mockWorkspaceId,
connectionId: mockConnectionId,
eventType: mockEventType,
metadata: {},
},
});
});
it("should throw error if connection not found", async () => {
prisma.federationConnection.findUnique.mockResolvedValue(null);
await expect(
service.subscribeToEventType(mockWorkspaceId, mockConnectionId, mockEventType)
).rejects.toThrow("Connection not found");
});
it("should throw error if connection not active", async () => {
const mockConnection = {
id: mockConnectionId,
workspaceId: mockWorkspaceId,
remoteInstanceId: mockRemoteInstanceId,
remoteUrl: "https://remote.example.com",
remotePublicKey: "public-key",
remoteCapabilities: {},
status: FederationConnectionStatus.SUSPENDED,
metadata: {},
createdAt: new Date(),
updatedAt: new Date(),
connectedAt: new Date(),
disconnectedAt: null,
};
prisma.federationConnection.findUnique.mockResolvedValue(mockConnection);
await expect(
service.subscribeToEventType(mockWorkspaceId, mockConnectionId, mockEventType)
).rejects.toThrow("Connection is not active");
});
});
describe("unsubscribeFromEventType", () => {
it("should delete an existing subscription", async () => {
const mockSubscription = {
id: "subscription-123",
workspaceId: mockWorkspaceId,
connectionId: mockConnectionId,
eventType: mockEventType,
metadata: {},
isActive: true,
createdAt: new Date(),
updatedAt: new Date(),
};
prisma.federationEventSubscription.findFirst.mockResolvedValue(mockSubscription);
prisma.federationEventSubscription.delete.mockResolvedValue(mockSubscription);
await service.unsubscribeFromEventType(mockWorkspaceId, mockConnectionId, mockEventType);
expect(prisma.federationEventSubscription.findFirst).toHaveBeenCalledWith({
where: {
workspaceId: mockWorkspaceId,
connectionId: mockConnectionId,
eventType: mockEventType,
},
});
expect(prisma.federationEventSubscription.delete).toHaveBeenCalledWith({
where: { id: mockSubscription.id },
});
});
it("should throw error if subscription not found", async () => {
prisma.federationEventSubscription.findFirst.mockResolvedValue(null);
await expect(
service.unsubscribeFromEventType(mockWorkspaceId, mockConnectionId, mockEventType)
).rejects.toThrow("Subscription not found");
});
});
describe("publishEvent", () => {
it("should publish event to subscribed connections", async () => {
const mockConnection = {
id: mockConnectionId,
workspaceId: mockWorkspaceId,
remoteInstanceId: mockRemoteInstanceId,
remoteUrl: "https://remote.example.com",
remotePublicKey: "public-key",
remoteCapabilities: {},
status: FederationConnectionStatus.ACTIVE,
metadata: {},
createdAt: new Date(),
updatedAt: new Date(),
connectedAt: new Date(),
disconnectedAt: null,
};
const mockSubscription = {
id: "subscription-123",
workspaceId: mockWorkspaceId,
connectionId: mockConnectionId,
eventType: mockEventType,
metadata: {},
isActive: true,
createdAt: new Date(),
updatedAt: new Date(),
connection: mockConnection,
};
const mockIdentity = {
id: "id-123",
instanceId: mockInstanceId,
name: "Local Instance",
url: "https://local.example.com",
publicKey: "public-key",
privateKey: "private-key",
capabilities: {},
metadata: {},
createdAt: new Date(),
updatedAt: new Date(),
};
const mockMessage = {
id: "message-123",
workspaceId: mockWorkspaceId,
connectionId: mockConnectionId,
messageType: FederationMessageType.EVENT,
messageId: expect.any(String),
correlationId: null,
query: null,
commandType: null,
eventType: mockEventType,
payload: { data: "test" },
response: null,
status: FederationMessageStatus.PENDING,
error: null,
signature: "signature-123",
createdAt: new Date(),
updatedAt: new Date(),
deliveredAt: null,
};
prisma.federationEventSubscription.findMany.mockResolvedValue([mockSubscription]);
federationService.getInstanceIdentity.mockResolvedValue(mockIdentity);
signatureService.signMessage.mockResolvedValue("signature-123");
prisma.federationMessage.create.mockResolvedValue(mockMessage);
httpService.post.mockReturnValue(
of({ data: {}, status: 200, statusText: "OK", headers: {}, config: {} as never })
);
const result = await service.publishEvent(mockWorkspaceId, mockEventType, { data: "test" });
expect(result).toHaveLength(1);
expect(result[0]).toMatchObject({
id: mockMessage.id,
workspaceId: mockMessage.workspaceId,
connectionId: mockMessage.connectionId,
messageType: mockMessage.messageType,
eventType: mockMessage.eventType,
status: mockMessage.status,
});
expect(prisma.federationEventSubscription.findMany).toHaveBeenCalledWith({
where: {
workspaceId: mockWorkspaceId,
eventType: mockEventType,
isActive: true,
},
include: {
connection: true,
},
});
expect(httpService.post).toHaveBeenCalledWith(
`${mockConnection.remoteUrl}/api/v1/federation/incoming/event`,
expect.objectContaining({
instanceId: mockInstanceId,
eventType: mockEventType,
payload: { data: "test" },
signature: "signature-123",
})
);
});
it("should return empty array if no active subscriptions", async () => {
prisma.federationEventSubscription.findMany.mockResolvedValue([]);
const result = await service.publishEvent(mockWorkspaceId, mockEventType, { data: "test" });
expect(result).toEqual([]);
});
it("should handle failed delivery", async () => {
const mockConnection = {
id: mockConnectionId,
workspaceId: mockWorkspaceId,
remoteInstanceId: mockRemoteInstanceId,
remoteUrl: "https://remote.example.com",
remotePublicKey: "public-key",
remoteCapabilities: {},
status: FederationConnectionStatus.ACTIVE,
metadata: {},
createdAt: new Date(),
updatedAt: new Date(),
connectedAt: new Date(),
disconnectedAt: null,
};
const mockSubscription = {
id: "subscription-123",
workspaceId: mockWorkspaceId,
connectionId: mockConnectionId,
eventType: mockEventType,
metadata: {},
isActive: true,
createdAt: new Date(),
updatedAt: new Date(),
connection: mockConnection,
};
const mockIdentity = {
id: "id-123",
instanceId: mockInstanceId,
name: "Local Instance",
url: "https://local.example.com",
publicKey: "public-key",
privateKey: "private-key",
capabilities: {},
metadata: {},
createdAt: new Date(),
updatedAt: new Date(),
};
const mockMessage = {
id: "message-123",
workspaceId: mockWorkspaceId,
connectionId: mockConnectionId,
messageType: FederationMessageType.EVENT,
messageId: expect.any(String),
correlationId: null,
query: null,
commandType: null,
eventType: mockEventType,
payload: { data: "test" },
response: null,
status: FederationMessageStatus.PENDING,
error: null,
signature: "signature-123",
createdAt: new Date(),
updatedAt: new Date(),
deliveredAt: null,
};
prisma.federationEventSubscription.findMany.mockResolvedValue([mockSubscription]);
federationService.getInstanceIdentity.mockResolvedValue(mockIdentity);
signatureService.signMessage.mockResolvedValue("signature-123");
prisma.federationMessage.create.mockResolvedValue(mockMessage);
httpService.post.mockReturnValue(throwError(() => new Error("Network error")));
prisma.federationMessage.update.mockResolvedValue({
...mockMessage,
status: FederationMessageStatus.FAILED,
error: "Network error",
});
const result = await service.publishEvent(mockWorkspaceId, mockEventType, { data: "test" });
expect(result).toHaveLength(1);
expect(prisma.federationMessage.update).toHaveBeenCalledWith({
where: { id: mockMessage.id },
data: {
status: FederationMessageStatus.FAILED,
error: "Network error",
},
});
});
});
describe("handleIncomingEvent", () => {
it("should handle incoming event and return acknowledgment", async () => {
const eventMessage: EventMessage = {
messageId: mockMessageId,
instanceId: mockRemoteInstanceId,
eventType: mockEventType,
payload: { data: "test" },
timestamp: Date.now(),
signature: "signature-123",
};
const mockConnection = {
id: mockConnectionId,
workspaceId: mockWorkspaceId,
remoteInstanceId: mockRemoteInstanceId,
remoteUrl: "https://remote.example.com",
remotePublicKey: "public-key",
remoteCapabilities: {},
status: FederationConnectionStatus.ACTIVE,
metadata: {},
createdAt: new Date(),
updatedAt: new Date(),
connectedAt: new Date(),
disconnectedAt: null,
};
const mockIdentity = {
id: "id-123",
instanceId: mockInstanceId,
name: "Local Instance",
url: "https://local.example.com",
publicKey: "public-key",
privateKey: "private-key",
capabilities: {},
metadata: {},
createdAt: new Date(),
updatedAt: new Date(),
};
signatureService.validateTimestamp.mockReturnValue(true);
prisma.federationConnection.findFirst.mockResolvedValue(mockConnection);
signatureService.verifyMessage.mockResolvedValue({ valid: true, error: null });
federationService.getInstanceIdentity.mockResolvedValue(mockIdentity);
signatureService.signMessage.mockResolvedValue("ack-signature-123");
const result = await service.handleIncomingEvent(eventMessage);
expect(result).toEqual({
messageId: expect.any(String),
correlationId: mockMessageId,
instanceId: mockInstanceId,
received: true,
timestamp: expect.any(Number),
signature: "ack-signature-123",
});
expect(signatureService.validateTimestamp).toHaveBeenCalledWith(eventMessage.timestamp);
expect(prisma.federationConnection.findFirst).toHaveBeenCalledWith({
where: {
remoteInstanceId: mockRemoteInstanceId,
status: FederationConnectionStatus.ACTIVE,
},
});
expect(signatureService.verifyMessage).toHaveBeenCalledWith(
{
messageId: eventMessage.messageId,
instanceId: eventMessage.instanceId,
eventType: eventMessage.eventType,
payload: eventMessage.payload,
timestamp: eventMessage.timestamp,
},
eventMessage.signature,
eventMessage.instanceId
);
});
it("should throw error for invalid timestamp", async () => {
const eventMessage: EventMessage = {
messageId: mockMessageId,
instanceId: mockRemoteInstanceId,
eventType: mockEventType,
payload: { data: "test" },
timestamp: Date.now(),
signature: "signature-123",
};
signatureService.validateTimestamp.mockReturnValue(false);
await expect(service.handleIncomingEvent(eventMessage)).rejects.toThrow(
"Event timestamp is outside acceptable range"
);
});
it("should throw error if no active connection found", async () => {
const eventMessage: EventMessage = {
messageId: mockMessageId,
instanceId: mockRemoteInstanceId,
eventType: mockEventType,
payload: { data: "test" },
timestamp: Date.now(),
signature: "signature-123",
};
signatureService.validateTimestamp.mockReturnValue(true);
prisma.federationConnection.findFirst.mockResolvedValue(null);
await expect(service.handleIncomingEvent(eventMessage)).rejects.toThrow(
"No connection found for remote instance"
);
});
it("should throw error for invalid signature", async () => {
const eventMessage: EventMessage = {
messageId: mockMessageId,
instanceId: mockRemoteInstanceId,
eventType: mockEventType,
payload: { data: "test" },
timestamp: Date.now(),
signature: "signature-123",
};
const mockConnection = {
id: mockConnectionId,
workspaceId: mockWorkspaceId,
remoteInstanceId: mockRemoteInstanceId,
remoteUrl: "https://remote.example.com",
remotePublicKey: "public-key",
remoteCapabilities: {},
status: FederationConnectionStatus.ACTIVE,
metadata: {},
createdAt: new Date(),
updatedAt: new Date(),
connectedAt: new Date(),
disconnectedAt: null,
};
signatureService.validateTimestamp.mockReturnValue(true);
prisma.federationConnection.findFirst.mockResolvedValue(mockConnection);
signatureService.verifyMessage.mockResolvedValue({
valid: false,
error: "Invalid signature",
});
await expect(service.handleIncomingEvent(eventMessage)).rejects.toThrow("Invalid signature");
});
});
describe("processEventAck", () => {
it("should process event acknowledgment", async () => {
const ack: EventAck = {
messageId: "ack-123",
correlationId: mockMessageId,
instanceId: mockRemoteInstanceId,
received: true,
timestamp: Date.now(),
signature: "ack-signature-123",
};
const mockMessage = {
id: "message-123",
workspaceId: mockWorkspaceId,
connectionId: mockConnectionId,
messageType: FederationMessageType.EVENT,
messageId: mockMessageId,
correlationId: null,
query: null,
commandType: null,
eventType: mockEventType,
payload: { data: "test" },
response: null,
status: FederationMessageStatus.PENDING,
error: null,
signature: "signature-123",
createdAt: new Date(),
updatedAt: new Date(),
deliveredAt: null,
};
signatureService.validateTimestamp.mockReturnValue(true);
prisma.federationMessage.findFirst.mockResolvedValue(mockMessage);
signatureService.verifyMessage.mockResolvedValue({ valid: true, error: null });
prisma.federationMessage.update.mockResolvedValue({
...mockMessage,
status: FederationMessageStatus.DELIVERED,
deliveredAt: new Date(),
});
await service.processEventAck(ack);
expect(signatureService.validateTimestamp).toHaveBeenCalledWith(ack.timestamp);
expect(prisma.federationMessage.findFirst).toHaveBeenCalledWith({
where: {
messageId: ack.correlationId,
messageType: FederationMessageType.EVENT,
},
});
expect(prisma.federationMessage.update).toHaveBeenCalledWith({
where: { id: mockMessage.id },
data: {
status: FederationMessageStatus.DELIVERED,
deliveredAt: expect.any(Date),
},
});
});
it("should throw error if original event not found", async () => {
const ack: EventAck = {
messageId: "ack-123",
correlationId: mockMessageId,
instanceId: mockRemoteInstanceId,
received: true,
timestamp: Date.now(),
signature: "ack-signature-123",
};
signatureService.validateTimestamp.mockReturnValue(true);
prisma.federationMessage.findFirst.mockResolvedValue(null);
await expect(service.processEventAck(ack)).rejects.toThrow(
"Original event message not found"
);
});
});
describe("getEventSubscriptions", () => {
it("should return all subscriptions for workspace", async () => {
const mockSubscriptions = [
{
id: "sub-1",
workspaceId: mockWorkspaceId,
connectionId: mockConnectionId,
eventType: "task.created",
metadata: {},
isActive: true,
createdAt: new Date(),
updatedAt: new Date(),
},
{
id: "sub-2",
workspaceId: mockWorkspaceId,
connectionId: mockConnectionId,
eventType: "task.updated",
metadata: {},
isActive: true,
createdAt: new Date(),
updatedAt: new Date(),
},
];
prisma.federationEventSubscription.findMany.mockResolvedValue(mockSubscriptions);
const result = await service.getEventSubscriptions(mockWorkspaceId);
expect(result).toHaveLength(2);
expect(prisma.federationEventSubscription.findMany).toHaveBeenCalledWith({
where: {
workspaceId: mockWorkspaceId,
},
orderBy: { createdAt: "desc" },
});
});
it("should filter by connectionId when provided", async () => {
const mockSubscriptions = [
{
id: "sub-1",
workspaceId: mockWorkspaceId,
connectionId: mockConnectionId,
eventType: "task.created",
metadata: {},
isActive: true,
createdAt: new Date(),
updatedAt: new Date(),
},
];
prisma.federationEventSubscription.findMany.mockResolvedValue(mockSubscriptions);
const result = await service.getEventSubscriptions(mockWorkspaceId, mockConnectionId);
expect(result).toHaveLength(1);
expect(prisma.federationEventSubscription.findMany).toHaveBeenCalledWith({
where: {
workspaceId: mockWorkspaceId,
connectionId: mockConnectionId,
},
orderBy: { createdAt: "desc" },
});
});
});
describe("getEventMessages", () => {
it("should return all event messages for workspace", async () => {
const mockMessages = [
{
id: "msg-1",
workspaceId: mockWorkspaceId,
connectionId: mockConnectionId,
messageType: FederationMessageType.EVENT,
messageId: "msg-id-1",
correlationId: null,
query: null,
commandType: null,
eventType: "task.created",
payload: { data: "test1" },
response: null,
status: FederationMessageStatus.DELIVERED,
error: null,
signature: "sig-1",
createdAt: new Date(),
updatedAt: new Date(),
deliveredAt: new Date(),
},
{
id: "msg-2",
workspaceId: mockWorkspaceId,
connectionId: mockConnectionId,
messageType: FederationMessageType.EVENT,
messageId: "msg-id-2",
correlationId: null,
query: null,
commandType: null,
eventType: "task.updated",
payload: { data: "test2" },
response: null,
status: FederationMessageStatus.PENDING,
error: null,
signature: "sig-2",
createdAt: new Date(),
updatedAt: new Date(),
deliveredAt: null,
},
];
prisma.federationMessage.findMany.mockResolvedValue(mockMessages);
const result = await service.getEventMessages(mockWorkspaceId);
expect(result).toHaveLength(2);
expect(prisma.federationMessage.findMany).toHaveBeenCalledWith({
where: {
workspaceId: mockWorkspaceId,
messageType: FederationMessageType.EVENT,
},
orderBy: { createdAt: "desc" },
});
});
it("should filter by status when provided", async () => {
const mockMessages = [
{
id: "msg-1",
workspaceId: mockWorkspaceId,
connectionId: mockConnectionId,
messageType: FederationMessageType.EVENT,
messageId: "msg-id-1",
correlationId: null,
query: null,
commandType: null,
eventType: "task.created",
payload: { data: "test1" },
response: null,
status: FederationMessageStatus.PENDING,
error: null,
signature: "sig-1",
createdAt: new Date(),
updatedAt: new Date(),
deliveredAt: null,
},
];
prisma.federationMessage.findMany.mockResolvedValue(mockMessages);
const result = await service.getEventMessages(
mockWorkspaceId,
FederationMessageStatus.PENDING
);
expect(result).toHaveLength(1);
expect(prisma.federationMessage.findMany).toHaveBeenCalledWith({
where: {
workspaceId: mockWorkspaceId,
messageType: FederationMessageType.EVENT,
status: FederationMessageStatus.PENDING,
},
orderBy: { createdAt: "desc" },
});
});
});
});