/** * Federation Service Tests */ import { describe, it, expect, beforeEach, afterEach, vi } from "vitest"; import { Test, TestingModule } from "@nestjs/testing"; import { FederationService } from "./federation.service"; import { CryptoService } from "./crypto.service"; import { PrismaService } from "../prisma/prisma.service"; import { ConfigService } from "@nestjs/config"; import { Instance } from "@prisma/client"; describe("FederationService", () => { let service: FederationService; let prismaService: PrismaService; let configService: ConfigService; let cryptoService: CryptoService; // Mock encrypted private key (simulates encrypted storage) const mockEncryptedPrivateKey = "iv:authTag:encryptedData"; const mockDecryptedPrivateKey = "-----BEGIN PRIVATE KEY-----\nMOCK\n-----END PRIVATE KEY-----"; const mockInstance: Instance = { id: "123e4567-e89b-12d3-a456-426614174000", instanceId: "test-instance-id", name: "Test Instance", url: "https://test.example.com", publicKey: "-----BEGIN PUBLIC KEY-----\nMOCK\n-----END PUBLIC KEY-----", privateKey: mockEncryptedPrivateKey, // Stored encrypted capabilities: { supportsQuery: true, supportsCommand: true, supportsEvent: true, protocolVersion: "1.0", }, metadata: {}, createdAt: new Date("2026-01-01T00:00:00Z"), updatedAt: new Date("2026-01-01T00:00:00Z"), }; beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ providers: [ FederationService, { provide: PrismaService, useValue: { instance: { findFirst: vi.fn(), create: vi.fn(), update: vi.fn(), }, }, }, { provide: ConfigService, useValue: { get: vi.fn((key: string) => { const config: Record = { INSTANCE_NAME: "Test Instance", INSTANCE_URL: "https://test.example.com", ENCRYPTION_KEY: "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef", }; return config[key]; }), }, }, { provide: CryptoService, useValue: { encrypt: vi.fn((data: string) => mockEncryptedPrivateKey), decrypt: vi.fn((encrypted: string) => mockDecryptedPrivateKey), }, }, ], }).compile(); service = module.get(FederationService); prismaService = module.get(PrismaService); configService = module.get(ConfigService); cryptoService = module.get(CryptoService); }); afterEach(() => { vi.clearAllMocks(); }); describe("getInstanceIdentity", () => { it("should return existing instance identity if found", async () => { // Arrange vi.spyOn(prismaService.instance, "findFirst").mockResolvedValue(mockInstance); // Act const result = await service.getInstanceIdentity(); // Assert expect(result.privateKey).toEqual(mockDecryptedPrivateKey); // Decrypted expect(cryptoService.decrypt).toHaveBeenCalledWith(mockEncryptedPrivateKey); expect(prismaService.instance.findFirst).toHaveBeenCalledTimes(1); }); it("should create new instance identity if not found", async () => { // Arrange vi.spyOn(prismaService.instance, "findFirst").mockResolvedValue(null); vi.spyOn(prismaService.instance, "create").mockResolvedValue(mockInstance); vi.spyOn(service, "generateKeypair").mockReturnValue({ publicKey: mockInstance.publicKey, privateKey: mockDecryptedPrivateKey, }); // Act const result = await service.getInstanceIdentity(); // Assert expect(result.privateKey).toEqual(mockDecryptedPrivateKey); expect(cryptoService.encrypt).toHaveBeenCalled(); // Private key encrypted before storage expect(prismaService.instance.findFirst).toHaveBeenCalledTimes(1); expect(service.generateKeypair).toHaveBeenCalledTimes(1); expect(prismaService.instance.create).toHaveBeenCalledTimes(1); }); it("should use config values for instance name and URL", async () => { // Arrange vi.spyOn(prismaService.instance, "findFirst").mockResolvedValue(null); vi.spyOn(prismaService.instance, "create").mockResolvedValue(mockInstance); vi.spyOn(service, "generateKeypair").mockReturnValue({ publicKey: mockInstance.publicKey, privateKey: mockDecryptedPrivateKey, }); // Act await service.getInstanceIdentity(); // Assert expect(configService.get).toHaveBeenCalledWith("INSTANCE_NAME"); expect(configService.get).toHaveBeenCalledWith("INSTANCE_URL"); }); it("should throw error for invalid URL", async () => { // Arrange vi.spyOn(prismaService.instance, "findFirst").mockResolvedValue(null); vi.spyOn(configService, "get").mockImplementation((key: string) => { if (key === "INSTANCE_URL") return "invalid-url"; return "Test Instance"; }); vi.spyOn(service, "generateKeypair").mockReturnValue({ publicKey: mockInstance.publicKey, privateKey: mockDecryptedPrivateKey, }); // Act & Assert await expect(service.getInstanceIdentity()).rejects.toThrow("Invalid INSTANCE_URL"); }); }); describe("getPublicIdentity", () => { it("should return instance identity without private key", async () => { // Arrange vi.spyOn(service, "getInstanceIdentity").mockResolvedValue(mockInstance); // Act const result = await service.getPublicIdentity(); // Assert expect(result).toEqual({ id: mockInstance.id, instanceId: mockInstance.instanceId, name: mockInstance.name, url: mockInstance.url, publicKey: mockInstance.publicKey, capabilities: mockInstance.capabilities, metadata: mockInstance.metadata, createdAt: mockInstance.createdAt, updatedAt: mockInstance.updatedAt, }); expect(result).not.toHaveProperty("privateKey"); }); }); describe("generateKeypair", () => { it("should generate valid RSA key pair", { timeout: 30000 }, () => { // Act const result = service.generateKeypair(); // Assert expect(result).toHaveProperty("publicKey"); expect(result).toHaveProperty("privateKey"); expect(result.publicKey).toContain("BEGIN PUBLIC KEY"); expect(result.privateKey).toContain("BEGIN PRIVATE KEY"); }); it("should generate different key pairs on each call", { timeout: 60000 }, () => { // Act const result1 = service.generateKeypair(); const result2 = service.generateKeypair(); // Assert expect(result1.publicKey).not.toEqual(result2.publicKey); expect(result1.privateKey).not.toEqual(result2.privateKey); }); it("should generate RSA-4096 key pairs for future-proof security", { timeout: 30000 }, () => { // Act const result = service.generateKeypair(); // Assert - Verify key size by checking approximate length // RSA-4096 keys are significantly larger than RSA-2048 // Private key in PKCS8 format: RSA-2048 ≈ 1700 bytes, RSA-4096 ≈ 3200 bytes // Public key in SPKI format: RSA-2048 ≈ 400 bytes, RSA-4096 ≈ 800 bytes expect(result.privateKey.length).toBeGreaterThan(3000); expect(result.publicKey.length).toBeGreaterThan(700); }); }); describe("regenerateKeypair", () => { it("should generate new keypair and update instance", async () => { // Arrange const updatedInstance = { ...mockInstance }; vi.spyOn(service, "getInstanceIdentity").mockResolvedValue({ ...mockInstance, privateKey: mockDecryptedPrivateKey, }); vi.spyOn(service, "generateKeypair").mockReturnValue({ publicKey: "NEW_PUBLIC_KEY", privateKey: "NEW_PRIVATE_KEY", }); vi.spyOn(prismaService.instance, "update").mockResolvedValue(updatedInstance); // Act const result = await service.regenerateKeypair(); // Assert expect(service.generateKeypair).toHaveBeenCalledTimes(1); expect(cryptoService.encrypt).toHaveBeenCalledWith("NEW_PRIVATE_KEY"); // Encrypted before storage expect(prismaService.instance.update).toHaveBeenCalled(); // SECURITY FIX: Verify private key is NOT in response expect(result).not.toHaveProperty("privateKey"); expect(result).toHaveProperty("publicKey"); expect(result).toHaveProperty("instanceId"); }); }); describe("updateInstanceConfiguration", () => { it("should update instance name", async () => { // Arrange const updatedInstance = { ...mockInstance, name: "Updated Instance" }; vi.spyOn(service, "getInstanceIdentity").mockResolvedValue({ ...mockInstance, privateKey: mockDecryptedPrivateKey, }); vi.spyOn(prismaService.instance, "update").mockResolvedValue(updatedInstance); // Act const result = await service.updateInstanceConfiguration({ name: "Updated Instance" }); // Assert expect(prismaService.instance.update).toHaveBeenCalledWith({ where: { id: mockInstance.id }, data: { name: "Updated Instance" }, }); expect(result.name).toBe("Updated Instance"); expect(result).not.toHaveProperty("privateKey"); }); it("should update instance capabilities", async () => { // Arrange const newCapabilities = { supportsQuery: true, supportsCommand: false, supportsEvent: true, supportsAgentSpawn: false, protocolVersion: "1.0", }; const updatedInstance = { ...mockInstance, capabilities: newCapabilities }; vi.spyOn(service, "getInstanceIdentity").mockResolvedValue({ ...mockInstance, privateKey: mockDecryptedPrivateKey, }); vi.spyOn(prismaService.instance, "update").mockResolvedValue(updatedInstance); // Act const result = await service.updateInstanceConfiguration({ capabilities: newCapabilities }); // Assert expect(prismaService.instance.update).toHaveBeenCalledWith({ where: { id: mockInstance.id }, data: { capabilities: newCapabilities }, }); expect(result.capabilities).toEqual(newCapabilities); }); it("should update instance metadata", async () => { // Arrange const newMetadata = { description: "Test description", region: "us-west-2" }; const updatedInstance = { ...mockInstance, metadata: newMetadata }; vi.spyOn(service, "getInstanceIdentity").mockResolvedValue({ ...mockInstance, privateKey: mockDecryptedPrivateKey, }); vi.spyOn(prismaService.instance, "update").mockResolvedValue(updatedInstance); // Act const result = await service.updateInstanceConfiguration({ metadata: newMetadata }); // Assert expect(prismaService.instance.update).toHaveBeenCalledWith({ where: { id: mockInstance.id }, data: { metadata: newMetadata }, }); expect(result.metadata).toEqual(newMetadata); }); it("should update multiple fields at once", async () => { // Arrange const updates = { name: "Updated Instance", capabilities: { supportsQuery: false, supportsCommand: false, supportsEvent: false, supportsAgentSpawn: false, protocolVersion: "1.0", }, metadata: { description: "Updated" }, }; const updatedInstance = { ...mockInstance, ...updates }; vi.spyOn(service, "getInstanceIdentity").mockResolvedValue({ ...mockInstance, privateKey: mockDecryptedPrivateKey, }); vi.spyOn(prismaService.instance, "update").mockResolvedValue(updatedInstance); // Act const result = await service.updateInstanceConfiguration(updates); // Assert expect(prismaService.instance.update).toHaveBeenCalledWith({ where: { id: mockInstance.id }, data: updates, }); expect(result.name).toBe("Updated Instance"); expect(result.capabilities).toEqual(updates.capabilities); expect(result.metadata).toEqual(updates.metadata); }); it("should not expose private key in response", async () => { // Arrange const updatedInstance = { ...mockInstance, name: "Updated" }; vi.spyOn(service, "getInstanceIdentity").mockResolvedValue({ ...mockInstance, privateKey: mockDecryptedPrivateKey, }); vi.spyOn(prismaService.instance, "update").mockResolvedValue(updatedInstance); // Act const result = await service.updateInstanceConfiguration({ name: "Updated" }); // Assert - SECURITY: Verify private key is NOT in response expect(result).not.toHaveProperty("privateKey"); expect(result).toHaveProperty("publicKey"); expect(result).toHaveProperty("instanceId"); }); }); });