import { Test, TestingModule } from "@nestjs/testing"; import { INestApplication, ValidationPipe } from "@nestjs/common"; import { AppModule } from "@/src/app.module"; const request = require("supertest"); /** * E2E Tests for MFA (Multi-Factor Authentication) Flow * * Prerequisites: * 1. Start test services: docker-compose -f test/docker-compose.yml up -d * 2. Run: pnpm test:e2e * * Tests cover: * - MFA status retrieval * - TOTP setup flow (secret + QR code generation) * - Email MFA setup * - MFA challenge during login * - Recovery code usage * - MFA disable flow (password-verified) * - Rate limiting on MFA endpoints */ describe("MFA Controller (e2e)", () => { let app: INestApplication; let sessionToken: string; const testUser = { email: `mfa-e2e-${Date.now()}@example.com`, username: `mfauser${Date.now()}`, password: "SecureMfaPass123!", }; beforeAll(async () => { const moduleFixture: TestingModule = await Test.createTestingModule({ imports: [AppModule], }).compile(); app = moduleFixture.createNestApplication(); app.useGlobalPipes( new ValidationPipe({ whitelist: true, transform: true, forbidNonWhitelisted: true, }), ); await app.init(); // Register and login to get session token const registerRes = await request(app.getHttpServer()) .post("/auth/register") .send(testUser); sessionToken = registerRes.body.sessionId; }); afterAll(async () => { await app.close(); }); describe("GET /auth/mfa/status", () => { it("should return MFA status for authenticated user", () => { return request(app.getHttpServer()) .get("/auth/mfa/status") .set("Authorization", `Bearer ${sessionToken}`) .expect(200) .expect((res) => { expect(res.body.enabled).toBe(false); expect(res.body.methods).toEqual([]); expect(res.body.totpEnabled).toBe(false); expect(res.body.emailEnabled).toBe(false); expect(res.body.recoveryCodesRemaining).toBeDefined(); }); }); it("should reject unauthenticated requests", () => { return request(app.getHttpServer()) .get("/auth/mfa/status") .expect(401); }); }); describe("POST /auth/mfa/setup/totp", () => { it("should return TOTP setup data with QR code", () => { return request(app.getHttpServer()) .post("/auth/mfa/setup/totp") .set("Authorization", `Bearer ${sessionToken}`) .expect(200) .expect((res) => { expect(res.body.secret).toBeDefined(); expect(res.body.secret.length).toBeGreaterThan(10); expect(res.body.otpauthUrl).toBeDefined(); expect(res.body.otpauthUrl).toContain("otpauth://totp/"); expect(res.body.qrCodeDataUrl).toBeDefined(); expect(res.body.qrCodeDataUrl).toContain("data:image/png;base64,"); }); }); it("should reject unauthenticated TOTP setup", () => { return request(app.getHttpServer()) .post("/auth/mfa/setup/totp") .expect(401); }); }); describe("POST /auth/mfa/setup/email", () => { it("should enable email MFA for authenticated user", () => { return request(app.getHttpServer()) .post("/auth/mfa/setup/email") .set("Authorization", `Bearer ${sessionToken}`) .expect(200) .expect((res) => { expect(res.body.success).toBe(true); }); }); it("should show email MFA enabled in status", async () => { const res = await request(app.getHttpServer()) .get("/auth/mfa/status") .set("Authorization", `Bearer ${sessionToken}`) .expect(200); expect(res.body.emailEnabled).toBe(true); expect(res.body.enabled).toBe(true); expect(res.body.methods).toContain("email"); }); }); describe("POST /auth/mfa/verify/totp", () => { it("should reject invalid TOTP code", () => { return request(app.getHttpServer()) .post("/auth/mfa/verify/totp") .set("Authorization", `Bearer ${sessionToken}`) .send({ secret: "INVALIDSECRET", code: "000000" }) .expect(400); }); }); describe("POST /auth/mfa/challenge", () => { it("should reject invalid pending session ID", () => { return request(app.getHttpServer()) .post("/auth/mfa/challenge") .send({ pendingSessionId: "nonexistent-session-id", method: "email", code: "123456", }) .expect(401); }); }); describe("POST /auth/mfa/send-code", () => { it("should reject invalid pending session", () => { return request(app.getHttpServer()) .post("/auth/mfa/send-code") .send({ pendingSessionId: "nonexistent-session-id" }) .expect(401); }); }); describe("POST /auth/mfa/recovery", () => { it("should reject invalid pending session for recovery", () => { return request(app.getHttpServer()) .post("/auth/mfa/recovery") .send({ pendingSessionId: "nonexistent-session-id", recoveryCode: "ABCD1234", }) .expect(401); }); }); describe("POST /auth/mfa/disable", () => { it("should reject disable without valid password", () => { return request(app.getHttpServer()) .post("/auth/mfa/disable") .set("Authorization", `Bearer ${sessionToken}`) .send({ password: "WrongPassword123!", method: "email" }) .expect(401); }); it("should disable MFA method with correct password", () => { return request(app.getHttpServer()) .post("/auth/mfa/disable") .set("Authorization", `Bearer ${sessionToken}`) .send({ password: testUser.password, method: "email" }) .expect(200) .expect((res) => { expect(res.body.success).toBe(true); }); }); }); describe("POST /auth/mfa/preferred", () => { it("should set preferred MFA method", async () => { // Re-enable email MFA first await request(app.getHttpServer()) .post("/auth/mfa/setup/email") .set("Authorization", `Bearer ${sessionToken}`) .expect(200); return request(app.getHttpServer()) .post("/auth/mfa/preferred") .set("Authorization", `Bearer ${sessionToken}`) .send({ method: "email" }) .expect(200) .expect((res) => { expect(res.body.success).toBe(true); }); }); }); describe("Full MFA Login Flow (with email MFA enabled)", () => { it("should require MFA challenge on login when MFA is enabled", async () => { const loginRes = await request(app.getHttpServer()) .post("/auth/login") .send({ email: testUser.email, password: testUser.password, }) .expect(200); // When MFA is enabled, login returns pending session instead of full session if (loginRes.body.mfaRequired) { expect(loginRes.body.pendingSessionId).toBeDefined(); expect(loginRes.body.availableMethods).toBeDefined(); expect(loginRes.body.availableMethods).toContain("email"); expect(loginRes.body.expiresAt).toBeDefined(); } }); }); });