/** * Federation Auth Controller Tests * * Tests for federated authentication API endpoints. */ import { describe, it, expect, beforeEach, vi } from "vitest"; import { Test, TestingModule } from "@nestjs/testing"; import { FederationAuthController } from "./federation-auth.controller"; import { OIDCService } from "./oidc.service"; import { FederationAuditService } from "./audit.service"; import { AuthGuard } from "../auth/guards/auth.guard"; import type { AuthenticatedRequest } from "../common/types/user.types"; import type { FederatedIdentity } from "./types/oidc.types"; import { InitiateFederatedAuthDto, LinkFederatedIdentityDto, ValidateFederatedTokenDto, } from "./dto/federated-auth.dto"; describe("FederationAuthController", () => { let controller: FederationAuthController; let oidcService: OIDCService; let auditService: FederationAuditService; const mockOIDCService = { generateAuthUrl: vi.fn(), linkFederatedIdentity: vi.fn(), getUserFederatedIdentities: vi.fn(), getFederatedIdentity: vi.fn(), revokeFederatedIdentity: vi.fn(), validateToken: vi.fn(), }; const mockAuditService = { logFederatedAuthInitiation: vi.fn(), logFederatedIdentityLinked: vi.fn(), logFederatedIdentityRevoked: vi.fn(), }; const mockUser = { id: "user-123", email: "user@example.com", workspaceId: "workspace-abc", }; beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ controllers: [FederationAuthController], providers: [ { provide: OIDCService, useValue: mockOIDCService }, { provide: FederationAuditService, useValue: mockAuditService }, ], }) .overrideGuard(AuthGuard) .useValue({ canActivate: () => true }) .compile(); controller = module.get(FederationAuthController); oidcService = module.get(OIDCService); auditService = module.get(FederationAuditService); vi.clearAllMocks(); }); describe("POST /api/v1/federation/auth/initiate", () => { it("should initiate federated auth flow", () => { const dto: InitiateFederatedAuthDto = { remoteInstanceId: "remote-instance-123", redirectUrl: "http://localhost:3000/callback", }; const mockAuthUrl = "https://auth.remote.com/authorize?client_id=abc&..."; mockOIDCService.generateAuthUrl.mockReturnValue(mockAuthUrl); const req = { user: mockUser } as AuthenticatedRequest; const result = controller.initiateAuth(req, dto); expect(result).toEqual({ authUrl: mockAuthUrl, state: dto.remoteInstanceId, }); expect(mockOIDCService.generateAuthUrl).toHaveBeenCalledWith( dto.remoteInstanceId, dto.redirectUrl ); expect(mockAuditService.logFederatedAuthInitiation).toHaveBeenCalledWith( mockUser.id, dto.remoteInstanceId ); }); it("should require authentication", () => { const dto: InitiateFederatedAuthDto = { remoteInstanceId: "remote-instance-123", }; const req = { user: null } as unknown as AuthenticatedRequest; expect(() => controller.initiateAuth(req, dto)).toThrow(); }); }); describe("POST /api/v1/federation/auth/link", () => { it("should link federated identity", async () => { const dto: LinkFederatedIdentityDto = { remoteInstanceId: "remote-instance-123", remoteUserId: "remote-user-456", oidcSubject: "oidc-sub-abc", email: "user@example.com", metadata: { displayName: "John Doe" }, }; const mockIdentity: FederatedIdentity = { id: "identity-uuid", localUserId: mockUser.id, remoteUserId: dto.remoteUserId, remoteInstanceId: dto.remoteInstanceId, oidcSubject: dto.oidcSubject, email: dto.email, metadata: dto.metadata ?? {}, createdAt: new Date(), updatedAt: new Date(), }; mockOIDCService.linkFederatedIdentity.mockResolvedValue(mockIdentity); const req = { user: mockUser } as AuthenticatedRequest; const result = await controller.linkIdentity(req, dto); expect(result).toEqual(mockIdentity); expect(mockOIDCService.linkFederatedIdentity).toHaveBeenCalledWith( mockUser.id, dto.remoteUserId, dto.remoteInstanceId, dto.oidcSubject, dto.email, dto.metadata ); expect(mockAuditService.logFederatedIdentityLinked).toHaveBeenCalledWith( mockUser.id, dto.remoteInstanceId ); }); it("should require authentication", async () => { const dto: LinkFederatedIdentityDto = { remoteInstanceId: "remote-instance-123", remoteUserId: "remote-user-456", oidcSubject: "oidc-sub-abc", email: "user@example.com", }; const req = { user: null } as unknown as AuthenticatedRequest; await expect(controller.linkIdentity(req, dto)).rejects.toThrow(); }); }); describe("GET /api/v1/federation/auth/identities", () => { it("should return user's federated identities", async () => { const mockIdentities: FederatedIdentity[] = [ { id: "identity-1", localUserId: mockUser.id, remoteUserId: "remote-1", remoteInstanceId: "instance-1", oidcSubject: "sub-1", email: mockUser.email, metadata: {}, createdAt: new Date(), updatedAt: new Date(), }, { id: "identity-2", localUserId: mockUser.id, remoteUserId: "remote-2", remoteInstanceId: "instance-2", oidcSubject: "sub-2", email: mockUser.email, metadata: {}, createdAt: new Date(), updatedAt: new Date(), }, ]; mockOIDCService.getUserFederatedIdentities.mockResolvedValue(mockIdentities); const req = { user: mockUser } as AuthenticatedRequest; const result = await controller.getIdentities(req); expect(result).toEqual(mockIdentities); expect(mockOIDCService.getUserFederatedIdentities).toHaveBeenCalledWith(mockUser.id); }); it("should require authentication", async () => { const req = { user: null } as unknown as AuthenticatedRequest; await expect(controller.getIdentities(req)).rejects.toThrow(); }); }); describe("DELETE /api/v1/federation/auth/identities/:instanceId", () => { it("should revoke federated identity", async () => { const instanceId = "remote-instance-123"; mockOIDCService.revokeFederatedIdentity.mockResolvedValue(undefined); const req = { user: mockUser } as AuthenticatedRequest; const result = await controller.revokeIdentity(req, instanceId); expect(result).toEqual({ success: true }); expect(mockOIDCService.revokeFederatedIdentity).toHaveBeenCalledWith(mockUser.id, instanceId); expect(mockAuditService.logFederatedIdentityRevoked).toHaveBeenCalledWith( mockUser.id, instanceId ); }); it("should require authentication", async () => { const req = { user: null } as unknown as AuthenticatedRequest; await expect(controller.revokeIdentity(req, "instance-123")).rejects.toThrow(); }); }); describe("POST /api/v1/federation/auth/validate", () => { it("should validate federated token", async () => { const dto: ValidateFederatedTokenDto = { token: "valid-token", instanceId: "remote-instance-123", }; const mockValidation = { valid: true, userId: "user-subject-123", instanceId: dto.instanceId, email: "user@example.com", subject: "user-subject-123", }; mockOIDCService.validateToken.mockResolvedValue(mockValidation); const result = await controller.validateToken(dto); expect(result).toEqual(mockValidation); expect(mockOIDCService.validateToken).toHaveBeenCalledWith(dto.token, dto.instanceId); }); it("should return invalid for expired token", async () => { const dto: ValidateFederatedTokenDto = { token: "expired-token", instanceId: "remote-instance-123", }; const mockValidation = { valid: false, error: "Token has expired", }; mockOIDCService.validateToken.mockResolvedValue(mockValidation); const result = await controller.validateToken(dto); expect(result.valid).toBe(false); expect(result.error).toBeDefined(); }); }); });