// API-level coverage for the enriched audit_logs metadata that the group // sub-resource endpoints write. We assert that: // - group.user.add / group.user.remove rows record `username` // - group.app.add / group.app.remove rows record `appSlug`, `appNameEn`, // `appNameAr` // - group.role.add / group.role.remove rows record `roleName` // // The rows themselves are produced by POST /api/groups/:id/:kind/:targetId // and DELETE /api/groups/:id/:kind/:targetId in routes/groups.ts, and are // what powers the readable summary formatter on the admin audit log UI. 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 adminId; let adminUsername; let adminCookie; let memberId; let memberUsername; let appId; let appSlug; let appNameEn; let appNameAr; let roleId; let roleName; let groupId; async function loginAndGetCookie(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)}`; adminUsername = `gam_admin_${stamp}`; memberUsername = `gam_user_${stamp}`; const a = await pool.query( `INSERT INTO users (username, email, password_hash, display_name_en, preferred_language, is_active) VALUES ($1, $2, $3, 'Audit Admin', 'en', true) RETURNING id`, [adminUsername, `${adminUsername}@example.com`, TEST_PASSWORD_HASH], ); adminId = a.rows[0].id; await pool.query( `INSERT INTO user_roles (user_id, role_id) SELECT $1, id FROM roles WHERE name = 'admin'`, [adminId], ); const u = await pool.query( `INSERT INTO users (username, email, password_hash, display_name_en, preferred_language, is_active) VALUES ($1, $2, $3, 'Audit Member', 'en', true) RETURNING id`, [memberUsername, `${memberUsername}@example.com`, TEST_PASSWORD_HASH], ); memberId = u.rows[0].id; appSlug = `gam_app_${stamp}`; appNameEn = `GAM App ${stamp}`; appNameAr = `تطبيق ${stamp}`; const ap = await pool.query( `INSERT INTO apps (slug, name_en, name_ar, icon_name, route, color, sort_order) VALUES ($1, $2, $3, 'Box', '/gam', '#000000', 999) RETURNING id`, [appSlug, appNameEn, appNameAr], ); appId = ap.rows[0].id; roleName = `gam_role_${stamp}`; const r = await pool.query( `INSERT INTO roles (name) VALUES ($1) RETURNING id`, [roleName], ); roleId = r.rows[0].id; // Empty group so we can drive sub-resource add/remove operations cleanly. const g = await pool.query( `INSERT INTO groups (name) VALUES ($1) RETURNING id`, [`gam_grp_${stamp}`], ); groupId = g.rows[0].id; adminCookie = await loginAndGetCookie(adminUsername, TEST_PASSWORD); }); after(async () => { // Clean up audit rows the tests produced before tearing down their targets, // otherwise actor_user_id/target_id FKs (or noisy leftovers) can linger. if (groupId !== undefined) { await pool.query(`DELETE FROM audit_logs WHERE target_type = 'group' AND target_id = $1`, [groupId]); await pool.query(`DELETE FROM user_groups WHERE group_id = $1`, [groupId]); 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 groups WHERE id = $1`, [groupId]); } if (memberId !== undefined) { await pool.query(`DELETE FROM audit_logs WHERE target_type = 'user' AND target_id = $1`, [memberId]); await pool.query(`DELETE FROM user_groups WHERE user_id = $1`, [memberId]); await pool.query(`DELETE FROM user_roles WHERE user_id = $1`, [memberId]); await pool.query(`DELETE FROM users WHERE id = $1`, [memberId]); } if (appId !== undefined) { await pool.query(`DELETE FROM audit_logs WHERE target_type = 'app' AND target_id = $1`, [appId]); await pool.query(`DELETE FROM group_apps WHERE app_id = $1`, [appId]); await pool.query(`DELETE FROM apps WHERE id = $1`, [appId]); } if (roleId !== undefined) { await pool.query(`DELETE FROM audit_logs WHERE target_type = 'role' AND target_id = $1`, [roleId]); await pool.query(`DELETE FROM group_roles WHERE role_id = $1`, [roleId]); await pool.query(`DELETE FROM user_roles WHERE role_id = $1`, [roleId]); await pool.query(`DELETE FROM roles WHERE id = $1`, [roleId]); } if (adminId !== undefined) { await pool.query(`DELETE FROM audit_logs WHERE actor_user_id = $1`, [adminId]); await pool.query(`DELETE FROM user_roles WHERE user_id = $1`, [adminId]); await pool.query(`DELETE FROM users WHERE id = $1`, [adminId]); } await pool.end(); }); // Look up the most recent audit_logs row for a given (action, target_type, // target_id) tuple so each test can target the row it just produced. async function latestAuditRow(action, targetType, targetId) { const { rows } = await pool.query( `SELECT metadata FROM audit_logs WHERE action = $1 AND target_type = $2 AND target_id = $3 ORDER BY id DESC LIMIT 1`, [action, targetType, targetId], ); assert.equal(rows.length, 1, `expected one ${action} row for ${targetType}#${targetId}`); return rows[0].metadata; } test("group.user.add audit row records username (not just userId)", async () => { const res = await fetch( `${API_BASE}/api/groups/${groupId}/users/${memberId}`, { method: "POST", headers: { Cookie: adminCookie } }, ); assert.equal(res.status, 204); const meta = await latestAuditRow("group.user.add", "group", groupId); assert.equal(meta.userId, memberId); assert.equal(meta.username, memberUsername); assert.ok(typeof meta.groupName === "string" && meta.groupName.length > 0); }); test("group.user.remove audit row records username (not just userId)", async () => { const res = await fetch( `${API_BASE}/api/groups/${groupId}/users/${memberId}`, { method: "DELETE", headers: { Cookie: adminCookie } }, ); assert.equal(res.status, 204); const meta = await latestAuditRow("group.user.remove", "group", groupId); assert.equal(meta.userId, memberId); assert.equal(meta.username, memberUsername); }); test("group.app.add audit row records appSlug, appNameEn, appNameAr (not just appId)", async () => { const res = await fetch( `${API_BASE}/api/groups/${groupId}/apps/${appId}`, { method: "POST", headers: { Cookie: adminCookie } }, ); assert.equal(res.status, 204); const meta = await latestAuditRow("group.app.add", "group", groupId); assert.equal(meta.appId, appId); assert.equal(meta.appSlug, appSlug); assert.equal(meta.appNameEn, appNameEn); assert.equal(meta.appNameAr, appNameAr); }); test("group.app.remove audit row records appSlug, appNameEn, appNameAr (not just appId)", async () => { const res = await fetch( `${API_BASE}/api/groups/${groupId}/apps/${appId}`, { method: "DELETE", headers: { Cookie: adminCookie } }, ); assert.equal(res.status, 204); const meta = await latestAuditRow("group.app.remove", "group", groupId); assert.equal(meta.appId, appId); assert.equal(meta.appSlug, appSlug); assert.equal(meta.appNameEn, appNameEn); assert.equal(meta.appNameAr, appNameAr); }); test("group.role.add audit row records roleName (not just roleId)", async () => { const res = await fetch( `${API_BASE}/api/groups/${groupId}/roles/${roleId}`, { method: "POST", headers: { Cookie: adminCookie } }, ); assert.equal(res.status, 204); const meta = await latestAuditRow("group.role.add", "group", groupId); assert.equal(meta.roleId, roleId); assert.equal(meta.roleName, roleName); }); test("group.role.remove audit row records roleName (not just roleId)", async () => { const res = await fetch( `${API_BASE}/api/groups/${groupId}/roles/${roleId}`, { method: "DELETE", headers: { Cookie: adminCookie } }, ); assert.equal(res.status, 204); const meta = await latestAuditRow("group.role.remove", "group", groupId); assert.equal(meta.roleId, roleId); assert.equal(meta.roleName, roleName); });