2026-04-28 08:02:41 +00:00
|
|
|
|
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_HASH =
|
|
|
|
|
|
"$2b$10$Bs636ukPMyz01nKrsi.5m.JlDXSN22AVCvn8cgPWWDbo5yJRQX2vu";
|
|
|
|
|
|
const TEST_PASSWORD = "TestPass123!";
|
|
|
|
|
|
|
|
|
|
|
|
const pool = new pg.Pool({ connectionString: DATABASE_URL });
|
|
|
|
|
|
|
|
|
|
|
|
const created = {
|
|
|
|
|
|
userIds: [],
|
|
|
|
|
|
meetingIds: [],
|
|
|
|
|
|
requestIds: [],
|
|
|
|
|
|
taskIds: [],
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
function uniqueName(prefix) {
|
|
|
|
|
|
return `${prefix}_${Date.now().toString(36)}_${Math.random()
|
|
|
|
|
|
.toString(36)
|
|
|
|
|
|
.slice(2, 8)}`;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function createUser(prefix, roleName) {
|
|
|
|
|
|
const username = uniqueName(prefix);
|
|
|
|
|
|
const { rows } = await pool.query(
|
|
|
|
|
|
`INSERT INTO users (username, email, password_hash, display_name_en, preferred_language, is_active)
|
|
|
|
|
|
VALUES ($1, $2, $3, 'EM Test', 'en', true) RETURNING id`,
|
|
|
|
|
|
[username, `${username}@example.com`, TEST_PASSWORD_HASH],
|
|
|
|
|
|
);
|
|
|
|
|
|
const id = rows[0].id;
|
|
|
|
|
|
created.userIds.push(id);
|
2026-04-28 08:14:01 +00:00
|
|
|
|
for (const r of ["user", roleName]) {
|
2026-04-28 08:02:41 +00:00
|
|
|
|
await pool.query(
|
|
|
|
|
|
`INSERT INTO user_roles (user_id, role_id)
|
|
|
|
|
|
SELECT $1, id FROM roles WHERE name = $2
|
|
|
|
|
|
ON CONFLICT DO NOTHING`,
|
|
|
|
|
|
[id, r],
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
return { id, username };
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function extractCookie(res) {
|
|
|
|
|
|
const setCookie = res.headers.get("set-cookie");
|
|
|
|
|
|
if (!setCookie) return null;
|
|
|
|
|
|
return setCookie
|
|
|
|
|
|
.split(",")
|
|
|
|
|
|
.map((c) => c.split(";")[0].trim())
|
|
|
|
|
|
.find((c) => c.startsWith("connect.sid=")) ?? null;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
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 should succeed for ${username}`);
|
|
|
|
|
|
const cookie = extractCookie(res);
|
|
|
|
|
|
assert.ok(cookie, "login response should set a session cookie");
|
|
|
|
|
|
return cookie;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function api(cookie, method, path, body) {
|
|
|
|
|
|
const init = {
|
|
|
|
|
|
method,
|
|
|
|
|
|
headers: {
|
|
|
|
|
|
Cookie: cookie,
|
|
|
|
|
|
...(body !== undefined ? { "Content-Type": "application/json" } : {}),
|
|
|
|
|
|
},
|
|
|
|
|
|
};
|
|
|
|
|
|
if (body !== undefined) init.body = JSON.stringify(body);
|
|
|
|
|
|
return fetch(`${API_BASE}${path}`, init);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
let adminCookie = null;
|
|
|
|
|
|
let adminUserId = null;
|
|
|
|
|
|
let coordCookie = null;
|
|
|
|
|
|
let coordUserId = null;
|
|
|
|
|
|
let leadCookie = null;
|
|
|
|
|
|
let leadUserId = null;
|
|
|
|
|
|
|
|
|
|
|
|
before(async () => {
|
|
|
|
|
|
adminCookie = await login("admin", "admin123");
|
|
|
|
|
|
const meRes = await api(adminCookie, "GET", "/api/auth/me");
|
|
|
|
|
|
const me = await meRes.json();
|
|
|
|
|
|
adminUserId = me.id ?? me.userId;
|
|
|
|
|
|
|
|
|
|
|
|
const coord = await createUser("em_coord", "executive_coordinator");
|
|
|
|
|
|
coordUserId = coord.id;
|
|
|
|
|
|
coordCookie = await login(coord.username, TEST_PASSWORD);
|
|
|
|
|
|
|
|
|
|
|
|
const lead = await createUser("em_lead", "executive_coord_lead");
|
|
|
|
|
|
leadUserId = lead.id;
|
|
|
|
|
|
leadCookie = await login(lead.username, TEST_PASSWORD);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
after(async () => {
|
|
|
|
|
|
if (created.taskIds.length > 0) {
|
|
|
|
|
|
await pool.query(`DELETE FROM executive_meeting_tasks WHERE id = ANY($1::int[])`, [
|
|
|
|
|
|
created.taskIds,
|
|
|
|
|
|
]);
|
|
|
|
|
|
}
|
|
|
|
|
|
if (created.requestIds.length > 0) {
|
|
|
|
|
|
await pool.query(`DELETE FROM executive_meeting_requests WHERE id = ANY($1::int[])`, [
|
|
|
|
|
|
created.requestIds,
|
|
|
|
|
|
]);
|
|
|
|
|
|
}
|
|
|
|
|
|
if (created.meetingIds.length > 0) {
|
|
|
|
|
|
await pool.query(`DELETE FROM executive_meeting_attendees WHERE meeting_id = ANY($1::int[])`, [
|
|
|
|
|
|
created.meetingIds,
|
|
|
|
|
|
]);
|
|
|
|
|
|
await pool.query(`DELETE FROM executive_meeting_audit_logs WHERE entity_id = ANY($1::int[]) AND entity_type IN ('meeting','request','task')`, [
|
|
|
|
|
|
created.meetingIds,
|
|
|
|
|
|
]);
|
|
|
|
|
|
await pool.query(`DELETE FROM executive_meetings WHERE id = ANY($1::int[])`, [
|
|
|
|
|
|
created.meetingIds,
|
|
|
|
|
|
]);
|
|
|
|
|
|
}
|
|
|
|
|
|
if (created.userIds.length > 0) {
|
|
|
|
|
|
await pool.query(`DELETE FROM executive_meeting_tasks WHERE assigned_to = ANY($1::int[])`, [
|
|
|
|
|
|
created.userIds,
|
|
|
|
|
|
]);
|
|
|
|
|
|
await pool.query(`DELETE FROM executive_meeting_requests WHERE requested_by = ANY($1::int[])`, [
|
|
|
|
|
|
created.userIds,
|
|
|
|
|
|
]);
|
|
|
|
|
|
await pool.query(`DELETE FROM user_roles WHERE user_id = ANY($1::int[])`, [
|
|
|
|
|
|
created.userIds,
|
|
|
|
|
|
]);
|
|
|
|
|
|
await pool.query(`DELETE FROM users WHERE id = ANY($1::int[])`, [
|
|
|
|
|
|
created.userIds,
|
|
|
|
|
|
]);
|
|
|
|
|
|
}
|
|
|
|
|
|
await pool.end();
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
const today = new Date().toISOString().slice(0, 10);
|
|
|
|
|
|
const tomorrow = new Date(Date.now() + 24 * 60 * 60 * 1000)
|
|
|
|
|
|
.toISOString()
|
|
|
|
|
|
.slice(0, 10);
|
|
|
|
|
|
|
2026-04-28 08:14:01 +00:00
|
|
|
|
test("GET /me exposes capability flags including canViewAllTasks", async () => {
|
2026-04-28 08:02:41 +00:00
|
|
|
|
const res = await api(adminCookie, "GET", "/api/executive-meetings/me");
|
|
|
|
|
|
assert.equal(res.status, 200);
|
|
|
|
|
|
const body = await res.json();
|
|
|
|
|
|
for (const key of [
|
|
|
|
|
|
"userId",
|
|
|
|
|
|
"roles",
|
|
|
|
|
|
"canRead",
|
|
|
|
|
|
"canMutate",
|
|
|
|
|
|
"canApprove",
|
|
|
|
|
|
"canSubmitRequest",
|
|
|
|
|
|
"canViewAudit",
|
|
|
|
|
|
"canViewTasks",
|
|
|
|
|
|
"canViewAllTasks",
|
|
|
|
|
|
]) {
|
|
|
|
|
|
assert.ok(key in body, `missing ${key} in /me response`);
|
|
|
|
|
|
}
|
|
|
|
|
|
assert.equal(body.canViewAllTasks, true);
|
|
|
|
|
|
|
|
|
|
|
|
const coordRes = await api(coordCookie, "GET", "/api/executive-meetings/me");
|
|
|
|
|
|
const coordMe = await coordRes.json();
|
|
|
|
|
|
assert.equal(coordMe.canViewTasks, true);
|
2026-04-28 08:14:01 +00:00
|
|
|
|
assert.equal(coordMe.canViewAllTasks, false);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
test("Meetings: bilingual title required (titleEn cannot be empty)", async () => {
|
|
|
|
|
|
const missingEn = await api(adminCookie, "POST", "/api/executive-meetings", {
|
|
|
|
|
|
titleAr: "اجتماع",
|
|
|
|
|
|
meetingDate: today,
|
|
|
|
|
|
});
|
|
|
|
|
|
assert.equal(missingEn.status, 400);
|
|
|
|
|
|
|
|
|
|
|
|
const emptyEn = await api(adminCookie, "POST", "/api/executive-meetings", {
|
|
|
|
|
|
titleAr: "اجتماع",
|
|
|
|
|
|
titleEn: "",
|
|
|
|
|
|
meetingDate: today,
|
|
|
|
|
|
});
|
|
|
|
|
|
assert.equal(emptyEn.status, 400);
|
2026-04-28 08:02:41 +00:00
|
|
|
|
});
|
|
|
|
|
|
|
2026-04-28 08:14:01 +00:00
|
|
|
|
test("Meetings: POST → GET day → DELETE roundtrip", async () => {
|
2026-04-28 08:02:41 +00:00
|
|
|
|
const create = await api(adminCookie, "POST", "/api/executive-meetings", {
|
|
|
|
|
|
titleAr: "اجتماع اختبار",
|
|
|
|
|
|
titleEn: "Test meeting",
|
|
|
|
|
|
meetingDate: today,
|
|
|
|
|
|
startTime: "09:00",
|
|
|
|
|
|
endTime: "10:00",
|
|
|
|
|
|
platform: "none",
|
|
|
|
|
|
status: "scheduled",
|
|
|
|
|
|
isHighlighted: 0,
|
|
|
|
|
|
attendees: [
|
|
|
|
|
|
{ name: "Tester One", attendanceType: "internal", sortOrder: 0 },
|
|
|
|
|
|
],
|
|
|
|
|
|
});
|
|
|
|
|
|
assert.equal(create.status, 201);
|
|
|
|
|
|
const meeting = await create.json();
|
|
|
|
|
|
assert.ok(meeting.id);
|
|
|
|
|
|
created.meetingIds.push(meeting.id);
|
|
|
|
|
|
|
|
|
|
|
|
const day = await api(
|
|
|
|
|
|
adminCookie,
|
|
|
|
|
|
"GET",
|
|
|
|
|
|
`/api/executive-meetings?date=${today}`,
|
|
|
|
|
|
);
|
|
|
|
|
|
assert.equal(day.status, 200);
|
|
|
|
|
|
const dayBody = await day.json();
|
|
|
|
|
|
assert.ok(Array.isArray(dayBody.meetings));
|
|
|
|
|
|
assert.ok(dayBody.meetings.some((m) => m.id === meeting.id));
|
|
|
|
|
|
|
|
|
|
|
|
const del = await api(
|
|
|
|
|
|
adminCookie,
|
|
|
|
|
|
"DELETE",
|
|
|
|
|
|
`/api/executive-meetings/${meeting.id}`,
|
|
|
|
|
|
);
|
|
|
|
|
|
assert.ok(del.status === 200 || del.status === 204);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2026-04-28 08:14:01 +00:00
|
|
|
|
test("Meetings: PUT /attendees replaces the attendee list", async () => {
|
|
|
|
|
|
const create = await api(adminCookie, "POST", "/api/executive-meetings", {
|
|
|
|
|
|
titleAr: "ا",
|
|
|
|
|
|
titleEn: "A",
|
|
|
|
|
|
meetingDate: today,
|
|
|
|
|
|
attendees: [{ name: "Old One", attendanceType: "internal", sortOrder: 0 }],
|
|
|
|
|
|
});
|
|
|
|
|
|
assert.equal(create.status, 201);
|
|
|
|
|
|
const meeting = await create.json();
|
|
|
|
|
|
created.meetingIds.push(meeting.id);
|
|
|
|
|
|
|
|
|
|
|
|
const replace = await api(
|
|
|
|
|
|
adminCookie,
|
|
|
|
|
|
"PUT",
|
|
|
|
|
|
`/api/executive-meetings/${meeting.id}/attendees`,
|
|
|
|
|
|
{
|
|
|
|
|
|
attendees: [
|
|
|
|
|
|
{ name: "New One", title: "Director", attendanceType: "internal", sortOrder: 0 },
|
|
|
|
|
|
{ name: "New Two", title: "Manager", attendanceType: "virtual", sortOrder: 1 },
|
|
|
|
|
|
],
|
|
|
|
|
|
},
|
|
|
|
|
|
);
|
|
|
|
|
|
assert.equal(replace.status, 200);
|
|
|
|
|
|
const fetched = await api(
|
|
|
|
|
|
adminCookie,
|
|
|
|
|
|
"GET",
|
|
|
|
|
|
`/api/executive-meetings/${meeting.id}`,
|
|
|
|
|
|
);
|
|
|
|
|
|
const body = await fetched.json();
|
|
|
|
|
|
assert.equal(body.attendees.length, 2);
|
|
|
|
|
|
assert.ok(body.attendees.find((a) => a.name === "New One"));
|
|
|
|
|
|
assert.ok(!body.attendees.find((a) => a.name === "Old One"));
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
test("Meetings: POST /duplicate clones a meeting onto another date", async () => {
|
|
|
|
|
|
const create = await api(adminCookie, "POST", "/api/executive-meetings", {
|
|
|
|
|
|
titleAr: "ب",
|
|
|
|
|
|
titleEn: "B",
|
|
|
|
|
|
meetingDate: today,
|
|
|
|
|
|
attendees: [{ name: "Dup Att", attendanceType: "internal", sortOrder: 0 }],
|
|
|
|
|
|
});
|
|
|
|
|
|
assert.equal(create.status, 201);
|
|
|
|
|
|
const original = await create.json();
|
|
|
|
|
|
created.meetingIds.push(original.id);
|
|
|
|
|
|
|
|
|
|
|
|
const dup = await api(
|
|
|
|
|
|
adminCookie,
|
|
|
|
|
|
"POST",
|
|
|
|
|
|
`/api/executive-meetings/${original.id}/duplicate`,
|
|
|
|
|
|
{ targetDate: tomorrow },
|
|
|
|
|
|
);
|
|
|
|
|
|
assert.equal(dup.status, 201);
|
|
|
|
|
|
const newRow = await dup.json();
|
|
|
|
|
|
assert.notEqual(newRow.id, original.id);
|
|
|
|
|
|
created.meetingIds.push(newRow.id);
|
|
|
|
|
|
|
|
|
|
|
|
const day = await api(
|
|
|
|
|
|
adminCookie,
|
|
|
|
|
|
"GET",
|
|
|
|
|
|
`/api/executive-meetings?date=${tomorrow}`,
|
|
|
|
|
|
);
|
|
|
|
|
|
const dayBody = await day.json();
|
|
|
|
|
|
assert.ok(dayBody.meetings.some((m) => m.id === newRow.id));
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
test("Requests: POST → admin can list", async () => {
|
2026-04-28 08:02:41 +00:00
|
|
|
|
const create = await api(
|
|
|
|
|
|
adminCookie,
|
|
|
|
|
|
"POST",
|
|
|
|
|
|
"/api/executive-meetings/requests",
|
|
|
|
|
|
{
|
|
|
|
|
|
requestType: "note",
|
|
|
|
|
|
requestDetails: { note: "Phase-2 test request" },
|
|
|
|
|
|
},
|
|
|
|
|
|
);
|
|
|
|
|
|
assert.equal(create.status, 201);
|
|
|
|
|
|
const reqRow = await create.json();
|
|
|
|
|
|
assert.ok(reqRow.id);
|
|
|
|
|
|
created.requestIds.push(reqRow.id);
|
|
|
|
|
|
|
|
|
|
|
|
const list = await api(adminCookie, "GET", "/api/executive-meetings/requests");
|
|
|
|
|
|
assert.equal(list.status, 200);
|
|
|
|
|
|
const listBody = await list.json();
|
|
|
|
|
|
assert.ok(Array.isArray(listBody.requests));
|
|
|
|
|
|
assert.ok(listBody.requests.some((r) => r.id === reqRow.id));
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2026-04-28 08:14:01 +00:00
|
|
|
|
test("Requests: full review + apply pipeline (approve → apply highlights meeting)", async () => {
|
|
|
|
|
|
const create = await api(adminCookie, "POST", "/api/executive-meetings", {
|
|
|
|
|
|
titleAr: "ت",
|
|
|
|
|
|
titleEn: "T",
|
|
|
|
|
|
meetingDate: today,
|
|
|
|
|
|
attendees: [],
|
|
|
|
|
|
isHighlighted: 0,
|
|
|
|
|
|
});
|
|
|
|
|
|
assert.equal(create.status, 201);
|
|
|
|
|
|
const meeting = await create.json();
|
|
|
|
|
|
created.meetingIds.push(meeting.id);
|
|
|
|
|
|
|
|
|
|
|
|
const reqRes = await api(adminCookie, "POST", "/api/executive-meetings/requests", {
|
|
|
|
|
|
requestType: "highlight",
|
|
|
|
|
|
meetingId: meeting.id,
|
|
|
|
|
|
requestDetails: { note: "highlight-please" },
|
|
|
|
|
|
});
|
|
|
|
|
|
assert.equal(reqRes.status, 201);
|
|
|
|
|
|
const reqRow = await reqRes.json();
|
|
|
|
|
|
created.requestIds.push(reqRow.id);
|
|
|
|
|
|
|
|
|
|
|
|
const approve = await api(
|
|
|
|
|
|
adminCookie,
|
|
|
|
|
|
"PATCH",
|
|
|
|
|
|
`/api/executive-meetings/requests/${reqRow.id}`,
|
|
|
|
|
|
{ status: "approved", reviewNotes: "ok" },
|
|
|
|
|
|
);
|
|
|
|
|
|
assert.equal(approve.status, 200);
|
|
|
|
|
|
|
|
|
|
|
|
const fetched = await api(
|
|
|
|
|
|
adminCookie,
|
|
|
|
|
|
"GET",
|
|
|
|
|
|
`/api/executive-meetings/${meeting.id}`,
|
|
|
|
|
|
);
|
|
|
|
|
|
const body = await fetched.json();
|
|
|
|
|
|
assert.equal(body.isHighlighted, 1);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2026-04-28 08:02:41 +00:00
|
|
|
|
test("Tasks: server-side scoping forces coordinators to assignedTo=self", async () => {
|
|
|
|
|
|
const t1 = await api(adminCookie, "POST", "/api/executive-meetings/tasks", {
|
|
|
|
|
|
taskType: "follow_up",
|
|
|
|
|
|
assignedTo: coordUserId,
|
|
|
|
|
|
notes: "for coord",
|
|
|
|
|
|
});
|
|
|
|
|
|
assert.equal(t1.status, 201);
|
|
|
|
|
|
const task1 = await t1.json();
|
|
|
|
|
|
created.taskIds.push(task1.id);
|
|
|
|
|
|
|
|
|
|
|
|
const t2 = await api(adminCookie, "POST", "/api/executive-meetings/tasks", {
|
|
|
|
|
|
taskType: "follow_up",
|
|
|
|
|
|
assignedTo: leadUserId,
|
|
|
|
|
|
notes: "for lead",
|
|
|
|
|
|
});
|
|
|
|
|
|
assert.equal(t2.status, 201);
|
|
|
|
|
|
const task2 = await t2.json();
|
|
|
|
|
|
created.taskIds.push(task2.id);
|
|
|
|
|
|
|
|
|
|
|
|
const coordList = await api(
|
|
|
|
|
|
coordCookie,
|
|
|
|
|
|
"GET",
|
|
|
|
|
|
`/api/executive-meetings/tasks?mine=0&assigneeId=${leadUserId}`,
|
|
|
|
|
|
);
|
|
|
|
|
|
assert.equal(coordList.status, 200);
|
|
|
|
|
|
const coordBody = await coordList.json();
|
|
|
|
|
|
const coordIds = coordBody.tasks.map((t) => t.id);
|
2026-04-28 08:14:01 +00:00
|
|
|
|
assert.ok(coordIds.includes(task1.id));
|
|
|
|
|
|
assert.ok(!coordIds.includes(task2.id));
|
2026-04-28 08:02:41 +00:00
|
|
|
|
for (const t of coordBody.tasks) {
|
2026-04-28 08:14:01 +00:00
|
|
|
|
assert.equal(t.assignedTo, coordUserId);
|
2026-04-28 08:02:41 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const leadList = await api(
|
|
|
|
|
|
leadCookie,
|
|
|
|
|
|
"GET",
|
|
|
|
|
|
`/api/executive-meetings/tasks?assigneeId=${leadUserId}`,
|
|
|
|
|
|
);
|
|
|
|
|
|
assert.equal(leadList.status, 200);
|
|
|
|
|
|
const leadBody = await leadList.json();
|
|
|
|
|
|
assert.ok(leadBody.tasks.some((t) => t.id === task2.id));
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2026-04-28 08:14:01 +00:00
|
|
|
|
test("Tasks: PATCH supports reassign + notes update", async () => {
|
|
|
|
|
|
const create = await api(adminCookie, "POST", "/api/executive-meetings/tasks", {
|
|
|
|
|
|
taskType: "follow_up",
|
|
|
|
|
|
assignedTo: coordUserId,
|
|
|
|
|
|
notes: "v1",
|
|
|
|
|
|
});
|
|
|
|
|
|
assert.equal(create.status, 201);
|
|
|
|
|
|
const task = await create.json();
|
|
|
|
|
|
created.taskIds.push(task.id);
|
|
|
|
|
|
|
|
|
|
|
|
const patch = await api(
|
|
|
|
|
|
adminCookie,
|
|
|
|
|
|
"PATCH",
|
|
|
|
|
|
`/api/executive-meetings/tasks/${task.id}`,
|
|
|
|
|
|
{ assignedTo: leadUserId, notes: "v2" },
|
|
|
|
|
|
);
|
|
|
|
|
|
assert.equal(patch.status, 200);
|
|
|
|
|
|
const updated = await patch.json();
|
|
|
|
|
|
assert.equal(updated.assignedTo, leadUserId);
|
|
|
|
|
|
assert.equal(updated.notes, "v2");
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2026-04-28 08:02:41 +00:00
|
|
|
|
test("Audit logs: admin can list, plain coordinator gets 403", async () => {
|
|
|
|
|
|
const ok = await api(
|
|
|
|
|
|
adminCookie,
|
|
|
|
|
|
"GET",
|
|
|
|
|
|
"/api/executive-meetings/audit-logs",
|
|
|
|
|
|
);
|
|
|
|
|
|
assert.equal(ok.status, 200);
|
|
|
|
|
|
const body = await ok.json();
|
|
|
|
|
|
assert.ok(Array.isArray(body.logs ?? body.auditLogs ?? body.entries ?? []));
|
|
|
|
|
|
|
|
|
|
|
|
const denied = await api(
|
|
|
|
|
|
coordCookie,
|
|
|
|
|
|
"GET",
|
|
|
|
|
|
"/api/executive-meetings/audit-logs",
|
|
|
|
|
|
);
|
|
|
|
|
|
assert.equal(denied.status, 403);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2026-04-28 08:14:01 +00:00
|
|
|
|
test("Audit logs: dateFrom/dateTo, action, and actorId filters all narrow results", async () => {
|
|
|
|
|
|
const create = await api(adminCookie, "POST", "/api/executive-meetings", {
|
|
|
|
|
|
titleAr: "س",
|
|
|
|
|
|
titleEn: "S",
|
|
|
|
|
|
meetingDate: today,
|
|
|
|
|
|
});
|
|
|
|
|
|
const meeting = await create.json();
|
|
|
|
|
|
created.meetingIds.push(meeting.id);
|
|
|
|
|
|
|
|
|
|
|
|
const filtered = await api(
|
|
|
|
|
|
adminCookie,
|
|
|
|
|
|
"GET",
|
|
|
|
|
|
`/api/executive-meetings/audit-logs?dateFrom=${today}&dateTo=${today}&action=create&actorId=${adminUserId}&entityType=meeting`,
|
|
|
|
|
|
);
|
|
|
|
|
|
assert.equal(filtered.status, 200);
|
|
|
|
|
|
const body = await filtered.json();
|
|
|
|
|
|
const entries = body.entries ?? body.logs ?? [];
|
|
|
|
|
|
assert.ok(Array.isArray(entries));
|
|
|
|
|
|
for (const e of entries) {
|
|
|
|
|
|
assert.equal(e.action, "create");
|
|
|
|
|
|
assert.equal(e.entityType, "meeting");
|
|
|
|
|
|
assert.equal(e.performedBy, adminUserId);
|
|
|
|
|
|
}
|
|
|
|
|
|
assert.ok(entries.some((e) => e.entityId === meeting.id));
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2026-04-28 08:02:41 +00:00
|
|
|
|
test("Notifications: GET filters by ?date= and tolerates empty days", async () => {
|
|
|
|
|
|
const empty = await api(
|
|
|
|
|
|
adminCookie,
|
|
|
|
|
|
"GET",
|
|
|
|
|
|
`/api/executive-meetings/notifications?date=1990-01-01`,
|
|
|
|
|
|
);
|
|
|
|
|
|
assert.equal(empty.status, 200);
|
|
|
|
|
|
const emptyBody = await empty.json();
|
|
|
|
|
|
assert.ok(Array.isArray(emptyBody.notifications));
|
|
|
|
|
|
|
|
|
|
|
|
const todayList = await api(
|
|
|
|
|
|
adminCookie,
|
|
|
|
|
|
"GET",
|
|
|
|
|
|
`/api/executive-meetings/notifications?date=${today}`,
|
|
|
|
|
|
);
|
|
|
|
|
|
assert.equal(todayList.status, 200);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2026-04-28 08:14:01 +00:00
|
|
|
|
test("Font settings: valid combo 200; invalid weight/size/family 400", async () => {
|
2026-04-28 08:02:41 +00:00
|
|
|
|
const ok = await api(
|
|
|
|
|
|
adminCookie,
|
|
|
|
|
|
"PATCH",
|
|
|
|
|
|
"/api/executive-meetings/font-settings",
|
|
|
|
|
|
{
|
|
|
|
|
|
scope: "user",
|
|
|
|
|
|
fontFamily: "Cairo",
|
|
|
|
|
|
fontSize: 16,
|
|
|
|
|
|
fontWeight: "bold",
|
|
|
|
|
|
alignment: "center",
|
|
|
|
|
|
},
|
|
|
|
|
|
);
|
|
|
|
|
|
assert.equal(ok.status, 200);
|
|
|
|
|
|
|
|
|
|
|
|
const badWeight = await api(
|
|
|
|
|
|
adminCookie,
|
|
|
|
|
|
"PATCH",
|
|
|
|
|
|
"/api/executive-meetings/font-settings",
|
|
|
|
|
|
{ scope: "user", fontWeight: "medium" },
|
|
|
|
|
|
);
|
|
|
|
|
|
assert.equal(badWeight.status, 400);
|
|
|
|
|
|
|
|
|
|
|
|
const badSize = await api(
|
|
|
|
|
|
adminCookie,
|
|
|
|
|
|
"PATCH",
|
|
|
|
|
|
"/api/executive-meetings/font-settings",
|
|
|
|
|
|
{ scope: "user", fontSize: 30 },
|
|
|
|
|
|
);
|
|
|
|
|
|
assert.equal(badSize.status, 400);
|
|
|
|
|
|
|
|
|
|
|
|
const badFamily = await api(
|
|
|
|
|
|
adminCookie,
|
|
|
|
|
|
"PATCH",
|
|
|
|
|
|
"/api/executive-meetings/font-settings",
|
|
|
|
|
|
{ scope: "user", fontFamily: "Comic Sans" },
|
|
|
|
|
|
);
|
|
|
|
|
|
assert.equal(badFamily.status, 400);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2026-04-28 08:14:01 +00:00
|
|
|
|
test("PDF archives: POST creates a snapshot and GET returns it", async () => {
|
|
|
|
|
|
const post = await api(
|
|
|
|
|
|
adminCookie,
|
|
|
|
|
|
"POST",
|
|
|
|
|
|
"/api/executive-meetings/pdf-archives",
|
|
|
|
|
|
{ archiveDate: today },
|
|
|
|
|
|
);
|
|
|
|
|
|
assert.equal(post.status, 201);
|
|
|
|
|
|
const archive = await post.json();
|
|
|
|
|
|
assert.ok(archive.id);
|
|
|
|
|
|
assert.ok(archive.version >= 1);
|
|
|
|
|
|
|
|
|
|
|
|
const list = await api(
|
2026-04-28 08:02:41 +00:00
|
|
|
|
adminCookie,
|
|
|
|
|
|
"GET",
|
|
|
|
|
|
`/api/executive-meetings/pdf-archives?date=${today}`,
|
|
|
|
|
|
);
|
2026-04-28 08:14:01 +00:00
|
|
|
|
assert.equal(list.status, 200);
|
|
|
|
|
|
const body = await list.json();
|
|
|
|
|
|
const archives = body.archives ?? body.items ?? body.pdfArchives ?? [];
|
|
|
|
|
|
assert.ok(Array.isArray(archives));
|
|
|
|
|
|
assert.ok(archives.some((a) => a.id === archive.id));
|
2026-04-28 08:02:41 +00:00
|
|
|
|
});
|