/** * Capability Guard Tests * * Verifies capability-based authorization enforcement. */ import { describe, it, expect, beforeEach, afterEach, vi } from "vitest"; import { ExecutionContext, ForbiddenException, UnauthorizedException } from "@nestjs/common"; import { Reflector } from "@nestjs/core"; import { Test, TestingModule } from "@nestjs/testing"; import { FederationConnectionStatus } from "@prisma/client"; import { CapabilityGuard, RequireCapability } from "./capability.guard"; import { ConnectionService } from "../connection.service"; import { FederationAuditService } from "../audit.service"; import type { ConnectionDetails } from "../types/connection.types"; describe("CapabilityGuard", () => { let guard: CapabilityGuard; let connectionService: vi.Mocked; let auditService: vi.Mocked; let reflector: Reflector; const mockConnection: ConnectionDetails = { id: "conn-123", workspaceId: "ws-456", remoteInstanceId: "remote-instance", remoteUrl: "https://remote.example.com", remotePublicKey: "public-key", remoteCapabilities: { supportsQuery: true, supportsCommand: false, supportsEvent: true, supportsAgentSpawn: undefined, // Explicitly test undefined }, status: FederationConnectionStatus.ACTIVE, metadata: {}, createdAt: new Date(), updatedAt: new Date(), connectedAt: new Date(), disconnectedAt: null, }; beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ providers: [ CapabilityGuard, { provide: ConnectionService, useValue: { getConnectionById: vi.fn(), }, }, { provide: FederationAuditService, useValue: { logCapabilityDenied: vi.fn(), }, }, Reflector, ], }).compile(); guard = module.get(CapabilityGuard); connectionService = module.get(ConnectionService) as vi.Mocked; auditService = module.get(FederationAuditService) as vi.Mocked; reflector = module.get(Reflector); }); afterEach(() => { vi.clearAllMocks(); }); /** * Helper to create mock execution context */ function createMockContext( requiredCapability: string | undefined, requestData: { body?: Record; headers?: Record; params?: Record; url?: string; } ): ExecutionContext { const mockHandler = vi.fn(); // Mock reflector to return required capability vi.spyOn(reflector, "get").mockReturnValue(requiredCapability); return { getHandler: () => mockHandler, switchToHttp: () => ({ getRequest: () => ({ body: requestData.body || {}, headers: requestData.headers || {}, params: requestData.params || {}, url: requestData.url || "/api/test", }), }), } as unknown as ExecutionContext; } describe("Capability Enforcement", () => { it("should allow access when no capability required", async () => { const context = createMockContext(undefined, {}); const result = await guard.canActivate(context); expect(result).toBe(true); expect(connectionService.getConnectionById).not.toHaveBeenCalled(); }); it("should deny access when connection ID is missing", async () => { const context = createMockContext("supportsQuery", {}); await expect(guard.canActivate(context)).rejects.toThrow(UnauthorizedException); await expect(guard.canActivate(context)).rejects.toThrow( "Federation connection not identified" ); }); it("should deny access when connection does not exist", async () => { const context = createMockContext("supportsQuery", { body: { connectionId: "nonexistent" }, }); connectionService.getConnectionById.mockResolvedValue(null); await expect(guard.canActivate(context)).rejects.toThrow(UnauthorizedException); await expect(guard.canActivate(context)).rejects.toThrow("Invalid federation connection"); }); it("should deny access when connection is not CONNECTED", async () => { const context = createMockContext("supportsQuery", { body: { connectionId: "conn-123" }, }); connectionService.getConnectionById.mockResolvedValue({ ...mockConnection, status: FederationConnectionStatus.PENDING, }); await expect(guard.canActivate(context)).rejects.toThrow(ForbiddenException); await expect(guard.canActivate(context)).rejects.toThrow("Connection is not active"); }); it("should deny access when capability is not granted", async () => { const context = createMockContext("supportsCommand", { body: { connectionId: "conn-123" }, url: "/api/execute-command", }); connectionService.getConnectionById.mockResolvedValue(mockConnection); await expect(guard.canActivate(context)).rejects.toThrow(ForbiddenException); await expect(guard.canActivate(context)).rejects.toThrow( "Operation requires capability: supportsCommand" ); expect(auditService.logCapabilityDenied).toHaveBeenCalledWith( "remote-instance", "supportsCommand", "/api/execute-command" ); }); it("should allow access when capability is granted", async () => { const context = createMockContext("supportsQuery", { body: { connectionId: "conn-123" }, }); connectionService.getConnectionById.mockResolvedValue(mockConnection); const result = await guard.canActivate(context); expect(result).toBe(true); expect(auditService.logCapabilityDenied).not.toHaveBeenCalled(); }); }); describe("Connection ID Extraction", () => { it("should extract connection ID from request body", async () => { const context = createMockContext("supportsQuery", { body: { connectionId: "conn-123" }, }); connectionService.getConnectionById.mockResolvedValue(mockConnection); await guard.canActivate(context); expect(connectionService.getConnectionById).toHaveBeenCalledWith("conn-123"); }); it("should extract connection ID from headers", async () => { const context = createMockContext("supportsQuery", { headers: { "x-federation-connection-id": "conn-456" }, }); connectionService.getConnectionById.mockResolvedValue(mockConnection); await guard.canActivate(context); expect(connectionService.getConnectionById).toHaveBeenCalledWith("conn-456"); }); it("should extract connection ID from route params", async () => { const context = createMockContext("supportsQuery", { params: { connectionId: "conn-789" }, }); connectionService.getConnectionById.mockResolvedValue(mockConnection); await guard.canActivate(context); expect(connectionService.getConnectionById).toHaveBeenCalledWith("conn-789"); }); }); describe("Fail-Closed Security", () => { it("should deny access when capability is undefined (fail-closed)", async () => { const context = createMockContext("supportsAgentSpawn", { body: { connectionId: "conn-123" }, }); connectionService.getConnectionById.mockResolvedValue(mockConnection); await expect(guard.canActivate(context)).rejects.toThrow(ForbiddenException); await expect(guard.canActivate(context)).rejects.toThrow( "Operation requires capability: supportsAgentSpawn" ); }); it("should only allow explicitly true values (not truthy)", async () => { const context = createMockContext("supportsEvent", { body: { connectionId: "conn-123" }, }); // Test with explicitly true (should pass) connectionService.getConnectionById.mockResolvedValue({ ...mockConnection, remoteCapabilities: { supportsEvent: true }, }); const resultTrue = await guard.canActivate(context); expect(resultTrue).toBe(true); // Test with truthy but not true (should fail) connectionService.getConnectionById.mockResolvedValue({ ...mockConnection, remoteCapabilities: { supportsEvent: 1 as unknown as boolean }, }); await expect(guard.canActivate(context)).rejects.toThrow(ForbiddenException); }); }); describe("RequireCapability Decorator", () => { it("should create decorator with correct metadata", () => { const decorator = RequireCapability("supportsQuery"); expect(decorator).toBeDefined(); expect(typeof decorator).toBe("function"); }); }); });