// __tests__/integration/auth.test.js const request = require("supertest"); const app = require("../../../server"); // Adjust path to your Express app const { pool, clearAllTables } = require("../../setup/testDbUtils"); // Adjust path const User = require("../../../src/models/User"); // Adjust path describe("Auth API Endpoints", () => { let server; beforeAll(() => { // If your app directly listens, you might not need this. // If app is just exported, supertest handles starting/stopping. // server = app.listen(process.env.PORT || 3001); // Use test port }); afterAll(async () => { // if (server) server.close(); // await pool.end(); // Already handled by global teardown }); beforeEach(async () => { await clearAllTables(); }); describe("POST /api/auth/register", () => { it("should register a new user successfully", async () => { const res = await request(app).post("/api/auth/register").send({ email: "newuser@example.com", password: "Password123!", first_name: "New", last_name: "User", }); expect(res.statusCode).toEqual(201); expect(res.body.success).toBe(true); expect(res.body.data.user.email).toBe("newuser@example.com"); const dbUser = await User.findByEmail("newuser@example.com"); expect(dbUser).toBeDefined(); }); it("should return 409 if email already exists", async () => { await User.create({ email: "existing@example.com", password: "Password123!", }); const res = await request(app) .post("/api/auth/register") .send({ email: "existing@example.com", password: "Password123!" }); expect(res.statusCode).toEqual(409); expect(res.body.message).toContain("already exists"); }); // ... more registration tests (validation, etc.) }); describe("POST /api/auth/login", () => { let testUser; beforeEach(async () => { testUser = await User.create({ email: "login@example.com", password: "Password123!", is_verified: 1, // Mark as verified for login }); }); it("should login an existing verified user and return tokens", async () => { const res = await request(app) .post("/api/auth/login") .send({ email: "login@example.com", password: "Password123!" }); expect(res.statusCode).toEqual(200); expect(res.body.success).toBe(true); expect(res.body.data.accessToken).toBeDefined(); expect(res.body.data.refreshToken).toBeDefined(); expect(res.body.data.user.email).toBe("login@example.com"); }); it("should return 401 for invalid credentials", async () => { const res = await request(app) .post("/api/auth/login") .send({ email: "login@example.com", password: "WrongPassword!" }); expect(res.statusCode).toEqual(401); }); // ... more login tests (unverified, locked, must_change_password for super_admin) // Example for super_admin must_change_password it("should return 403 with MUST_CHANGE_PASSWORD for super_admin first login", async () => { // Ensure the default super_admin exists with must_change_password = TRUE // and password_hash = 'NEEDS_TO_BE_SET_ON_FIRST_LOGIN' // This requires the special handling in LocalStrategy as discussed. // For this test, you might need to manually insert/update the super_admin in testDb. await pool.query( `INSERT INTO users (email, password_hash, role, is_verified, is_active, must_change_password, uuid) VALUES ($1, $2, 'super_admin', TRUE, TRUE, TRUE, $3) ON CONFLICT (email) DO UPDATE SET password_hash = $2, must_change_password = TRUE`, [ "admin@formies.local", "NEEDS_TO_BE_SET_ON_FIRST_LOGIN", require("uuid").v4(), ] ); // This also assumes your special login logic for this specific hash exists const res = await request(app) .post("/api/auth/login") .send({ email: "admin@formies.local", password: "anypassword" }); // Password might be ignored by special logic if ( res.statusCode === 200 && res.body?.data?.user?.must_change_password ) { // This means your special login logic works by issuing a token even if bcrypt would fail, // and your /login route has a check for user.must_change_password AFTER successful auth by passport. // The client would then be responsible for triggering the force-change-password flow. // This is one way to handle it. console.warn( "Super admin login with must_change_password=true returned 200, client must handle redirection to force password change." ); } else { // The ideal case from previous discussion: // expect(res.statusCode).toEqual(403); // expect(res.body.success).toBe(false); // expect(res.body.code).toBe('MUST_CHANGE_PASSWORD'); // expect(res.body.data.user.email).toBe('admin@formies.local'); // For now, let's check for either the 403, or the 200 with the flag, as implementation details may vary slightly. expect([200, 403]).toContain(res.statusCode); if (res.statusCode === 200) expect(res.body.data.user.must_change_password).toBe(1); // or true if (res.statusCode === 403) expect(res.body.code).toBe("MUST_CHANGE_PASSWORD"); } }); }); describe("POST /api/auth/force-change-password", () => { let superAdminToken; beforeEach(async () => { // Simulate super admin login that requires password change await pool.query( `INSERT INTO users (id, email, password_hash, role, is_verified, is_active, must_change_password, uuid) VALUES (999, $1, $2, 'super_admin', TRUE, TRUE, TRUE, $3) ON CONFLICT (email) DO UPDATE SET password_hash = $2, must_change_password = TRUE`, [ "admin@formies.local", "NEEDS_TO_BE_SET_ON_FIRST_LOGIN", require("uuid").v4(), ] ); // This part is tricky: how do you get a token if login itself is blocked? // Option 1: Special login route for first-time setup (not implemented). // Option 2: Modify LocalStrategy to issue a temporary token for this specific case. // Option 3: Assume `must_change_password` doesn't block login fully but returns a flag, // and a normal token is issued, which is then used here. // Let's assume Option 3 for this test, where login provides a token. const loginRes = await request(app) .post("/api/auth/login") .send({ email: "admin@formies.local", password: "anypassword" }); // Password will be bypassed by special logic if (loginRes.body.data && loginRes.body.data.accessToken) { superAdminToken = loginRes.body.data.accessToken; } else { // If login directly returns 403 for MUST_CHANGE_PASSWORD, then this test needs rethinking. // It implies the client makes this call *without* a token initially, which is unusual for a POST. // Or, the client gets some other form of temporary credential. // For now, this test assumes a token is acquired. console.warn( "Could not get token for superAdmin requiring password change. /force-change-password test may be invalid." ); } }); it("should allow super_admin to change password if must_change_password is true", async () => { if (!superAdminToken) { console.warn("Skipping force-change-password test: no superAdminToken"); return; // or expect(superAdminToken).toBeDefined(); to fail if setup is wrong } const res = await request(app) .post("/api/auth/force-change-password") .set("Authorization", `Bearer ${superAdminToken}`) .send({ newPassword: "NewSecurePassword123!" }); expect(res.statusCode).toEqual(200); expect(res.body.success).toBe(true); expect(res.body.message).toContain("Password changed successfully"); const dbUser = await User.findByEmail("admin@formies.local"); expect(dbUser.must_change_password).toBe(0); // Or FALSE const isMatch = await require("bcryptjs").compare( "NewSecurePassword123!", dbUser.password_hash ); expect(isMatch).toBe(true); }); }); // ... tests for /refresh, /logout, /verify-email, /forgot-password, /reset-password, /profile etc. });