import { test, before, after } from "node:test"; import assert from "node:assert/strict"; import pg from "pg"; const API_BASE = process.env.TEST_API_BASE ?? "http://localhost:8080"; const DATABASE_URL = process.env.DATABASE_URL; if (!DATABASE_URL) { throw new Error("DATABASE_URL must be set to run these tests"); } const TEST_PASSWORD = "TestPass123!"; const TEST_PASSWORD_HASH = "$2b$10$Bs636ukPMyz01nKrsi.5m.JlDXSN22AVCvn8cgPWWDbo5yJRQX2vu"; const pool = new pg.Pool({ connectionString: DATABASE_URL }); const createdUserIds = []; const createdUsernames = []; function uniqueName(prefix) { return `${prefix}_${Date.now().toString(36)}_${Math.random() .toString(36) .slice(2, 8)}`; } async function createUser(prefix) { const username = uniqueName(prefix); const { rows } = await pool.query( `INSERT INTO users (username, email, password_hash, display_name_en, preferred_language, is_active) VALUES ($1, $2, $3, 'Session Test', 'en', true) RETURNING id`, [username, `${username}@example.com`, TEST_PASSWORD_HASH], ); const id = rows[0].id; createdUserIds.push(id); createdUsernames.push(username); await pool.query( `INSERT INTO user_roles (user_id, role_id) SELECT $1, id FROM roles WHERE name = 'user'`, [id], ); return { id, username }; } function extractSidCookie(res) { const setCookie = res.headers.get("set-cookie"); if (!setCookie) return null; return setCookie .split(",") .map((c) => c.split(";")[0].trim()) .find((c) => c.startsWith("connect.sid=")) ?? null; } after(async () => { if (createdUserIds.length > 0) { await pool.query(`DELETE FROM user_roles WHERE user_id = ANY($1::int[])`, [ createdUserIds, ]); await pool.query(`DELETE FROM users WHERE id = ANY($1::int[])`, [ createdUserIds, ]); } // Also drop registration-created users (whose ids we don't know up front). if (createdUsernames.length > 0) { await pool.query( `DELETE FROM user_roles WHERE user_id IN (SELECT id FROM users WHERE username = ANY($1::text[]))`, [createdUsernames], ); await pool.query(`DELETE FROM users WHERE username = ANY($1::text[])`, [ createdUsernames, ]); } await pool.end(); }); test("login session is persisted before response so an immediate /auth/me succeeds", async () => { const user = await createUser("session_login"); const loginRes = await fetch(`${API_BASE}/api/auth/login`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ username: user.username, password: TEST_PASSWORD }), }); assert.equal(loginRes.status, 200, "login should succeed"); const cookie = extractSidCookie(loginRes); assert.ok(cookie, "login must set connect.sid cookie"); // Immediate follow-up authenticated request — must already see the session. const meRes = await fetch(`${API_BASE}/api/auth/me`, { headers: { Cookie: cookie }, }); assert.equal( meRes.status, 200, "fast follow-up /auth/me after login must be authenticated", ); const me = await meRes.json(); assert.equal(me.username, user.username); }); test("registration session is persisted before response so an immediate /auth/me succeeds", async () => { const username = uniqueName("session_register"); createdUsernames.push(username); const regRes = await fetch(`${API_BASE}/api/auth/register`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ username, email: `${username}@example.com`, password: TEST_PASSWORD, displayNameEn: "Session Register", preferredLanguage: "en", }), }); // Registration may be closed in some environments; if so, skip the assertion // about persistence — there is nothing to verify in that case. if (regRes.status === 403) { return; } assert.equal(regRes.status, 201, "register should succeed"); const cookie = extractSidCookie(regRes); assert.ok(cookie, "register must set connect.sid cookie"); const meRes = await fetch(`${API_BASE}/api/auth/me`, { headers: { Cookie: cookie }, }); assert.equal( meRes.status, 200, "fast follow-up /auth/me after register must be authenticated", ); }); test("logout destroys the session before response so an immediate /auth/me is rejected", async () => { const user = await createUser("session_logout"); const loginRes = await fetch(`${API_BASE}/api/auth/login`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ username: user.username, password: TEST_PASSWORD }), }); assert.equal(loginRes.status, 200); const cookie = extractSidCookie(loginRes); assert.ok(cookie); const logoutRes = await fetch(`${API_BASE}/api/auth/logout`, { method: "POST", headers: { Cookie: cookie }, }); assert.equal(logoutRes.status, 200, "logout should succeed"); // Immediate follow-up using the same (now-destroyed) sid must be unauthorized. const meRes = await fetch(`${API_BASE}/api/auth/me`, { headers: { Cookie: cookie }, }); assert.equal( meRes.status, 401, "fast follow-up /auth/me after logout must be unauthorized", ); });