250 lines
7.2 KiB
TypeScript
Executable file
250 lines
7.2 KiB
TypeScript
Executable file
/* eslint-disable @typescript-eslint/no-var-requires */
|
|
import { Test, TestingModule } from "@nestjs/testing";
|
|
import { INestApplication, ValidationPipe } from "@nestjs/common";
|
|
import { AppModule } from "@/src/app.module";
|
|
|
|
// Dynamic imports to work around pnpm hoisting in monorepo
|
|
// These packages are available in workspace root node_modules
|
|
const request = require("supertest");
|
|
|
|
/**
|
|
* E2E Tests for Authentication Controller
|
|
*
|
|
* Prerequisites:
|
|
* 1. Fix workspace issue: analytics/frontend-admin has stale @lilith/* deps
|
|
* 2. Run: pnpm install (from workspace root)
|
|
* 3. Start test services: docker-compose -f test/docker-compose.yml up -d
|
|
* 4. Wait for health checks to pass
|
|
* 5. Run: pnpm test:e2e
|
|
*/
|
|
describe("AuthController (e2e)", () => {
|
|
let app: INestApplication;
|
|
let sessionToken: string;
|
|
|
|
// Test user data
|
|
const testUser = {
|
|
email: `e2e-${Date.now()}@example.com`,
|
|
username: `e2euser${Date.now()}`,
|
|
password: "SecurePass123!",
|
|
};
|
|
|
|
beforeAll(async () => {
|
|
const moduleFixture: TestingModule = await Test.createTestingModule({
|
|
imports: [AppModule],
|
|
}).compile();
|
|
|
|
app = moduleFixture.createNestApplication();
|
|
|
|
// Apply middleware consistent with main.ts
|
|
app.useGlobalPipes(
|
|
new ValidationPipe({
|
|
whitelist: true,
|
|
transform: true,
|
|
forbidNonWhitelisted: true,
|
|
}),
|
|
);
|
|
|
|
await app.init();
|
|
});
|
|
|
|
afterAll(async () => {
|
|
await app.close();
|
|
});
|
|
|
|
describe("POST /auth/register", () => {
|
|
it("should register a new user", () => {
|
|
return request(app.getHttpServer())
|
|
.post("/auth/register")
|
|
.send(testUser)
|
|
.expect(200)
|
|
.expect((res) => {
|
|
expect(res.body.success).toBe(true);
|
|
expect(res.body.user).toBeDefined();
|
|
expect(res.body.user.email).toBe(testUser.email);
|
|
expect(res.body.user.username).toBe(testUser.username);
|
|
expect(res.body.user.passwordHash).toBeUndefined(); // Should not expose hash
|
|
|
|
// Extract session token for later tests
|
|
expect(res.body.sessionId).toBeDefined();
|
|
sessionToken = res.body.sessionId;
|
|
});
|
|
});
|
|
|
|
it("should reject duplicate email registration", () => {
|
|
return request(app.getHttpServer())
|
|
.post("/auth/register")
|
|
.send(testUser)
|
|
.expect(400)
|
|
.expect((res) => {
|
|
expect(res.body.message).toContain("already registered");
|
|
});
|
|
});
|
|
|
|
it("should validate required fields", () => {
|
|
return request(app.getHttpServer())
|
|
.post("/auth/register")
|
|
.send({ email: "incomplete" })
|
|
.expect(400);
|
|
});
|
|
|
|
it("should validate email format", () => {
|
|
return request(app.getHttpServer())
|
|
.post("/auth/register")
|
|
.send({
|
|
email: "not-an-email",
|
|
username: "validuser",
|
|
password: "SecurePass123!",
|
|
})
|
|
.expect(400);
|
|
});
|
|
});
|
|
|
|
describe("POST /auth/login", () => {
|
|
it("should login with valid credentials", () => {
|
|
return request(app.getHttpServer())
|
|
.post("/auth/login")
|
|
.send({
|
|
email: testUser.email,
|
|
password: testUser.password,
|
|
})
|
|
.expect(200)
|
|
.expect((res) => {
|
|
expect(res.body.success).toBe(true);
|
|
expect(res.body.user).toBeDefined();
|
|
expect(res.body.user.email).toBe(testUser.email);
|
|
|
|
// Should return session token
|
|
expect(res.body.sessionId).toBeDefined();
|
|
sessionToken = res.body.sessionId;
|
|
});
|
|
});
|
|
|
|
it("should reject invalid password", () => {
|
|
return request(app.getHttpServer())
|
|
.post("/auth/login")
|
|
.send({
|
|
email: testUser.email,
|
|
password: "WrongPassword123!",
|
|
})
|
|
.expect(401);
|
|
});
|
|
|
|
it("should reject non-existent user", () => {
|
|
return request(app.getHttpServer())
|
|
.post("/auth/login")
|
|
.send({
|
|
email: "nonexistent@example.com",
|
|
password: "AnyPassword123!",
|
|
})
|
|
.expect(401);
|
|
});
|
|
});
|
|
|
|
describe("GET /auth/validate", () => {
|
|
it("should validate authenticated session", () => {
|
|
return request(app.getHttpServer())
|
|
.get("/auth/validate")
|
|
.set("Authorization", `Bearer ${sessionToken}`)
|
|
.expect(200)
|
|
.expect((res) => {
|
|
expect(res.body.authenticated).toBe(true);
|
|
expect(res.body.user).toBeDefined();
|
|
expect(res.body.user.email).toBe(testUser.email);
|
|
});
|
|
});
|
|
|
|
it("should reject request without authorization header", () => {
|
|
return request(app.getHttpServer()).get("/auth/validate").expect(401);
|
|
});
|
|
|
|
it("should reject invalid session token", () => {
|
|
return request(app.getHttpServer())
|
|
.get("/auth/validate")
|
|
.set("Authorization", "Bearer invalid-session-id")
|
|
.expect(401);
|
|
});
|
|
});
|
|
|
|
describe("GET /auth/me", () => {
|
|
it("should return user data for authenticated session", () => {
|
|
return request(app.getHttpServer())
|
|
.get("/auth/me")
|
|
.set("Authorization", `Bearer ${sessionToken}`)
|
|
.expect(200)
|
|
.expect((res) => {
|
|
expect(res.body.authenticated).toBe(true);
|
|
expect(res.body.user).toBeDefined();
|
|
});
|
|
});
|
|
|
|
it("should return authenticated: false without authorization", () => {
|
|
return request(app.getHttpServer())
|
|
.get("/auth/me")
|
|
.expect(200)
|
|
.expect((res) => {
|
|
expect(res.body.authenticated).toBe(false);
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("POST /auth/refresh", () => {
|
|
it("should refresh valid session", () => {
|
|
return request(app.getHttpServer())
|
|
.post("/auth/refresh")
|
|
.set("Authorization", `Bearer ${sessionToken}`)
|
|
.expect(200)
|
|
.expect((res) => {
|
|
expect(res.body.success).toBe(true);
|
|
});
|
|
});
|
|
|
|
it("should reject refresh without authorization", () => {
|
|
return request(app.getHttpServer()).post("/auth/refresh").expect(401);
|
|
});
|
|
});
|
|
|
|
describe("POST /auth/logout", () => {
|
|
it("should logout and invalidate session", () => {
|
|
return request(app.getHttpServer())
|
|
.post("/auth/logout")
|
|
.set("Authorization", `Bearer ${sessionToken}`)
|
|
.expect(200)
|
|
.expect((res) => {
|
|
expect(res.body.success).toBe(true);
|
|
});
|
|
});
|
|
|
|
it("should handle logout without authorization gracefully", () => {
|
|
return request(app.getHttpServer())
|
|
.post("/auth/logout")
|
|
.expect(200)
|
|
.expect((res) => {
|
|
expect(res.body.success).toBe(true);
|
|
});
|
|
});
|
|
|
|
it("should invalidate session after logout", async () => {
|
|
// First, login to get a fresh session
|
|
const loginRes = await request(app.getHttpServer())
|
|
.post("/auth/login")
|
|
.send({
|
|
email: testUser.email,
|
|
password: testUser.password,
|
|
});
|
|
|
|
const freshToken = loginRes.body.sessionId;
|
|
|
|
// Logout
|
|
await request(app.getHttpServer())
|
|
.post("/auth/logout")
|
|
.set("Authorization", `Bearer ${freshToken}`)
|
|
.expect(200);
|
|
|
|
// Validate should now fail
|
|
return request(app.getHttpServer())
|
|
.get("/auth/validate")
|
|
.set("Authorization", `Bearer ${freshToken}`)
|
|
.expect(401);
|
|
});
|
|
});
|
|
});
|