/** * ICD Test Runner — CRUD: Notifications * Full preference lifecycle + notification endpoint error paths. */ (function () { 'use strict'; var T = window.ICD; if (!T) return; if (!T.crud) T.crud = {}; T.crud.notifications = async function (testTag) { // ── Preference: Set ── await T.test('crud', 'notifications', 'PUT /notifications/preferences/:type (set)', async function () { var d = await T.apiPut('/notifications/preferences/kb.ready', { in_app: true, email: true }); T.assert(typeof d === 'object', 'expected object'); T.assert(d.type === 'kb.ready', 'type should be kb.ready, got: ' + d.type); T.assert(d.in_app === true, 'in_app should be true'); T.assert(d.email === true, 'email should be true'); }); // ── Preference: Read back via list ── await T.test('crud', 'notifications', 'GET /notifications/preferences (round-trip)', async function () { var d = await T.apiGet('/notifications/preferences'); T.assertHasKey(d, 'data', 'preferences envelope'); T.assert(Array.isArray(d.data), 'data should be array'); var found = d.data.find(function (p) { return p.type === 'kb.ready'; }); T.assert(found, 'kb.ready preference should be in list'); T.assert(found.in_app === true, 'in_app should be true'); T.assert(found.email === true, 'email should be true'); }); // ── Preference: Partial update ── await T.test('crud', 'notifications', 'PUT /notifications/preferences/:type (partial update)', async function () { var d = await T.apiPut('/notifications/preferences/kb.ready', { email: false }); T.assert(d.in_app === true, 'in_app should remain true after partial update'); T.assert(d.email === false, 'email should be false after partial update'); }); // ── Preference: Wildcard ── await T.test('crud', 'notifications', 'PUT /notifications/preferences/* (wildcard)', async function () { var d = await T.apiPut('/notifications/preferences/*', { in_app: false, email: true }); T.assert(d.type === '*', 'type should be *, got: ' + d.type); T.assert(d.in_app === false, 'in_app should be false'); T.assert(d.email === true, 'email should be true'); }); // ── Preference: Delete + verify removed ── await T.test('crud', 'notifications', 'DELETE /notifications/preferences/:type (reset)', async function () { await T.safeDelete('/notifications/preferences/kb.ready'); var d = await T.apiGet('/notifications/preferences'); var found = (d.data || []).find(function (p) { return p.type === 'kb.ready'; }); T.assert(!found, 'kb.ready should be removed after delete'); }); // ── Preference: Idempotent delete ── await T.test('crud', 'notifications', 'DELETE /notifications/preferences/:type (idempotent)', async function () { await T.safeDelete('/notifications/preferences/nonexistent.type'); }); // ── Cleanup wildcard preference ── await T.test('crud', 'notifications', 'DELETE /notifications/preferences/* (cleanup)', async function () { await T.safeDelete('/notifications/preferences/*'); }); // ── Notification list: envelope shape ── await T.test('crud', 'notifications', 'GET /notifications (envelope shape)', async function () { var d = await T.apiGet('/notifications'); T.assertHasKey(d, 'data', 'notifications envelope'); T.assertHasKey(d, 'total', 'notifications total'); T.assertHasKey(d, 'limit', 'notifications limit'); T.assertHasKey(d, 'offset', 'notifications offset'); T.assert(Array.isArray(d.data), 'data should be array'); T.assert(typeof d.total === 'number', 'total should be number'); T.assert(typeof d.limit === 'number', 'limit should be number'); T.assert(typeof d.offset === 'number', 'offset should be number'); }); // ── Notification list: pagination params ── await T.test('crud', 'notifications', 'GET /notifications?limit=2 (pagination)', async function () { var d = await T.apiGet('/notifications?limit=2&offset=0'); T.assert(d.limit === 2, 'limit should be 2, got: ' + d.limit); T.assert(d.offset === 0, 'offset should be 0, got: ' + d.offset); }); // ── Notification list: unread_only filter ── await T.test('crud', 'notifications', 'GET /notifications?unread_only=true', async function () { var d = await T.apiGet('/notifications?unread_only=true'); T.assertHasKey(d, 'data', 'unread envelope'); T.assert(Array.isArray(d.data), 'unread data should be array'); }); // ── Unread count: shape ── await T.test('crud', 'notifications', 'GET /notifications/unread-count (shape)', async function () { var d = await T.apiGet('/notifications/unread-count'); T.assertHasKey(d, 'count', 'unread-count'); T.assert(typeof d.count === 'number', 'count should be number'); }); // ── Mark all read: no-op on empty ── await T.test('crud', 'notifications', 'POST /notifications/mark-all-read (no-op)', async function () { var d = await T.apiPost('/notifications/mark-all-read'); T.assertHasKey(d, 'ok', 'mark-all-read'); T.assert(d.ok === true, 'ok should be true'); }); // ── Mark read: non-existent → 404 ── await T.test('crud', 'notifications', 'PATCH /notifications/:id/read (not found → 404)', async function () { var token = await T.getAuthToken(); var d = await T.authFetch(token, 'PATCH', '/notifications/00000000-0000-0000-0000-000000000000/read'); T.assertStatus(d, 404, 'mark read non-existent'); }); // ── Delete: non-existent → 404 ── await T.test('crud', 'notifications', 'DELETE /notifications/:id (not found → 404)', async function () { var token = await T.getAuthToken(); var d = await T.authFetch(token, 'DELETE', '/notifications/00000000-0000-0000-0000-000000000000'); T.assertStatus(d, 404, 'delete non-existent'); }); // ── Admin broadcast (v0.28.6) ── await T.test('crud', 'notifications', 'POST /admin/notifications/broadcast', async function () { var d = await T.apiPost('/admin/notifications/broadcast', { title: 'ICD Test Broadcast', message: 'Automated test broadcast.', level: 'info' }); T.assertHasKey(d, 'message', 'broadcast response'); T.assertHasKey(d, 'count', 'broadcast response'); T.assert(d.count >= 1, 'count should be >= 1'); }); await T.test('crud', 'notifications', 'POST /admin/notifications/broadcast (missing title → 400)', async function () { var token = await T.getAuthToken(); var d = await T.authFetch(token, 'POST', '/admin/notifications/broadcast', { message: 'no title' }); T.assertStatus(d, 400, 'missing title'); }); await T.test('crud', 'notifications', 'POST /admin/notifications/broadcast (invalid level → 400)', async function () { var token = await T.getAuthToken(); var d = await T.authFetch(token, 'POST', '/admin/notifications/broadcast', { title: 'x', message: 'x', level: 'extreme' }); T.assertStatus(d, 400, 'invalid level'); }); }; })();