/** * 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); 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" }, }); }); }); });