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 STAMP = `${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 6)}`; let adminId; let adminCookie; let depUserId; let depAppId; let depServiceId; let depGroupId; let depPermissionId; let depConvId; 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 adminUsername = `dep_admin_${STAMP}`; const a = await pool.query( `INSERT INTO users (username, email, password_hash, display_name_en, preferred_language, is_active) VALUES ($1, $2, $3, 'Dep 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, 'Dep User', 'en', true) RETURNING id`, [`dep_user_${STAMP}`, `dep_user_${STAMP}@example.com`, TEST_PASSWORD_HASH], ); depUserId = u.rows[0].id; const app = await pool.query( `INSERT INTO apps (slug, name_ar, name_en, route, color, icon_name) VALUES ($1, $2, $3, '/dep', '#000', 'Box') RETURNING id`, [`dep-app-${STAMP}`, `DepApp_${STAMP}`, `DepApp_${STAMP}`], ); depAppId = app.rows[0].id; const svc = await pool.query( `INSERT INTO services (name_ar, name_en, description_ar, description_en) VALUES ($1, $2, 'desc', 'desc') RETURNING id`, [`DepSvc_${STAMP}`, `DepSvc_${STAMP}`], ); depServiceId = svc.rows[0].id; const grp = await pool.query( `INSERT INTO groups (name, description_en) VALUES ($1, 'dep grp') RETURNING id`, [`DepGrp_${STAMP}`], ); depGroupId = grp.rows[0].id; // Create a custom permission to attach as an app restriction. const perm = await pool.query( `INSERT INTO permissions (name, description_en) VALUES ($1, 'dep perm') RETURNING id`, [`dep_perm_${STAMP}`], ); depPermissionId = perm.rows[0].id; // Dependents on the user: a note, an order, a conversation, a message. await pool.query( `INSERT INTO notes (user_id, content) VALUES ($1, 'note for dep test')`, [depUserId], ); await pool.query( `INSERT INTO service_orders (service_id, user_id, status, notes) VALUES ($1, $2, 'pending', 'dep order')`, [depServiceId, depUserId], ); const conv = await pool.query( `INSERT INTO conversations (is_group, name_en, created_by) VALUES (true, 'dep conv', $1) RETURNING id`, [depUserId], ); depConvId = conv.rows[0].id; await pool.query( `INSERT INTO conversation_participants (conversation_id, user_id) VALUES ($1, $2)`, [depConvId, depUserId], ); await pool.query( `INSERT INTO messages (conversation_id, sender_id, content) VALUES ($1, $2, 'hi')`, [depConvId, depUserId], ); // App dependents: group link, permission restriction, an open record. await pool.query( `INSERT INTO group_apps (group_id, app_id) VALUES ($1, $2)`, [depGroupId, depAppId], ); await pool.query( `INSERT INTO app_permissions (app_id, permission_id) VALUES ($1, $2)`, [depAppId, depPermissionId], ); await pool.query( `INSERT INTO app_opens (app_id, user_id) VALUES ($1, $2)`, [depAppId, depUserId], ); adminCookie = await loginAndGetCookie(adminUsername, TEST_PASSWORD); }); after(async () => { await pool.query(`DELETE FROM messages WHERE sender_id = $1`, [depUserId]); await pool.query(`DELETE FROM conversation_participants WHERE user_id = $1`, [depUserId]); await pool.query(`DELETE FROM conversations WHERE id = $1`, [depConvId]); await pool.query(`DELETE FROM service_orders WHERE user_id = $1`, [depUserId]); await pool.query(`DELETE FROM notes WHERE user_id = $1`, [depUserId]); await pool.query(`DELETE FROM app_opens WHERE app_id = $1`, [depAppId]); await pool.query(`DELETE FROM app_permissions WHERE app_id = $1`, [depAppId]); await pool.query(`DELETE FROM group_apps WHERE app_id = $1`, [depAppId]); await pool.query(`DELETE FROM permissions WHERE id = $1`, [depPermissionId]); await pool.query(`DELETE FROM apps WHERE id = $1`, [depAppId]); await pool.query(`DELETE FROM services WHERE id = $1`, [depServiceId]); await pool.query(`DELETE FROM groups WHERE id = $1`, [depGroupId]); await pool.query(`DELETE FROM user_roles WHERE user_id = ANY($1::int[])`, [[adminId, depUserId]]); await pool.query(`DELETE FROM users WHERE id = ANY($1::int[])`, [[adminId, depUserId]]); await pool.end(); }); test("GET /api/admin/apps exposes groupCount/restrictionCount/openCount", async () => { const res = await fetch(`${API_BASE}/api/admin/apps`, { headers: { Cookie: adminCookie }, }); assert.equal(res.status, 200); const apps = await res.json(); const target = apps.find((a) => a.id === depAppId); assert.ok(target, "expected dep app in admin apps list"); assert.equal(target.groupCount, 1, "groupCount should be 1"); assert.equal(target.restrictionCount, 1, "restrictionCount should be 1"); assert.equal(target.openCount, 1, "openCount should be 1"); }); test("GET /api/services exposes orderCount", async () => { const res = await fetch(`${API_BASE}/api/services`, { headers: { Cookie: adminCookie }, }); assert.equal(res.status, 200); const services = await res.json(); const target = services.find((s) => s.id === depServiceId); assert.ok(target, "expected dep service in services list"); assert.equal(target.orderCount, 1, "orderCount should be 1"); }); test("GET /api/users exposes noteCount/orderCount/conversationCount/messageCount", async () => { const res = await fetch(`${API_BASE}/api/users`, { headers: { Cookie: adminCookie }, }); assert.equal(res.status, 200); const users = await res.json(); const target = users.find((u) => u.id === depUserId); assert.ok(target, "expected dep user in users list"); assert.equal(target.noteCount, 1, "noteCount should be 1"); assert.equal(target.orderCount, 1, "orderCount should be 1"); assert.equal(target.conversationCount, 1, "conversationCount should be 1"); assert.equal(target.messageCount, 1, "messageCount should be 1"); }); test("Apps list returns zero counts for entities without dependents", async () => { const cleanApp = await pool.query( `INSERT INTO apps (slug, name_ar, name_en, route, color, icon_name) VALUES ($1, $2, $3, '/clean', '#000', 'Box') RETURNING id`, [`clean-app-${STAMP}`, `CleanApp_${STAMP}`, `CleanApp_${STAMP}`], ); const cleanAppId = cleanApp.rows[0].id; try { const res = await fetch(`${API_BASE}/api/admin/apps`, { headers: { Cookie: adminCookie }, }); assert.equal(res.status, 200); const apps = await res.json(); const target = apps.find((a) => a.id === cleanAppId); assert.ok(target, "expected clean app in admin apps list"); assert.equal(target.groupCount, 0); assert.equal(target.restrictionCount, 0); assert.equal(target.openCount, 0); } finally { await pool.query(`DELETE FROM apps WHERE id = $1`, [cleanAppId]); } }); test("Services list returns zero orderCount for services without orders", async () => { const cleanSvc = await pool.query( `INSERT INTO services (name_ar, name_en, description_ar, description_en) VALUES ($1, $2, 'd', 'd') RETURNING id`, [`CleanSvc_${STAMP}`, `CleanSvc_${STAMP}`], ); const cleanSvcId = cleanSvc.rows[0].id; try { const res = await fetch(`${API_BASE}/api/services`, { headers: { Cookie: adminCookie }, }); assert.equal(res.status, 200); const services = await res.json(); const target = services.find((s) => s.id === cleanSvcId); assert.ok(target, "expected clean service in services list"); assert.equal(target.orderCount, 0); } finally { await pool.query(`DELETE FROM services WHERE id = $1`, [cleanSvcId]); } }); test("Users list returns zero counts for users without dependents", async () => { const cleanUser = await pool.query( `INSERT INTO users (username, email, password_hash, display_name_en, preferred_language, is_active) VALUES ($1, $2, $3, 'Clean User', 'en', true) RETURNING id`, [`clean_user_${STAMP}`, `clean_user_${STAMP}@example.com`, TEST_PASSWORD_HASH], ); const cleanUserId = cleanUser.rows[0].id; try { const res = await fetch(`${API_BASE}/api/users`, { headers: { Cookie: adminCookie }, }); assert.equal(res.status, 200); const users = await res.json(); const target = users.find((u) => u.id === cleanUserId); assert.ok(target, "expected clean user in users list"); assert.equal(target.noteCount, 0); assert.equal(target.orderCount, 0); assert.equal(target.conversationCount, 0); assert.equal(target.messageCount, 0); } finally { await pool.query(`DELETE FROM users WHERE id = $1`, [cleanUserId]); } });