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 Session Lifecycle Management * * Tests cover: * - Session creation on login/register * - Session validation across multiple requests * - Session refresh (TTL extension) * - Session invalidation on logout * - Multiple concurrent sessions * - Session persistence across requests (Redis) * - Account lockout after failed attempts */ describe("Session Lifecycle (e2e)", () => { let app: INestApplication; const testUser = { email: `session-e2e-${Date.now()}@example.com`, username: `sessionuser${Date.now()}`, password: "SecureSession123!", }; 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 test user await request(app.getHttpServer()) .post("/auth/register") .send(testUser); }); afterAll(async () => { await app.close(); }); describe("Session Creation", () => { it("should create session on successful registration", async () => { const uniqueUser = { email: `session-reg-${Date.now()}@example.com`, username: `sessionreg${Date.now()}`, password: "SecurePass123!", }; const res = await request(app.getHttpServer()) .post("/auth/register") .send(uniqueUser) .expect(200); expect(res.body.sessionId).toBeDefined(); expect(res.body.sessionId.length).toBeGreaterThan(10); expect(res.body.user).toBeDefined(); expect(res.body.user.email).toBe(uniqueUser.email); }); it("should create session on successful login", async () => { const res = await request(app.getHttpServer()) .post("/auth/login") .send({ email: testUser.email, password: testUser.password, }) .expect(200); expect(res.body.sessionId).toBeDefined(); expect(res.body.user).toBeDefined(); }); }); describe("Multiple Concurrent Sessions", () => { it("should support multiple active sessions for same user", async () => { // Create two sessions const login1 = await request(app.getHttpServer()) .post("/auth/login") .send({ email: testUser.email, password: testUser.password }) .expect(200); const login2 = await request(app.getHttpServer()) .post("/auth/login") .send({ email: testUser.email, password: testUser.password }) .expect(200); const token1 = login1.body.sessionId; const token2 = login2.body.sessionId; // Both sessions should be different expect(token1).not.toBe(token2); // Both should be valid await request(app.getHttpServer()) .get("/auth/validate") .set("Authorization", `Bearer ${token1}`) .expect(200); await request(app.getHttpServer()) .get("/auth/validate") .set("Authorization", `Bearer ${token2}`) .expect(200); }); it("should only invalidate specific session on logout", async () => { const login1 = await request(app.getHttpServer()) .post("/auth/login") .send({ email: testUser.email, password: testUser.password }) .expect(200); const login2 = await request(app.getHttpServer()) .post("/auth/login") .send({ email: testUser.email, password: testUser.password }) .expect(200); const token1 = login1.body.sessionId; const token2 = login2.body.sessionId; // Logout session 1 await request(app.getHttpServer()) .post("/auth/logout") .set("Authorization", `Bearer ${token1}`) .expect(200); // Session 1 should be invalid await request(app.getHttpServer()) .get("/auth/validate") .set("Authorization", `Bearer ${token1}`) .expect(401); // Session 2 should still be valid await request(app.getHttpServer()) .get("/auth/validate") .set("Authorization", `Bearer ${token2}`) .expect(200); }); }); describe("Session Refresh", () => { it("should refresh session TTL", async () => { const loginRes = await request(app.getHttpServer()) .post("/auth/login") .send({ email: testUser.email, password: testUser.password }) .expect(200); const token = loginRes.body.sessionId; // Refresh session await request(app.getHttpServer()) .post("/auth/refresh") .set("Authorization", `Bearer ${token}`) .expect(200) .expect((res) => { expect(res.body.success).toBe(true); }); // Session should still be valid after refresh await request(app.getHttpServer()) .get("/auth/validate") .set("Authorization", `Bearer ${token}`) .expect(200); }); }); describe("Session Data Consistency", () => { it("should return consistent user data across /validate and /me", async () => { const loginRes = await request(app.getHttpServer()) .post("/auth/login") .send({ email: testUser.email, password: testUser.password }) .expect(200); const token = loginRes.body.sessionId; const validateRes = await request(app.getHttpServer()) .get("/auth/validate") .set("Authorization", `Bearer ${token}`) .expect(200); const meRes = await request(app.getHttpServer()) .get("/auth/me") .set("Authorization", `Bearer ${token}`) .expect(200); // Both endpoints should return same user data expect(validateRes.body.user.email).toBe(meRes.body.user.email); expect(validateRes.body.user.username).toBe(meRes.body.user.username); expect(validateRes.body.user.id).toBe(meRes.body.user.id); // Neither should expose password hash expect(validateRes.body.user.passwordHash).toBeUndefined(); expect(meRes.body.user.passwordHash).toBeUndefined(); }); }); describe("Account Lockout", () => { it("should return remaining attempts after failed login", async () => { const res = await request(app.getHttpServer()) .post("/auth/login") .send({ email: testUser.email, password: "WrongPassword123!", }); // Should be 401 with remaining attempts info expect(res.status).toBe(401); if (res.body.remainingAttempts !== undefined) { expect(res.body.remainingAttempts).toBeDefined(); } }); }); describe("Edge Cases", () => { it("should handle empty Authorization header", () => { return request(app.getHttpServer()) .get("/auth/validate") .set("Authorization", "") .expect(401); }); it("should handle malformed Bearer token", () => { return request(app.getHttpServer()) .get("/auth/validate") .set("Authorization", "Bearer ") .expect(401); }); it("should handle non-Bearer auth scheme", () => { return request(app.getHttpServer()) .get("/auth/validate") .set("Authorization", "Basic dXNlcjpwYXNz") .expect(401); }); }); });