Task #243: Admin audit log — focused readability + actor-filter subset
Landed a tight subset of the 13-item umbrella, mirroring the proven narrow-then-defer pattern from #242: - #195 — Plain DELETE /api/services/:id now writes a `service.delete` audit row carrying nameEn + nameAr (force-with-deps still uses the dedicated `service.force_delete`). Added matching `service.delete` formatter case + EN/AR i18n keys, and surfaced nameAr on the existing `service.force_delete` summary. - #197 — `actorUserId` filter for `/admin/audit-logs` and CSV export. openapi.yaml updated, codegen regenerated, server filter wired through parseFilters/buildWhere with 400-on-invalid handling, AuditLogPanel UI got an actor dropdown wired into params + export URL + reset, and a new audit-logs-actor-filter API test (4 cases) covers list narrowing, exclusion, invalid input, and CSV export. - #178 — Formatter unit tests for user.delete (id-only, EN/AR display name resolution, force flag, force + name) and the new service.delete (id-only, EN/AR), 11 new cases (33/33 pass). Skipped #194 — already implemented; users.ts DELETE persists displayName fields and audit-summary already renders user.deleteWithName/forceDeleteWithName. Deferred via follow-ups (no duplicate of existing #182/#183/#184): - F1: #196 recent-activity endpoint + 5 admin panels - F2: #205+#206+#208 permission history CSV/name resolution/timeline - F3: #209+#210 cascade/bulk audit rows + e2e UI spec for History tabs Validation: tx-os typecheck clean; pre-existing executive-meetings.ts errors not regressed; all targeted server tests pass (delete-force-warnings 10, audit-logs target-filter 7, forced-only 6, audit-log-coverage 27, new actor-filter 4, broader audit/services sweep 40); e2e test verified actor dropdown rendering, filter behavior, readable Arabic service.delete summary, and CSV export honoring the filter.
This commit is contained in:
@@ -355,8 +355,8 @@ test("DELETE /api/apps/:id?force=true deletes app and writes audit log", async (
|
||||
|
||||
test("DELETE /api/services/:id without dependents succeeds (no force needed)", async () => {
|
||||
const s = await pool.query(
|
||||
`INSERT INTO services (name_ar, name_en, price, is_available) VALUES ('خ', $1, 1.00, true) RETURNING id`,
|
||||
[`${SVC_PREFIX}Clean`],
|
||||
`INSERT INTO services (name_ar, name_en, price, is_available) VALUES ($1, $2, 1.00, true) RETURNING id`,
|
||||
[`${SVC_PREFIX}CleanAr`, `${SVC_PREFIX}Clean`],
|
||||
);
|
||||
const id = s.rows[0].id;
|
||||
createdServiceIds.push(id);
|
||||
@@ -366,6 +366,43 @@ test("DELETE /api/services/:id without dependents succeeds (no force needed)", a
|
||||
headers: { Cookie: adminCookie },
|
||||
});
|
||||
assert.equal(res.status, 204);
|
||||
|
||||
// #195: plain (no-deps) deletes now write a `service.delete` audit row
|
||||
// carrying both nameEn and nameAr so the audit log can render the service
|
||||
// name in either locale even after the row is gone.
|
||||
const audit = await pool.query(
|
||||
`SELECT action, metadata FROM audit_logs WHERE target_type = 'service' AND target_id = $1`,
|
||||
[String(id)],
|
||||
);
|
||||
assert.equal(audit.rowCount, 1);
|
||||
assert.equal(audit.rows[0].action, "service.delete");
|
||||
assert.equal(audit.rows[0].metadata.nameEn, `${SVC_PREFIX}Clean`);
|
||||
assert.equal(audit.rows[0].metadata.nameAr, `${SVC_PREFIX}CleanAr`);
|
||||
});
|
||||
|
||||
test("DELETE /api/services/:id?force=true with no dependents writes service.delete (not service.force_delete)", async () => {
|
||||
// #195: the `force` query has no effect when there are no dependents, so
|
||||
// the plain `service.delete` row is correct here. `service.force_delete`
|
||||
// is reserved for the path that actually had to clear dependent orders.
|
||||
const s = await pool.query(
|
||||
`INSERT INTO services (name_ar, name_en, price, is_available) VALUES ($1, $2, 1.50, true) RETURNING id`,
|
||||
[`${SVC_PREFIX}ForceCleanAr`, `${SVC_PREFIX}ForceClean`],
|
||||
);
|
||||
const id = s.rows[0].id;
|
||||
createdServiceIds.push(id);
|
||||
|
||||
const res = await fetch(`${API_BASE}/api/services/${id}?force=true`, {
|
||||
method: "DELETE",
|
||||
headers: { Cookie: adminCookie },
|
||||
});
|
||||
assert.equal(res.status, 204);
|
||||
|
||||
const audit = await pool.query(
|
||||
`SELECT action FROM audit_logs WHERE target_type = 'service' AND target_id = $1`,
|
||||
[String(id)],
|
||||
);
|
||||
assert.equal(audit.rowCount, 1);
|
||||
assert.equal(audit.rows[0].action, "service.delete");
|
||||
});
|
||||
|
||||
test("DELETE /api/services/:id with orders returns 409 with counts", async () => {
|
||||
|
||||
Reference in New Issue
Block a user