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 }); // Per-run uniqueness: every row this test creates is tagged with this stamp // so the after-hook can clean up (and a defensive sweep can mop up any // leftovers from a prior aborted run). const STAMP = `${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 6)}`; const USERNAME_PREFIX = `emvis_${STAMP}_`; let receiverUserId; let receiverUsername; let receiverCookie; async function login(username, password) { const res = await fetch(`${API_BASE}/api/auth/login`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ username, password }), }); assert.equal(res.status, 200, `login expected 200, got ${res.status}`); const setCookie = res.headers.get("set-cookie"); assert.ok(setCookie, "login response missing set-cookie header"); const cookie = setCookie .split(",") .map((c) => c.split(";")[0].trim()) .find((c) => c.startsWith("connect.sid=")); assert.ok(cookie, "no connect.sid cookie returned by login"); return cookie; } async function listApps(cookie) { const res = await fetch(`${API_BASE}/api/apps`, { headers: { cookie }, }); assert.equal(res.status, 200, `apps expected 200, got ${res.status}`); return res.json(); } before(async () => { // Defensive sweep: only remove leftovers from this exact run's stamp, // which is fresh per process. Avoids touching unrelated fixtures that // happen to share the `emvis_` family in a shared test DB. await pool.query( `DELETE FROM users WHERE username LIKE $1`, [`${USERNAME_PREFIX}%`], ); receiverUsername = `${USERNAME_PREFIX}receiver`; const u = await pool.query( `INSERT INTO users (username, email, password_hash, display_name_en, preferred_language, is_active) VALUES ($1, $2, $3, 'EM Visibility Test', 'en', true) RETURNING id`, [receiverUsername, `${receiverUsername}@ex.com`, TEST_PASSWORD_HASH], ); receiverUserId = u.rows[0].id; // Give them only the 'order_receiver' role — explicitly NOT any // executive_* role. They should also not be in any admin group. await pool.query( `INSERT INTO user_roles (user_id, role_id) SELECT $1, id FROM roles WHERE name = 'order_receiver'`, [receiverUserId], ); receiverCookie = await login(receiverUsername, TEST_PASSWORD); }); after(async () => { if (receiverUserId) { await pool.query( `DELETE FROM user_roles WHERE user_id = $1`, [receiverUserId], ); await pool.query( `DELETE FROM user_groups WHERE user_id = $1`, [receiverUserId], ); await pool.query(`DELETE FROM users WHERE id = $1`, [receiverUserId]); } await pool.end(); }); test("user with only order_receiver role does NOT see the executive-meetings app", async () => { const apps = await listApps(receiverCookie); const slugs = apps.map((a) => a.slug); assert.ok( !slugs.includes("executive-meetings"), `expected executive-meetings NOT in apps for non-executive user; got slugs: ${slugs.join(", ")}`, ); }); test("after granting executive_coordinator, the same user DOES see executive-meetings", async () => { await pool.query( `INSERT INTO user_roles (user_id, role_id) SELECT $1, id FROM roles WHERE name = 'executive_coordinator' ON CONFLICT DO NOTHING`, [receiverUserId], ); // Re-login so the new role is reflected in the session's permission cache // (some setups cache derived permissions on the session object). receiverCookie = await login(receiverUsername, TEST_PASSWORD); const apps = await listApps(receiverCookie); const slugs = apps.map((a) => a.slug); assert.ok( slugs.includes("executive-meetings"), `expected executive-meetings in apps after granting executive_coordinator; got slugs: ${slugs.join(", ")}`, ); });