246 lines
7.4 KiB
TypeScript
246 lines
7.4 KiB
TypeScript
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);
|
|
});
|
|
});
|
|
});
|