Files
TX/artifacts/api-server/tests/audit-log-coverage.test.mjs
T
riyadhafraa 84398de390 Task #511: Fully remove Chat feature
Destructive removal per user confirmation ("حذف نهائي ما يرجع").

Removed:
- API: routes/conversations.ts, schema/conversations.ts, all chat
  socket handlers in src/index.ts, /admin/users/:id/dependents/
  conversations+messages endpoints, conversation/message dependency
  counts in users/stats routes.
- Web: pages/chat.tsx, /chat route, dock chat filter, MessageSquare
  icon and messages StatCard on home, all chat-related UI in
  notifications + admin (dependency badges, delete-dialog rows,
  UserDependentConversations/Messages sections, count map keys).
- Locales: nav.chat, home.stats.messages, full chat.* block,
  admin.deleteUser conv/msgCount, admin.users.counts.conv/msg,
  admin.audit.unit.conversation_*/message_*, admin.dependents.user*.
- OpenAPI spec: tags, all /conversations/* paths, conv/msg dependent
  paths, related schemas (ConversationWithDetails, MessageWithSender,
  UserDependentConversation/MessageItem+Page, etc.), UserProfile and
  UserDeletionConflict conv/msg fields, HomeStats.unreadMessages.
  Regenerated client via orval.
- Database: dropped message_reads, messages,
  conversation_participants, conversations (CASCADE); deleted
  notifications with related_type='conversation' or type='chat';
  deleted apps row with slug='chat'; ran drizzle push-force.
- Seed: removed chat:access permission + user-role assignment +
  seeded chat app entry from scripts/src/seed.ts.
- Tests: deleted conversations-leave.test.mjs; cleaned chat refs from
  list-dependency-counts, delete-force-warnings, audit-log-coverage,
  and admin-inline-dependency-counts (e2e) — replaced chat dependents
  with note dependents where needed for force-delete coverage.

Notes preserved: notes.tsx noConversationsYet/conversationWith refer
to NOTE THREADS (not chat) and were intentionally NOT touched.

executive-meetings.ts not modified per replit.md restriction.
Pre-existing flaky test failures in executive-meetings/group/etc
suites remain unrelated to this task.
2026-05-12 10:51:31 +00:00

1124 lines
36 KiB
JavaScript

// Verifies that every sensitive admin action that the API claims to audit
// actually writes the expected `audit_logs` row. Each test exercises a
// single endpoint, then queries the table and asserts:
// - exactly one matching row exists
// - action, actor_user_id, target_type, target_id are correct
// - the metadata blob has the documented shape
//
// The suite owns its own admin user, so `actor_user_id = adminId` is enough
// to tell our rows apart from other rows produced by parallel suites.
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 adminUsername;
let adminCookie;
const createdUserIds = [];
const createdGroupIds = [];
const createdRoleIds = [];
const createdAppIds = [];
const createdServiceIds = [];
let originalSettings = null;
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="));
}
function uniqueName(prefix) {
return `${prefix}_${STAMP}_${Math.random().toString(36).slice(2, 8)}`;
}
async function insertUser(prefix) {
const username = uniqueName(prefix).toLowerCase();
const r = await pool.query(
`INSERT INTO users (username, email, password_hash, display_name_en, preferred_language, is_active)
VALUES ($1, $2, $3, 'Audit Coverage', 'en', true) RETURNING id`,
[username, `${username}@example.com`, TEST_PASSWORD_HASH],
);
const id = r.rows[0].id;
createdUserIds.push(id);
// Always membership in Everyone so the seed-default trigger / FK behavior matches prod.
await pool.query(
`INSERT INTO user_groups (user_id, group_id)
SELECT $1, id FROM groups WHERE name = 'Everyone'
ON CONFLICT DO NOTHING`,
[id],
);
return { id, username };
}
async function insertApp(slugPrefix) {
const slug = uniqueName(slugPrefix).toLowerCase();
const r = await pool.query(
`INSERT INTO apps (slug, name_ar, name_en, route, sort_order)
VALUES ($1, 'تطبيق تدقيق', 'Audit App', '/x', 999) RETURNING id`,
[slug],
);
const id = r.rows[0].id;
createdAppIds.push(id);
return { id, slug };
}
async function insertGroup(prefix) {
const name = uniqueName(prefix);
const r = await pool.query(
`INSERT INTO groups (name, description_en) VALUES ($1, 'audit coverage group') RETURNING id`,
[name],
);
const id = r.rows[0].id;
createdGroupIds.push(id);
return { id, name };
}
async function insertService(prefix) {
const nameEn = uniqueName(prefix);
const nameAr = `${nameEn}_ar`;
const r = await pool.query(
`INSERT INTO services (name_ar, name_en, price, is_available)
VALUES ($1, $2, 1.00, true) RETURNING id`,
[nameAr, nameEn],
);
const id = r.rows[0].id;
createdServiceIds.push(id);
return { id, nameEn, nameAr };
}
async function insertRole(prefix) {
const name = uniqueName(prefix).toLowerCase();
const r = await pool.query(
`INSERT INTO roles (name, description_en) VALUES ($1, 'audit coverage role') RETURNING id`,
[name],
);
const id = r.rows[0].id;
createdRoleIds.push(id);
return { id, name };
}
async function fetchAuditRows({ action, targetId, targetType }) {
const rows = await pool.query(
`SELECT id, actor_user_id, action, target_type, target_id, metadata, created_at
FROM audit_logs
WHERE actor_user_id = $1
AND action = $2
AND target_id = $3
AND target_type = $4
ORDER BY id`,
[adminId, action, targetId, targetType],
);
return rows.rows;
}
before(async () => {
adminUsername = `audit_cov_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, 'Audit Coverage Admin', 'en', true) RETURNING id`,
[adminUsername, `${adminUsername}@example.com`, TEST_PASSWORD_HASH],
);
adminId = a.rows[0].id;
createdUserIds.push(adminId);
await pool.query(
`INSERT INTO user_roles (user_id, role_id) SELECT $1, id FROM roles WHERE name = 'admin'`,
[adminId],
);
await pool.query(
`INSERT INTO user_groups (user_id, group_id)
SELECT $1, id FROM groups WHERE name IN ('Everyone', 'Admins')
ON CONFLICT DO NOTHING`,
[adminId],
);
// Capture the current settings row so we can restore exactly what was
// there before this suite mutated it (settings.update is global state,
// shared with the rest of the app).
const s = await pool.query(`SELECT * FROM app_settings WHERE id = 1`);
if (s.rowCount === 1) {
originalSettings = s.rows[0];
}
adminCookie = await loginAndGetCookie(adminUsername, TEST_PASSWORD);
});
after(async () => {
// Wipe audit rows we created so they don't contaminate the global table.
await pool.query(`DELETE FROM audit_logs WHERE actor_user_id = $1`, [
adminId,
]);
if (createdRoleIds.length) {
await pool.query(`DELETE FROM user_roles WHERE role_id = ANY($1::int[])`, [
createdRoleIds,
]);
await pool.query(`DELETE FROM group_roles WHERE role_id = ANY($1::int[])`, [
createdRoleIds,
]);
await pool.query(
`DELETE FROM role_permissions WHERE role_id = ANY($1::int[])`,
[createdRoleIds],
);
await pool.query(
`DELETE FROM role_permission_audit WHERE role_id = ANY($1::int[])`,
[createdRoleIds],
);
await pool.query(`DELETE FROM roles WHERE id = ANY($1::int[])`, [
createdRoleIds,
]);
}
if (createdGroupIds.length) {
await pool.query(`DELETE FROM user_groups WHERE group_id = ANY($1::int[])`, [
createdGroupIds,
]);
await pool.query(`DELETE FROM group_apps WHERE group_id = ANY($1::int[])`, [
createdGroupIds,
]);
await pool.query(`DELETE FROM group_roles WHERE group_id = ANY($1::int[])`, [
createdGroupIds,
]);
await pool.query(`DELETE FROM groups WHERE id = ANY($1::int[])`, [
createdGroupIds,
]);
}
if (createdServiceIds.length) {
await pool.query(
`DELETE FROM service_orders WHERE service_id = ANY($1::int[])`,
[createdServiceIds],
);
await pool.query(`DELETE FROM services WHERE id = ANY($1::int[])`, [
createdServiceIds,
]);
}
if (createdAppIds.length) {
await pool.query(`DELETE FROM app_opens WHERE app_id = ANY($1::int[])`, [
createdAppIds,
]);
await pool.query(
`DELETE FROM app_permissions WHERE app_id = ANY($1::int[])`,
[createdAppIds],
);
await pool.query(`DELETE FROM group_apps WHERE app_id = ANY($1::int[])`, [
createdAppIds,
]);
await pool.query(
`DELETE FROM user_app_orders WHERE app_id = ANY($1::int[])`,
[createdAppIds],
);
await pool.query(`DELETE FROM apps WHERE id = ANY($1::int[])`, [
createdAppIds,
]);
}
for (const uid of createdUserIds) {
if (uid !== undefined) {
await pool.query(`DELETE FROM password_reset_tokens WHERE user_id = $1`, [
uid,
]);
await pool.query(`DELETE FROM user_groups WHERE user_id = $1`, [uid]);
await pool.query(`DELETE FROM user_roles WHERE user_id = $1`, [uid]);
await pool.query(`DELETE FROM users WHERE id = $1`, [uid]);
}
}
// Restore settings exactly as captured (clears any change rows we wrote).
if (originalSettings) {
await pool.query(
`UPDATE app_settings
SET site_name_ar = $1,
site_name_en = $2,
registration_open = $3,
footer_text_ar = $4,
footer_text_en = $5
WHERE id = 1`,
[
originalSettings.site_name_ar,
originalSettings.site_name_en,
originalSettings.registration_open,
originalSettings.footer_text_ar,
originalSettings.footer_text_en,
],
);
}
await pool.end();
});
// ---------- users ----------
test("DELETE /api/users/:id (no-force, no deps) writes one user.delete row with force=false", async () => {
const { id, username } = await insertUser("u_nf");
const email = `${username}@example.com`;
const res = await fetch(`${API_BASE}/api/users/${id}`, {
method: "DELETE",
headers: { Cookie: adminCookie },
});
assert.equal(res.status, 204);
const rows = await fetchAuditRows({
action: "user.delete",
targetId: id,
targetType: "user",
});
assert.equal(rows.length, 1, "expected exactly one user.delete audit row");
const row = rows[0];
assert.equal(row.actor_user_id, adminId);
assert.equal(row.metadata.username, username);
assert.equal(row.metadata.email, email);
assert.equal(row.metadata.displayNameEn, "Audit Coverage");
assert.equal(row.metadata.displayNameAr, null);
assert.equal(row.metadata.force, false);
// No-deps path must NOT include the dependency counts.
assert.equal(row.metadata.noteCount, undefined);
assert.equal(row.metadata.orderCount, undefined);
});
test("DELETE /api/users/:id without force on a user with deps returns 409 and writes NO audit row", async () => {
const { id } = await insertUser("u_409");
// Give the user a dependency that blocks delete.
await pool.query(
`INSERT INTO notes (user_id, title, content) VALUES ($1, 'n', 'x')`,
[id],
);
const res = await fetch(`${API_BASE}/api/users/${id}`, {
method: "DELETE",
headers: { Cookie: adminCookie },
});
assert.equal(res.status, 409);
const stillThere = await pool.query(`SELECT id FROM users WHERE id = $1`, [
id,
]);
assert.equal(stillThere.rowCount, 1, "user must still exist after 409");
const rows = await fetchAuditRows({
action: "user.delete",
targetId: id,
targetType: "user",
});
assert.equal(
rows.length,
0,
"denied delete (409) must NOT emit a user.delete audit row",
);
});
test("DELETE /api/users/:id?force=true writes one user.delete row with force=true and dep counts", async () => {
const { id, username } = await insertUser("u_f");
// Add a dependency so the force branch is taken meaningfully.
await pool.query(
`INSERT INTO notes (user_id, title, content) VALUES ($1, 'n', 'x')`,
[id],
);
const res = await fetch(`${API_BASE}/api/users/${id}?force=true`, {
method: "DELETE",
headers: { Cookie: adminCookie },
});
assert.equal(res.status, 204);
const rows = await fetchAuditRows({
action: "user.delete",
targetId: id,
targetType: "user",
});
assert.equal(rows.length, 1);
const row = rows[0];
assert.equal(row.actor_user_id, adminId);
assert.equal(row.metadata.username, username);
assert.equal(row.metadata.force, true);
// Force-with-deps path records the counts.
assert.equal(typeof row.metadata.noteCount, "number");
assert.ok(row.metadata.noteCount >= 1);
});
// ---------- roles ----------
test("POST /api/roles writes one role.create row", async () => {
const name = uniqueName("aud_role_c").toLowerCase();
const res = await fetch(`${API_BASE}/api/roles`, {
method: "POST",
headers: { "Content-Type": "application/json", Cookie: adminCookie },
body: JSON.stringify({
name,
descriptionEn: "audit role create",
descriptionAr: "تدقيق إنشاء الدور",
}),
});
assert.equal(res.status, 201);
const created = await res.json();
createdRoleIds.push(created.id);
const rows = await fetchAuditRows({
action: "role.create",
targetId: created.id,
targetType: "role",
});
assert.equal(rows.length, 1);
assert.equal(rows[0].actor_user_id, adminId);
assert.equal(rows[0].metadata.name, name);
assert.equal(rows[0].metadata.descriptionEn, "audit role create");
assert.equal(rows[0].metadata.descriptionAr, "تدقيق إنشاء الدور");
});
test("PATCH /api/roles/:id (rename) writes one role.update row marked renamed", async () => {
const role = await insertRole("aud_role_u");
const newName = uniqueName("aud_role_u_new").toLowerCase();
const res = await fetch(`${API_BASE}/api/roles/${role.id}`, {
method: "PATCH",
headers: { "Content-Type": "application/json", Cookie: adminCookie },
body: JSON.stringify({ name: newName, descriptionEn: "renamed" }),
});
assert.equal(res.status, 200);
const rows = await fetchAuditRows({
action: "role.update",
targetId: role.id,
targetType: "role",
});
assert.equal(rows.length, 1);
const row = rows[0];
assert.equal(row.actor_user_id, adminId);
assert.equal(row.metadata.previousName, role.name);
assert.equal(row.metadata.name, newName);
assert.equal(row.metadata.renamed, true);
assert.equal(row.metadata.renamedFrom, role.name);
assert.equal(row.metadata.renamedTo, newName);
assert.ok(row.metadata.changes, "metadata.changes must be present");
assert.equal(row.metadata.changes.name, newName);
assert.equal(row.metadata.changes.descriptionEn, "renamed");
});
test("PATCH /api/roles/:id with no effective changes does NOT write an audit row", async () => {
const role = await insertRole("aud_role_noop");
const res = await fetch(`${API_BASE}/api/roles/${role.id}`, {
method: "PATCH",
headers: { "Content-Type": "application/json", Cookie: adminCookie },
body: JSON.stringify({ name: role.name }),
});
assert.equal(res.status, 200);
const rows = await fetchAuditRows({
action: "role.update",
targetId: role.id,
targetType: "role",
});
assert.equal(rows.length, 0, "no-op patch must not emit role.update");
});
test("DELETE /api/roles/:id (no users/groups) writes one role.delete row", async () => {
const role = await insertRole("aud_role_d");
const res = await fetch(`${API_BASE}/api/roles/${role.id}`, {
method: "DELETE",
headers: { Cookie: adminCookie },
});
assert.equal(res.status, 204);
const rows = await fetchAuditRows({
action: "role.delete",
targetId: role.id,
targetType: "role",
});
assert.equal(rows.length, 1);
assert.equal(rows[0].actor_user_id, adminId);
assert.equal(rows[0].metadata.roleName, role.name);
});
// ---------- groups ----------
test("POST /api/groups writes one group.create row with size counts", async () => {
const name = uniqueName("aud_grp_c");
const member = await insertUser("aud_grp_member");
const res = await fetch(`${API_BASE}/api/groups`, {
method: "POST",
headers: { "Content-Type": "application/json", Cookie: adminCookie },
body: JSON.stringify({
name,
descriptionEn: "audit group create",
userIds: [member.id],
appIds: [],
roleIds: [],
}),
});
assert.equal(res.status, 201);
const created = await res.json();
createdGroupIds.push(created.id);
const rows = await fetchAuditRows({
action: "group.create",
targetId: created.id,
targetType: "group",
});
assert.equal(rows.length, 1);
assert.equal(rows[0].actor_user_id, adminId);
assert.equal(rows[0].metadata.name, name);
assert.equal(rows[0].metadata.memberCount, 1);
assert.equal(rows[0].metadata.appCount, 0);
assert.equal(rows[0].metadata.roleCount, 0);
});
test("PATCH /api/groups/:id (members + apps + roles) writes one aggregated group.update row with diffs", async () => {
const group = await insertGroup("aud_grp_u");
const userOld = await insertUser("aud_grp_uOld");
const userNew = await insertUser("aud_grp_uNew");
const appOld = await insertApp("aud_grp_aOld");
const appNew = await insertApp("aud_grp_aNew");
const roleOld = await insertRole("aud_grp_rOld");
const roleNew = await insertRole("aud_grp_rNew");
// Seed the "previous" associations directly so we can predict the diff.
await pool.query(
`INSERT INTO user_groups (user_id, group_id) VALUES ($1, $2)`,
[userOld.id, group.id],
);
await pool.query(
`INSERT INTO group_apps (group_id, app_id) VALUES ($1, $2)`,
[group.id, appOld.id],
);
await pool.query(
`INSERT INTO group_roles (group_id, role_id) VALUES ($1, $2)`,
[group.id, roleOld.id],
);
const res = await fetch(`${API_BASE}/api/groups/${group.id}`, {
method: "PATCH",
headers: { "Content-Type": "application/json", Cookie: adminCookie },
body: JSON.stringify({
userIds: [userNew.id],
appIds: [appNew.id],
roleIds: [roleNew.id],
}),
});
assert.equal(res.status, 200);
const rows = await fetchAuditRows({
action: "group.update",
targetId: group.id,
targetType: "group",
});
assert.equal(rows.length, 1, "aggregate PATCH must emit a single group.update row");
const meta = rows[0].metadata;
assert.equal(meta.name, group.name);
// Each diff is { added: number[], removed: number[] }
assert.deepEqual(meta.members.added, [userNew.id]);
assert.deepEqual(meta.members.removed, [userOld.id]);
assert.deepEqual(meta.apps.added, [appNew.id]);
assert.deepEqual(meta.apps.removed, [appOld.id]);
assert.deepEqual(meta.roles.added, [roleNew.id]);
assert.deepEqual(meta.roles.removed, [roleOld.id]);
});
test("PATCH /api/groups/:id with no diffs does NOT write group.update", async () => {
const group = await insertGroup("aud_grp_noop");
const res = await fetch(`${API_BASE}/api/groups/${group.id}`, {
method: "PATCH",
headers: { "Content-Type": "application/json", Cookie: adminCookie },
body: JSON.stringify({}),
});
assert.equal(res.status, 200);
const rows = await fetchAuditRows({
action: "group.update",
targetId: group.id,
targetType: "group",
});
assert.equal(rows.length, 0);
});
test("POST /api/groups/:id/users/:targetId writes one group.user.add row with username", async () => {
const group = await insertGroup("aud_sub_uadd");
const user = await insertUser("aud_sub_uadd_user");
const res = await fetch(
`${API_BASE}/api/groups/${group.id}/users/${user.id}`,
{ method: "POST", headers: { Cookie: adminCookie } },
);
assert.equal(res.status, 204);
const rows = await fetchAuditRows({
action: "group.user.add",
targetId: group.id,
targetType: "group",
});
assert.equal(rows.length, 1);
assert.equal(rows[0].metadata.groupName, group.name);
assert.equal(rows[0].metadata.userId, user.id);
assert.equal(rows[0].metadata.username, user.username);
});
test("POST then DELETE /api/groups/:id/users/:targetId writes one add row and one group.user.remove row", async () => {
const group = await insertGroup("aud_sub_uremove");
const user = await insertUser("aud_sub_uremove_user");
const add = await fetch(
`${API_BASE}/api/groups/${group.id}/users/${user.id}`,
{ method: "POST", headers: { Cookie: adminCookie } },
);
assert.equal(add.status, 204);
const addRows = await fetchAuditRows({
action: "group.user.add",
targetId: group.id,
targetType: "group",
});
assert.equal(addRows.length, 1);
const rm = await fetch(
`${API_BASE}/api/groups/${group.id}/users/${user.id}`,
{ method: "DELETE", headers: { Cookie: adminCookie } },
);
assert.equal(rm.status, 204);
const rmRows = await fetchAuditRows({
action: "group.user.remove",
targetId: group.id,
targetType: "group",
});
assert.equal(rmRows.length, 1);
assert.equal(rmRows[0].actor_user_id, adminId);
assert.equal(rmRows[0].target_type, "group");
assert.equal(rmRows[0].target_id, group.id);
assert.equal(rmRows[0].metadata.groupName, group.name);
assert.equal(rmRows[0].metadata.userId, user.id);
assert.equal(rmRows[0].metadata.username, user.username);
});
test("POST then DELETE /api/groups/:id/apps/:targetId writes one add row and one remove row", async () => {
const group = await insertGroup("aud_sub_app");
const app = await insertApp("aud_sub_app_app");
const add = await fetch(`${API_BASE}/api/groups/${group.id}/apps/${app.id}`, {
method: "POST",
headers: { Cookie: adminCookie },
});
assert.equal(add.status, 204);
const addRows = await fetchAuditRows({
action: "group.app.add",
targetId: group.id,
targetType: "group",
});
assert.equal(addRows.length, 1);
assert.equal(addRows[0].metadata.groupName, group.name);
assert.equal(addRows[0].metadata.appId, app.id);
assert.equal(addRows[0].metadata.appSlug, app.slug);
const rm = await fetch(`${API_BASE}/api/groups/${group.id}/apps/${app.id}`, {
method: "DELETE",
headers: { Cookie: adminCookie },
});
assert.equal(rm.status, 204);
const rmRows = await fetchAuditRows({
action: "group.app.remove",
targetId: group.id,
targetType: "group",
});
assert.equal(rmRows.length, 1);
assert.equal(rmRows[0].metadata.appId, app.id);
assert.equal(rmRows[0].metadata.appSlug, app.slug);
});
test("POST then DELETE /api/groups/:id/roles/:targetId writes one add row and one remove row", async () => {
const group = await insertGroup("aud_sub_role");
const role = await insertRole("aud_sub_role_role");
const add = await fetch(
`${API_BASE}/api/groups/${group.id}/roles/${role.id}`,
{ method: "POST", headers: { Cookie: adminCookie } },
);
assert.equal(add.status, 204);
const addRows = await fetchAuditRows({
action: "group.role.add",
targetId: group.id,
targetType: "group",
});
assert.equal(addRows.length, 1);
assert.equal(addRows[0].metadata.roleId, role.id);
assert.equal(addRows[0].metadata.roleName, role.name);
const rm = await fetch(
`${API_BASE}/api/groups/${group.id}/roles/${role.id}`,
{ method: "DELETE", headers: { Cookie: adminCookie } },
);
assert.equal(rm.status, 204);
const rmRows = await fetchAuditRows({
action: "group.role.remove",
targetId: group.id,
targetType: "group",
});
assert.equal(rmRows.length, 1);
assert.equal(rmRows[0].metadata.roleName, role.name);
});
test("DELETE /api/groups/:id (empty group, no force) writes group.delete with force=false and zero counts", async () => {
const group = await insertGroup("aud_grp_d_nf");
const res = await fetch(`${API_BASE}/api/groups/${group.id}`, {
method: "DELETE",
headers: { Cookie: adminCookie },
});
assert.equal(res.status, 204);
const rows = await fetchAuditRows({
action: "group.delete",
targetId: group.id,
targetType: "group",
});
assert.equal(rows.length, 1);
assert.equal(rows[0].actor_user_id, adminId);
assert.equal(rows[0].metadata.name, group.name);
assert.equal(rows[0].metadata.force, false);
assert.equal(rows[0].metadata.memberCount, 0);
assert.equal(rows[0].metadata.appCount, 0);
assert.equal(rows[0].metadata.roleCount, 0);
});
test("DELETE /api/groups/:id without force on a non-empty group returns 409 and writes NO audit row", async () => {
const group = await insertGroup("aud_grp_409");
const member = await insertUser("aud_grp_409_member");
await pool.query(
`INSERT INTO user_groups (user_id, group_id) VALUES ($1, $2)`,
[member.id, group.id],
);
const res = await fetch(`${API_BASE}/api/groups/${group.id}`, {
method: "DELETE",
headers: { Cookie: adminCookie },
});
assert.equal(res.status, 409);
const stillThere = await pool.query(`SELECT id FROM groups WHERE id = $1`, [
group.id,
]);
assert.equal(stillThere.rowCount, 1, "group must still exist after 409");
const rows = await fetchAuditRows({
action: "group.delete",
targetId: group.id,
targetType: "group",
});
assert.equal(
rows.length,
0,
"denied delete (409) must NOT emit a group.delete audit row",
);
});
test("DELETE /api/groups/:id?force=true (with members) writes one group.delete with force=true and counts", async () => {
const group = await insertGroup("aud_grp_d_f");
const member = await insertUser("aud_grp_d_f_member");
await pool.query(
`INSERT INTO user_groups (user_id, group_id) VALUES ($1, $2)`,
[member.id, group.id],
);
const res = await fetch(`${API_BASE}/api/groups/${group.id}?force=true`, {
method: "DELETE",
headers: { Cookie: adminCookie },
});
assert.equal(res.status, 204);
const rows = await fetchAuditRows({
action: "group.delete",
targetId: group.id,
targetType: "group",
});
assert.equal(rows.length, 1);
assert.equal(rows[0].metadata.force, true);
assert.equal(rows[0].metadata.memberCount, 1);
});
// ---------- apps ----------
test("POST /api/apps writes one app.create row", async () => {
const slug = uniqueName("aud_app_c").toLowerCase();
const res = await fetch(`${API_BASE}/api/apps`, {
method: "POST",
headers: { "Content-Type": "application/json", Cookie: adminCookie },
body: JSON.stringify({
slug,
nameAr: "تطبيق",
nameEn: "Audit Create App",
iconName: "Box",
route: "/aud-c",
color: "#fff",
}),
});
assert.equal(res.status, 201);
const created = await res.json();
createdAppIds.push(created.id);
const rows = await fetchAuditRows({
action: "app.create",
targetId: created.id,
targetType: "app",
});
assert.equal(rows.length, 1);
assert.equal(rows[0].metadata.slug, slug);
assert.equal(rows[0].metadata.nameEn, "Audit Create App");
assert.equal(rows[0].metadata.route, "/aud-c");
});
test("PATCH /api/apps/:id (real change) writes one app.update row with from/to changes", async () => {
const app = await insertApp("aud_app_u");
const res = await fetch(`${API_BASE}/api/apps/${app.id}`, {
method: "PATCH",
headers: { "Content-Type": "application/json", Cookie: adminCookie },
body: JSON.stringify({ nameEn: "Audit App Renamed" }),
});
assert.equal(res.status, 200);
const rows = await fetchAuditRows({
action: "app.update",
targetId: app.id,
targetType: "app",
});
assert.equal(rows.length, 1);
assert.equal(rows[0].metadata.slug, app.slug);
assert.equal(rows[0].metadata.changes.nameEn.from, "Audit App");
assert.equal(rows[0].metadata.changes.nameEn.to, "Audit App Renamed");
});
test("PATCH /api/apps/:id with same value does NOT write app.update", async () => {
const app = await insertApp("aud_app_noop");
const res = await fetch(`${API_BASE}/api/apps/${app.id}`, {
method: "PATCH",
headers: { "Content-Type": "application/json", Cookie: adminCookie },
body: JSON.stringify({ nameEn: "Audit App" }),
});
assert.equal(res.status, 200);
const rows = await fetchAuditRows({
action: "app.update",
targetId: app.id,
targetType: "app",
});
assert.equal(rows.length, 0);
});
test("DELETE /api/apps/:id (no deps) writes app.delete with force=false and zero counts", async () => {
const app = await insertApp("aud_app_d_nf");
const res = await fetch(`${API_BASE}/api/apps/${app.id}`, {
method: "DELETE",
headers: { Cookie: adminCookie },
});
assert.equal(res.status, 204);
const rows = await fetchAuditRows({
action: "app.delete",
targetId: app.id,
targetType: "app",
});
assert.equal(rows.length, 1);
assert.equal(rows[0].metadata.appSlug, app.slug);
assert.equal(rows[0].metadata.appNameEn, "Audit App");
assert.equal(rows[0].metadata.appNameAr, "تطبيق تدقيق");
assert.equal(rows[0].metadata.force, false);
assert.equal(rows[0].metadata.groupCount, 0);
assert.equal(rows[0].metadata.restrictionCount, 0);
assert.equal(rows[0].metadata.openCount, 0);
});
test("DELETE /api/apps/:id without force on an app with deps returns 409 and writes NO audit row", async () => {
const app = await insertApp("aud_app_409");
const group = await insertGroup("aud_app_409_grp");
await pool.query(
`INSERT INTO group_apps (group_id, app_id) VALUES ($1, $2)`,
[group.id, app.id],
);
const res = await fetch(`${API_BASE}/api/apps/${app.id}`, {
method: "DELETE",
headers: { Cookie: adminCookie },
});
assert.equal(res.status, 409);
const stillThere = await pool.query(`SELECT id FROM apps WHERE id = $1`, [
app.id,
]);
assert.equal(stillThere.rowCount, 1, "app must still exist after 409");
const rows = await fetchAuditRows({
action: "app.delete",
targetId: app.id,
targetType: "app",
});
assert.equal(
rows.length,
0,
"denied delete (409) must NOT emit an app.delete audit row",
);
});
test("DELETE /api/apps/:id?force=true (with deps) writes app.delete with force=true and dep counts", async () => {
const app = await insertApp("aud_app_d_f");
const group = await insertGroup("aud_app_d_f_grp");
await pool.query(
`INSERT INTO group_apps (group_id, app_id) VALUES ($1, $2)`,
[group.id, app.id],
);
await pool.query(`INSERT INTO app_opens (user_id, app_id) VALUES ($1, $2)`, [
adminId,
app.id,
]);
const res = await fetch(`${API_BASE}/api/apps/${app.id}?force=true`, {
method: "DELETE",
headers: { Cookie: adminCookie },
});
assert.equal(res.status, 204);
const rows = await fetchAuditRows({
action: "app.delete",
targetId: app.id,
targetType: "app",
});
assert.equal(rows.length, 1);
assert.equal(rows[0].metadata.force, true);
assert.equal(rows[0].metadata.groupCount, 1);
assert.equal(rows[0].metadata.openCount, 1);
});
// ---------- services ----------
test("DELETE /api/services/:id (no deps, no force) writes one service.delete row with nameEn and nameAr", async () => {
// #195: a clean delete (no orders) used to leave no audit trail at all.
// It must now write a `service.delete` row carrying both names so the
// audit log can render the service name in either locale after deletion.
const svc = await insertService("aud_svc_d_nf");
const res = await fetch(`${API_BASE}/api/services/${svc.id}`, {
method: "DELETE",
headers: { Cookie: adminCookie },
});
assert.equal(res.status, 204);
const rows = await fetchAuditRows({
action: "service.delete",
targetId: svc.id,
targetType: "service",
});
assert.equal(rows.length, 1, "expected exactly one service.delete audit row");
const row = rows[0];
assert.equal(row.actor_user_id, adminId);
assert.equal(row.metadata.nameEn, svc.nameEn);
assert.equal(row.metadata.nameAr, svc.nameAr);
// No-deps path must NOT include the dependency counts that
// `service.force_delete` carries.
assert.equal(row.metadata.orderCount, undefined);
// The forced action must not be emitted on the clean path.
const forced = await fetchAuditRows({
action: "service.force_delete",
targetId: svc.id,
targetType: "service",
});
assert.equal(forced.length, 0);
});
test("DELETE /api/services/:id without force on a service with deps returns 409 and writes NO audit row", async () => {
const svc = await insertService("aud_svc_409");
await pool.query(
`INSERT INTO service_orders (user_id, service_id, status) VALUES ($1, $2, 'pending')`,
[adminId, svc.id],
);
const res = await fetch(`${API_BASE}/api/services/${svc.id}`, {
method: "DELETE",
headers: { Cookie: adminCookie },
});
assert.equal(res.status, 409);
const stillThere = await pool.query(
`SELECT id FROM services WHERE id = $1`,
[svc.id],
);
assert.equal(stillThere.rowCount, 1, "service must still exist after 409");
const deleteRows = await fetchAuditRows({
action: "service.delete",
targetId: svc.id,
targetType: "service",
});
const forceRows = await fetchAuditRows({
action: "service.force_delete",
targetId: svc.id,
targetType: "service",
});
assert.equal(
deleteRows.length + forceRows.length,
0,
"denied delete (409) must NOT emit any service audit row",
);
});
test("DELETE /api/services/:id?force=true (with deps) writes one service.force_delete row with orderCount", async () => {
const svc = await insertService("aud_svc_d_f");
await pool.query(
`INSERT INTO service_orders (user_id, service_id, status) VALUES ($1, $2, 'pending'), ($1, $2, 'completed')`,
[adminId, svc.id],
);
const res = await fetch(`${API_BASE}/api/services/${svc.id}?force=true`, {
method: "DELETE",
headers: { Cookie: adminCookie },
});
assert.equal(res.status, 204);
const rows = await fetchAuditRows({
action: "service.force_delete",
targetId: svc.id,
targetType: "service",
});
assert.equal(rows.length, 1);
const row = rows[0];
assert.equal(row.actor_user_id, adminId);
assert.equal(row.metadata.nameEn, svc.nameEn);
assert.equal(row.metadata.nameAr, svc.nameAr);
assert.equal(row.metadata.orderCount, 2);
// Forced path must not also emit the plain `service.delete` row.
const plain = await fetchAuditRows({
action: "service.delete",
targetId: svc.id,
targetType: "service",
});
assert.equal(plain.length, 0);
});
// ---------- auth: password reset link ----------
test("POST /api/auth/admin/users/:id/issue-reset-link writes one auth.issue_reset_link row", async () => {
const target = await insertUser("aud_reset");
const res = await fetch(
`${API_BASE}/api/auth/admin/users/${target.id}/issue-reset-link`,
{ method: "POST", headers: { Cookie: adminCookie } },
);
assert.equal(res.status, 200);
const body = await res.json();
assert.ok(body.resetUrl);
assert.ok(body.expiresAt);
const rows = await fetchAuditRows({
action: "auth.issue_reset_link",
targetId: target.id,
targetType: "user",
});
assert.equal(rows.length, 1);
assert.equal(rows[0].actor_user_id, adminId);
assert.equal(rows[0].metadata.username, target.username);
assert.equal(rows[0].metadata.email, `${target.username}@example.com`);
assert.equal(rows[0].metadata.expiresAt, body.expiresAt);
});
// ---------- settings ----------
test("PATCH /api/settings with a real change writes one settings.update row", async () => {
// Read current value, change it, observe the audit row, then restore.
const before = await pool.query(
`SELECT site_name_en FROM app_settings WHERE id = 1`,
);
const previous = before.rows[0]?.site_name_en ?? "Tx OS";
const next = `Audit ${STAMP}`;
const res = await fetch(`${API_BASE}/api/settings`, {
method: "PATCH",
headers: { "Content-Type": "application/json", Cookie: adminCookie },
body: JSON.stringify({ siteNameEn: next }),
});
assert.equal(res.status, 200);
const rows = await fetchAuditRows({
action: "settings.update",
targetId: 1,
targetType: "settings",
});
assert.equal(rows.length, 1, "exactly one settings.update row for a real change");
assert.equal(rows[0].actor_user_id, adminId);
assert.equal(rows[0].metadata.changes.siteNameEn.from, previous);
assert.equal(rows[0].metadata.changes.siteNameEn.to, next);
// Clean up the audit row produced by this test so the next test starts
// from zero settings.update rows for our admin actor.
await pool.query(
`DELETE FROM audit_logs WHERE actor_user_id = $1 AND action = 'settings.update'`,
[adminId],
);
// Restore the original value via the API; that emits another audit row,
// which we also clean up so a later run is unaffected.
const restore = await fetch(`${API_BASE}/api/settings`, {
method: "PATCH",
headers: { "Content-Type": "application/json", Cookie: adminCookie },
body: JSON.stringify({ siteNameEn: previous }),
});
assert.equal(restore.status, 200);
await pool.query(
`DELETE FROM audit_logs WHERE actor_user_id = $1 AND action = 'settings.update'`,
[adminId],
);
});
test("PATCH /api/settings with no effective change does NOT write settings.update", async () => {
const before = await pool.query(
`SELECT site_name_en FROM app_settings WHERE id = 1`,
);
const current = before.rows[0]?.site_name_en ?? "Tx OS";
// PATCHing with the value that's already there must be a no-op.
const res = await fetch(`${API_BASE}/api/settings`, {
method: "PATCH",
headers: { "Content-Type": "application/json", Cookie: adminCookie },
body: JSON.stringify({ siteNameEn: current }),
});
assert.equal(res.status, 200);
const rows = await fetchAuditRows({
action: "settings.update",
targetId: 1,
targetType: "settings",
});
assert.equal(
rows.length,
0,
"no-op settings PATCH must not emit a settings.update audit row",
);
});