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 }); let groupAdminUsername; let memberUsername; let outsiderUsername; let groupAdminId; let memberId; let outsiderId; let groupAdminCookie; let memberCookie; let outsiderCookie; let groupId; let restrictedAppId; let unrestrictedAppId; let permissionId; 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"); return setCookie .split(",") .map((c) => c.split(";")[0].trim()) .find((c) => c.startsWith("connect.sid=")); } before(async () => { const stamp = `${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 6)}`; groupAdminUsername = `gv_gadmin_${stamp}`; memberUsername = `gv_member_${stamp}`; outsiderUsername = `gv_out_${stamp}`; // Create three users without any direct admin role. const ga = await pool.query( `INSERT INTO users (username, email, password_hash, display_name_en, preferred_language, is_active) VALUES ($1, $2, $3, 'Group Admin', 'en', true) RETURNING id`, [groupAdminUsername, `${groupAdminUsername}@ex.com`, TEST_PASSWORD_HASH], ); groupAdminId = ga.rows[0].id; const m = await pool.query( `INSERT INTO users (username, email, password_hash, display_name_en, preferred_language, is_active) VALUES ($1, $2, $3, 'Member', 'en', true) RETURNING id`, [memberUsername, `${memberUsername}@ex.com`, TEST_PASSWORD_HASH], ); memberId = m.rows[0].id; const o = await pool.query( `INSERT INTO users (username, email, password_hash, display_name_en, preferred_language, is_active) VALUES ($1, $2, $3, 'Outsider', 'en', true) RETURNING id`, [outsiderUsername, `${outsiderUsername}@ex.com`, TEST_PASSWORD_HASH], ); outsiderId = o.rows[0].id; // Give member + outsider the basic 'user' role so they can list apps. await pool.query( `INSERT INTO user_roles (user_id, role_id) SELECT u.id, r.id FROM users u, roles r WHERE u.id IN ($1, $2, $3) AND r.name = 'user'`, [groupAdminId, memberId, outsiderId], ); // Create a fresh group, attach 'admin' role to it (group-derived admin). const grp = await pool.query( `INSERT INTO groups (name, description_en) VALUES ($1, $2) RETURNING id`, [`vis_group_${stamp}`, "visibility test group"], ); groupId = grp.rows[0].id; // group-derived admin → groupAdmin user await pool.query( `INSERT INTO group_roles (group_id, role_id) SELECT $1, id FROM roles WHERE name = 'admin'`, [groupId], ); await pool.query( `INSERT INTO user_groups (user_id, group_id) VALUES ($1, $2)`, [groupAdminId, groupId], ); // Create an unrestricted app (no permissions attached). const unr = await pool.query( `INSERT INTO apps (name_en, name_ar, slug, icon_name, route, color, sort_order, is_active) VALUES ($1, $1, $2, 'Grid', '/test-unr', '#6366f1', 999, true) RETURNING id`, [`UnrestrictedApp_${stamp}`, `unr_${stamp}`], ); unrestrictedAppId = unr.rows[0].id; // Create a restricted app gated by a fresh permission. const rest = await pool.query( `INSERT INTO apps (name_en, name_ar, slug, icon_name, route, color, sort_order, is_active) VALUES ($1, $1, $2, 'Grid', '/test-rest', '#6366f1', 998, true) RETURNING id`, [`RestrictedApp_${stamp}`, `rest_${stamp}`], ); restrictedAppId = rest.rows[0].id; const perm = await pool.query( `INSERT INTO permissions (name, description_en) VALUES ($1, $2) RETURNING id`, [`vis_perm_${stamp}`, "visibility test perm"], ); permissionId = perm.rows[0].id; await pool.query( `INSERT INTO app_permissions (app_id, permission_id) VALUES ($1, $2)`, [restrictedAppId, permissionId], ); // Grant the restricted app to the group via group_apps → only group members see it. await pool.query( `INSERT INTO group_apps (group_id, app_id) VALUES ($1, $2)`, [groupId, restrictedAppId], ); // Add 'member' user to the group too (so they should see the restricted app). await pool.query( `INSERT INTO user_groups (user_id, group_id) VALUES ($1, $2)`, [memberId, groupId], ); groupAdminCookie = await login(groupAdminUsername, TEST_PASSWORD); memberCookie = await login(memberUsername, TEST_PASSWORD); outsiderCookie = await login(outsiderUsername, TEST_PASSWORD); }); after(async () => { await pool.query(`DELETE FROM group_apps WHERE group_id = $1`, [groupId]); await pool.query(`DELETE FROM group_roles WHERE group_id = $1`, [groupId]); await pool.query( `DELETE FROM user_groups WHERE user_id = ANY($1::int[]) OR group_id = $2`, [[groupAdminId, memberId, outsiderId], groupId], ); await pool.query( `DELETE FROM user_roles WHERE user_id = ANY($1::int[])`, [[groupAdminId, memberId, outsiderId]], ); await pool.query(`DELETE FROM groups WHERE id = $1`, [groupId]); await pool.query( `DELETE FROM app_permissions WHERE app_id = ANY($1::int[])`, [[restrictedAppId, unrestrictedAppId]], ); await pool.query(`DELETE FROM permissions WHERE id = $1`, [permissionId]); await pool.query( `DELETE FROM apps WHERE id = ANY($1::int[])`, [[restrictedAppId, unrestrictedAppId]], ); await pool.query(`DELETE FROM users WHERE id = ANY($1::int[])`, [ [groupAdminId, memberId, outsiderId], ]); await pool.end(); }); async function listApps(cookie) { const res = await fetch(`${API_BASE}/api/apps`, { headers: { cookie }, }); assert.equal(res.status, 200); return res.json(); } test("group-derived admin sees both restricted and unrestricted apps", async () => { const apps = await listApps(groupAdminCookie); const ids = apps.map((a) => a.id); assert.ok(ids.includes(unrestrictedAppId), "admin should see unrestricted app"); assert.ok(ids.includes(restrictedAppId), "admin should see restricted app"); }); test("group member sees the group-granted restricted app and the unrestricted one", async () => { const apps = await listApps(memberCookie); const ids = apps.map((a) => a.id); assert.ok(ids.includes(unrestrictedAppId), "member should see unrestricted app"); assert.ok( ids.includes(restrictedAppId), "member should see restricted app via group_apps", ); }); test("non-member outsider sees unrestricted but NOT the restricted app", async () => { const apps = await listApps(outsiderCookie); const ids = apps.map((a) => a.id); assert.ok(ids.includes(unrestrictedAppId), "outsider should see unrestricted app"); assert.ok( !ids.includes(restrictedAppId), "outsider must NOT see restricted app (no permission, no group)", ); });