@actuate-media/cms-core 0.21.0 → 0.23.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (109) hide show
  1. package/dist/__tests__/actions/document-crud.test.js +74 -2
  2. package/dist/__tests__/actions/document-crud.test.js.map +1 -1
  3. package/dist/__tests__/api/cache-headers.test.d.ts +2 -0
  4. package/dist/__tests__/api/cache-headers.test.d.ts.map +1 -0
  5. package/dist/__tests__/api/cache-headers.test.js +135 -0
  6. package/dist/__tests__/api/cache-headers.test.js.map +1 -0
  7. package/dist/__tests__/api/collections-discovery.test.d.ts +2 -0
  8. package/dist/__tests__/api/collections-discovery.test.d.ts.map +1 -0
  9. package/dist/__tests__/api/collections-discovery.test.js +129 -0
  10. package/dist/__tests__/api/collections-discovery.test.js.map +1 -0
  11. package/dist/__tests__/api/comments-routes.test.d.ts +2 -0
  12. package/dist/__tests__/api/comments-routes.test.d.ts.map +1 -0
  13. package/dist/__tests__/api/comments-routes.test.js +266 -0
  14. package/dist/__tests__/api/comments-routes.test.js.map +1 -0
  15. package/dist/__tests__/api/notifications-routes.test.d.ts +2 -0
  16. package/dist/__tests__/api/notifications-routes.test.d.ts.map +1 -0
  17. package/dist/__tests__/api/notifications-routes.test.js +225 -0
  18. package/dist/__tests__/api/notifications-routes.test.js.map +1 -0
  19. package/dist/__tests__/api/preview-and-scheduling.test.js +53 -0
  20. package/dist/__tests__/api/preview-and-scheduling.test.js.map +1 -1
  21. package/dist/__tests__/fields/relations.test.js +62 -8
  22. package/dist/__tests__/fields/relations.test.js.map +1 -1
  23. package/dist/__tests__/preview-event-bus.test.d.ts +2 -0
  24. package/dist/__tests__/preview-event-bus.test.d.ts.map +1 -0
  25. package/dist/__tests__/preview-event-bus.test.js +89 -0
  26. package/dist/__tests__/preview-event-bus.test.js.map +1 -0
  27. package/dist/__tests__/preview-sse-stream.test.d.ts +2 -0
  28. package/dist/__tests__/preview-sse-stream.test.d.ts.map +1 -0
  29. package/dist/__tests__/preview-sse-stream.test.js +117 -0
  30. package/dist/__tests__/preview-sse-stream.test.js.map +1 -0
  31. package/dist/__tests__/realtime/comments.test.d.ts +2 -0
  32. package/dist/__tests__/realtime/comments.test.d.ts.map +1 -0
  33. package/dist/__tests__/realtime/comments.test.js +508 -0
  34. package/dist/__tests__/realtime/comments.test.js.map +1 -0
  35. package/dist/__tests__/realtime/gateway-ws-integration.test.d.ts +2 -0
  36. package/dist/__tests__/realtime/gateway-ws-integration.test.d.ts.map +1 -0
  37. package/dist/__tests__/realtime/gateway-ws-integration.test.js +170 -0
  38. package/dist/__tests__/realtime/gateway-ws-integration.test.js.map +1 -0
  39. package/dist/__tests__/realtime/gateway.test.d.ts +2 -0
  40. package/dist/__tests__/realtime/gateway.test.d.ts.map +1 -0
  41. package/dist/__tests__/realtime/gateway.test.js +402 -0
  42. package/dist/__tests__/realtime/gateway.test.js.map +1 -0
  43. package/dist/__tests__/realtime/notifications.test.d.ts +2 -0
  44. package/dist/__tests__/realtime/notifications.test.d.ts.map +1 -0
  45. package/dist/__tests__/realtime/notifications.test.js +317 -0
  46. package/dist/__tests__/realtime/notifications.test.js.map +1 -0
  47. package/dist/__tests__/realtime/persistence.test.d.ts +2 -0
  48. package/dist/__tests__/realtime/persistence.test.d.ts.map +1 -0
  49. package/dist/__tests__/realtime/persistence.test.js +124 -0
  50. package/dist/__tests__/realtime/persistence.test.js.map +1 -0
  51. package/dist/__tests__/schema-preview-features.test.d.ts +2 -0
  52. package/dist/__tests__/schema-preview-features.test.d.ts.map +1 -0
  53. package/dist/__tests__/schema-preview-features.test.js +42 -0
  54. package/dist/__tests__/schema-preview-features.test.js.map +1 -0
  55. package/dist/actions.d.ts.map +1 -1
  56. package/dist/actions.js +71 -26
  57. package/dist/actions.js.map +1 -1
  58. package/dist/api/handlers.d.ts.map +1 -1
  59. package/dist/api/handlers.js +772 -188
  60. package/dist/api/handlers.js.map +1 -1
  61. package/dist/cache/__tests__/http.test.d.ts +2 -0
  62. package/dist/cache/__tests__/http.test.d.ts.map +1 -0
  63. package/dist/cache/__tests__/http.test.js +285 -0
  64. package/dist/cache/__tests__/http.test.js.map +1 -0
  65. package/dist/cache/http.d.ts +119 -0
  66. package/dist/cache/http.d.ts.map +1 -0
  67. package/dist/cache/http.js +188 -0
  68. package/dist/cache/http.js.map +1 -0
  69. package/dist/config/types.d.ts +92 -0
  70. package/dist/config/types.d.ts.map +1 -1
  71. package/dist/fields/relations.d.ts.map +1 -1
  72. package/dist/fields/relations.js +43 -11
  73. package/dist/fields/relations.js.map +1 -1
  74. package/dist/index.d.ts +5 -0
  75. package/dist/index.d.ts.map +1 -1
  76. package/dist/index.js +2 -0
  77. package/dist/index.js.map +1 -1
  78. package/dist/page-builder/schema.d.ts +2 -2
  79. package/dist/preview/event-bus.d.ts +39 -0
  80. package/dist/preview/event-bus.d.ts.map +1 -0
  81. package/dist/preview/event-bus.js +73 -0
  82. package/dist/preview/event-bus.js.map +1 -0
  83. package/dist/preview/sse-stream.d.ts +50 -0
  84. package/dist/preview/sse-stream.d.ts.map +1 -0
  85. package/dist/preview/sse-stream.js +120 -0
  86. package/dist/preview/sse-stream.js.map +1 -0
  87. package/dist/realtime/comments.d.ts +217 -0
  88. package/dist/realtime/comments.d.ts.map +1 -0
  89. package/dist/realtime/comments.js +282 -0
  90. package/dist/realtime/comments.js.map +1 -0
  91. package/dist/realtime/gateway.d.ts +107 -0
  92. package/dist/realtime/gateway.d.ts.map +1 -0
  93. package/dist/realtime/gateway.js +215 -0
  94. package/dist/realtime/gateway.js.map +1 -0
  95. package/dist/realtime/index.d.ts +18 -0
  96. package/dist/realtime/index.d.ts.map +1 -0
  97. package/dist/realtime/index.js +18 -0
  98. package/dist/realtime/index.js.map +1 -0
  99. package/dist/realtime/notifications.d.ts +209 -0
  100. package/dist/realtime/notifications.d.ts.map +1 -0
  101. package/dist/realtime/notifications.js +209 -0
  102. package/dist/realtime/notifications.js.map +1 -0
  103. package/dist/realtime/persistence.d.ts +72 -0
  104. package/dist/realtime/persistence.d.ts.map +1 -0
  105. package/dist/realtime/persistence.js +53 -0
  106. package/dist/realtime/persistence.js.map +1 -0
  107. package/package.json +13 -4
  108. package/prisma/cms-schema.prisma +135 -4
  109. package/prisma/schema.prisma +17 -3
@@ -0,0 +1,266 @@
1
+ import { beforeEach, describe, expect, it } from 'vitest';
2
+ import { handleActuateAPI } from '../../api/index.js';
3
+ import { createSession } from '../../auth/session.js';
4
+ import { initDB } from '../../db.js';
5
+ import { generateToken as generateCsrfToken } from '../../security/csrf.js';
6
+ /**
7
+ * Integration tests for the comment REST endpoints introduced in slice 4
8
+ * of Phase 3. The point of these tests is to exercise the *handler* layer
9
+ * end-to-end (auth, CSRF, JSON envelopes, status code mapping) — the pure
10
+ * service logic is covered by `realtime/comments.test.ts`.
11
+ *
12
+ * We back the mock DB with a tiny in-memory implementation of the subset
13
+ * of Prisma we use, so the routes execute against real-ish data without a
14
+ * Postgres instance.
15
+ */
16
+ const SECRET = 'test-secret-for-comments-routes-1234567890abcd';
17
+ function createMockDB() {
18
+ const rows = new Map();
19
+ let counter = 0;
20
+ return {
21
+ rows,
22
+ session: {
23
+ findUnique: async () => ({ revokedAt: null }),
24
+ },
25
+ user: {},
26
+ media: {},
27
+ documentComment: {
28
+ async findUnique({ where }) {
29
+ return rows.get(where.id) ?? null;
30
+ },
31
+ async findMany({ where, orderBy, take, }) {
32
+ let r = Array.from(rows.values()).filter((row) => {
33
+ if (where.documentId !== undefined && row.documentId !== where.documentId)
34
+ return false;
35
+ if (Object.prototype.hasOwnProperty.call(where, 'resolvedAt') && row.resolvedAt !== null)
36
+ return false;
37
+ if (Object.prototype.hasOwnProperty.call(where, 'deletedAt') && row.deletedAt !== null)
38
+ return false;
39
+ return true;
40
+ });
41
+ if (orderBy?.createdAt === 'asc') {
42
+ r = r.sort((a, b) => {
43
+ const ax = a.createdAt instanceof Date ? a.createdAt.getTime() : 0;
44
+ const bx = b.createdAt instanceof Date ? b.createdAt.getTime() : 0;
45
+ return ax - bx;
46
+ });
47
+ }
48
+ if (typeof take === 'number')
49
+ r = r.slice(0, take);
50
+ return r;
51
+ },
52
+ async create({ data, }) {
53
+ counter += 1;
54
+ const now = new Date(Date.UTC(2026, 5, 1, 0, 0, counter));
55
+ const row = {
56
+ id: `c-${counter}`,
57
+ documentId: data.documentId,
58
+ parentId: data.parentId,
59
+ userId: data.userId,
60
+ body: data.body,
61
+ anchor: data.anchor,
62
+ resolvedAt: null,
63
+ resolvedById: null,
64
+ deletedAt: null,
65
+ createdAt: now,
66
+ updatedAt: now,
67
+ };
68
+ rows.set(row.id, row);
69
+ return row;
70
+ },
71
+ async update({ where, data }) {
72
+ const existing = rows.get(where.id);
73
+ if (!existing)
74
+ throw new Error(`row ${where.id} not found`);
75
+ const next = {
76
+ ...existing,
77
+ ...data,
78
+ updatedAt: new Date(),
79
+ };
80
+ rows.set(where.id, next);
81
+ return next;
82
+ },
83
+ },
84
+ auditLog: {
85
+ // logEvent does fire-and-forget writes; we just accept anything.
86
+ create: async () => ({}),
87
+ },
88
+ };
89
+ }
90
+ async function sessionHeaders(role, userId = 'user-1') {
91
+ const token = await createSession({ userId, role, sessionId: `session-${userId}` }, { secret: SECRET });
92
+ const csrf = await generateCsrfToken();
93
+ return {
94
+ cookie: `actuate_session=${token}; actuate_csrf=${csrf}`,
95
+ 'x-csrf-token': csrf,
96
+ 'content-type': 'application/json',
97
+ };
98
+ }
99
+ describe('comments REST endpoints (Phase 3 slice 4)', () => {
100
+ let db;
101
+ let handler;
102
+ beforeEach(() => {
103
+ process.env.CMS_SECRET = SECRET;
104
+ db = createMockDB();
105
+ initDB(db);
106
+ handler = handleActuateAPI({ prismaClient: db });
107
+ });
108
+ it('rejects unauthenticated POST', async () => {
109
+ const res = await handler(new Request('https://example.com/api/cms/documents/doc-1/comments', {
110
+ method: 'POST',
111
+ // No session cookie — auth should reject before CSRF runs.
112
+ headers: { 'content-type': 'application/json' },
113
+ body: JSON.stringify({ body: 'hi' }),
114
+ }));
115
+ // CSRF runs *before* auth, so an unauthenticated POST without a CSRF
116
+ // token returns 403 instead of 401. Either status is fine for this
117
+ // test — we just want to confirm anonymous writes are blocked.
118
+ expect([401, 403]).toContain(res.status);
119
+ });
120
+ it('requires write role for POST', async () => {
121
+ const res = await handler(new Request('https://example.com/api/cms/documents/doc-1/comments', {
122
+ method: 'POST',
123
+ headers: await sessionHeaders('VIEWER'),
124
+ body: JSON.stringify({ body: 'hi' }),
125
+ }));
126
+ expect(res.status).toBe(403);
127
+ });
128
+ it('creates a top-level comment for an editor', async () => {
129
+ const res = await handler(new Request('https://example.com/api/cms/documents/doc-1/comments', {
130
+ method: 'POST',
131
+ headers: await sessionHeaders('EDITOR'),
132
+ body: JSON.stringify({ body: 'Looks great!', anchor: { from: 'AQ==', to: 'Ag==' } }),
133
+ }));
134
+ expect(res.status).toBe(201);
135
+ const json = (await res.json());
136
+ expect(json.data.body).toBe('Looks great!');
137
+ expect(json.data.anchor).toEqual({ from: 'AQ==', to: 'Ag==' });
138
+ });
139
+ it('returns 400 on empty body', async () => {
140
+ const res = await handler(new Request('https://example.com/api/cms/documents/doc-1/comments', {
141
+ method: 'POST',
142
+ headers: await sessionHeaders('EDITOR'),
143
+ body: JSON.stringify({ body: ' ' }),
144
+ }));
145
+ expect(res.status).toBe(400);
146
+ });
147
+ it('lists active comments only by default', async () => {
148
+ const headers = await sessionHeaders('EDITOR');
149
+ await handler(new Request('https://example.com/api/cms/documents/doc-1/comments', {
150
+ method: 'POST',
151
+ headers,
152
+ body: JSON.stringify({ body: 'open one' }),
153
+ }));
154
+ const createRes = await handler(new Request('https://example.com/api/cms/documents/doc-1/comments', {
155
+ method: 'POST',
156
+ headers,
157
+ body: JSON.stringify({ body: 'will resolve' }),
158
+ }));
159
+ const created = (await createRes.json());
160
+ await handler(new Request(`https://example.com/api/cms/comments/${created.data.id}/resolve`, {
161
+ method: 'POST',
162
+ headers,
163
+ }));
164
+ const listRes = await handler(new Request('https://example.com/api/cms/documents/doc-1/comments', {
165
+ headers: { cookie: headers.cookie },
166
+ }));
167
+ expect(listRes.status).toBe(200);
168
+ const list = (await listRes.json());
169
+ expect(list.data.map((c) => c.body)).toEqual(['open one']);
170
+ const listAll = await handler(new Request('https://example.com/api/cms/documents/doc-1/comments?includeResolved=true', {
171
+ headers: { cookie: headers.cookie },
172
+ }));
173
+ const allBodies = (await listAll.json()).data.map((c) => c.body);
174
+ expect(allBodies).toContain('will resolve');
175
+ });
176
+ it('blocks includeDeleted for non-admins', async () => {
177
+ const res = await handler(new Request('https://example.com/api/cms/documents/doc-1/comments?includeDeleted=true', {
178
+ headers: { cookie: (await sessionHeaders('EDITOR')).cookie },
179
+ }));
180
+ expect(res.status).toBe(403);
181
+ });
182
+ it('lets admins request includeDeleted', async () => {
183
+ const res = await handler(new Request('https://example.com/api/cms/documents/doc-1/comments?includeDeleted=true', {
184
+ headers: { cookie: (await sessionHeaders('ADMIN')).cookie },
185
+ }));
186
+ expect(res.status).toBe(200);
187
+ });
188
+ it('allows the author to edit but blocks other editors', async () => {
189
+ const authorHeaders = await sessionHeaders('EDITOR', 'user-1');
190
+ const otherHeaders = await sessionHeaders('EDITOR', 'user-2');
191
+ const create = await handler(new Request('https://example.com/api/cms/documents/doc-1/comments', {
192
+ method: 'POST',
193
+ headers: authorHeaders,
194
+ body: JSON.stringify({ body: 'mine' }),
195
+ }));
196
+ const created = (await create.json());
197
+ const evil = await handler(new Request(`https://example.com/api/cms/comments/${created.data.id}`, {
198
+ method: 'PATCH',
199
+ headers: otherHeaders,
200
+ body: JSON.stringify({ body: 'tampered' }),
201
+ }));
202
+ expect(evil.status).toBe(403);
203
+ const good = await handler(new Request(`https://example.com/api/cms/comments/${created.data.id}`, {
204
+ method: 'PATCH',
205
+ headers: authorHeaders,
206
+ body: JSON.stringify({ body: 'fixed' }),
207
+ }));
208
+ expect(good.status).toBe(200);
209
+ const dto = (await good.json());
210
+ expect(dto.data.body).toBe('fixed');
211
+ });
212
+ it('resolves and re-opens a thread, then soft-deletes it', async () => {
213
+ const headers = await sessionHeaders('EDITOR');
214
+ const create = await handler(new Request('https://example.com/api/cms/documents/doc-1/comments', {
215
+ method: 'POST',
216
+ headers,
217
+ body: JSON.stringify({ body: 'thread' }),
218
+ }));
219
+ const { data } = (await create.json());
220
+ const resolved = await handler(new Request(`https://example.com/api/cms/comments/${data.id}/resolve`, {
221
+ method: 'POST',
222
+ headers,
223
+ }));
224
+ expect(resolved.status).toBe(200);
225
+ const resolvedDto = (await resolved.json());
226
+ expect(resolvedDto.data.isResolved).toBe(true);
227
+ const reopened = await handler(new Request(`https://example.com/api/cms/comments/${data.id}/reopen`, {
228
+ method: 'POST',
229
+ headers,
230
+ }));
231
+ expect(reopened.status).toBe(200);
232
+ const deleted = await handler(new Request(`https://example.com/api/cms/comments/${data.id}`, {
233
+ method: 'DELETE',
234
+ headers,
235
+ }));
236
+ expect(deleted.status).toBe(200);
237
+ const deletedDto = (await deleted.json());
238
+ expect(deletedDto.data.deletedAt).not.toBeNull();
239
+ });
240
+ it('returns 404 when patching a missing comment', async () => {
241
+ const res = await handler(new Request('https://example.com/api/cms/comments/does-not-exist', {
242
+ method: 'PATCH',
243
+ headers: await sessionHeaders('EDITOR'),
244
+ body: JSON.stringify({ body: 'ghost' }),
245
+ }));
246
+ expect(res.status).toBe(404);
247
+ });
248
+ it('threads a reply through the create endpoint', async () => {
249
+ const headers = await sessionHeaders('EDITOR');
250
+ const create = await handler(new Request('https://example.com/api/cms/documents/doc-1/comments', {
251
+ method: 'POST',
252
+ headers,
253
+ body: JSON.stringify({ body: 'root' }),
254
+ }));
255
+ const root = (await create.json()).data;
256
+ const reply = await handler(new Request('https://example.com/api/cms/documents/doc-1/comments', {
257
+ method: 'POST',
258
+ headers,
259
+ body: JSON.stringify({ body: 'reply', parentId: root.id }),
260
+ }));
261
+ expect(reply.status).toBe(201);
262
+ const replyDto = (await reply.json());
263
+ expect(replyDto.data.parentId).toBe(root.id);
264
+ });
265
+ });
266
+ //# sourceMappingURL=comments-routes.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"comments-routes.test.js","sourceRoot":"","sources":["../../../src/__tests__/api/comments-routes.test.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=notifications-routes.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"notifications-routes.test.d.ts","sourceRoot":"","sources":["../../../src/__tests__/api/notifications-routes.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,225 @@
1
+ import { beforeEach, describe, expect, it } from 'vitest';
2
+ import { handleActuateAPI } from '../../api/index.js';
3
+ import { createSession } from '../../auth/session.js';
4
+ import { initDB } from '../../db.js';
5
+ import { generateToken as generateCsrfToken } from '../../security/csrf.js';
6
+ /**
7
+ * Integration tests for the notifications REST endpoints introduced in
8
+ * slice 6 of Phase 3. The pure service is covered by
9
+ * `realtime/notifications.test.ts`; these tests exercise the handler
10
+ * layer end-to-end (auth, JSON envelopes, status codes) and verify
11
+ * that the bell only sees its own user's rows.
12
+ */
13
+ const SECRET = 'test-secret-for-notifications-routes-1234567890ab';
14
+ function createMockDB() {
15
+ const rows = new Map();
16
+ let counter = 0;
17
+ let clock = Date.UTC(2026, 5, 1, 0, 0, 0);
18
+ return {
19
+ rows,
20
+ session: { findUnique: async () => ({ revokedAt: null }) },
21
+ user: {},
22
+ media: {},
23
+ documentComment: {},
24
+ auditLog: { create: async () => ({}) },
25
+ documentNotification: {
26
+ async create({ data, }) {
27
+ counter += 1;
28
+ clock += 1000;
29
+ const row = {
30
+ id: `n-${counter}`,
31
+ userId: data.userId,
32
+ kind: data.kind,
33
+ documentId: data.documentId ?? null,
34
+ commentId: data.commentId ?? null,
35
+ payload: data.payload,
36
+ readAt: data.readAt ?? null,
37
+ createdAt: new Date(clock),
38
+ };
39
+ rows.set(row.id, row);
40
+ return row;
41
+ },
42
+ async findUnique({ where }) {
43
+ return rows.get(where.id) ?? null;
44
+ },
45
+ async findMany({ where, orderBy, take, }) {
46
+ let r = Array.from(rows.values()).filter((row) => {
47
+ if (row.userId !== where.userId)
48
+ return false;
49
+ if (Object.prototype.hasOwnProperty.call(where, 'readAt')) {
50
+ if (where.readAt === null && row.readAt !== null)
51
+ return false;
52
+ }
53
+ return true;
54
+ });
55
+ r.sort((a, b) => orderBy?.createdAt === 'asc'
56
+ ? a.createdAt.getTime() - b.createdAt.getTime()
57
+ : b.createdAt.getTime() - a.createdAt.getTime());
58
+ if (typeof take === 'number')
59
+ r = r.slice(0, take);
60
+ return r;
61
+ },
62
+ async update({ where, data }) {
63
+ const row = rows.get(where.id);
64
+ if (!row)
65
+ throw new Error(`row ${where.id} not found`);
66
+ if (data.readAt !== undefined)
67
+ row.readAt = data.readAt;
68
+ return row;
69
+ },
70
+ async updateMany({ where, data, }) {
71
+ let count = 0;
72
+ for (const row of rows.values()) {
73
+ if (row.userId !== where.userId)
74
+ continue;
75
+ if (where.readAt === null && row.readAt !== null)
76
+ continue;
77
+ row.readAt = data.readAt;
78
+ count += 1;
79
+ }
80
+ return { count };
81
+ },
82
+ async count({ where }) {
83
+ let n = 0;
84
+ for (const row of rows.values()) {
85
+ if (row.userId === where.userId && row.readAt === null)
86
+ n += 1;
87
+ }
88
+ return n;
89
+ },
90
+ },
91
+ };
92
+ }
93
+ async function sessionHeaders(role, userId = 'user-1') {
94
+ const token = await createSession({ userId, role, sessionId: `session-${userId}` }, { secret: SECRET });
95
+ const csrf = await generateCsrfToken();
96
+ return {
97
+ cookie: `actuate_session=${token}; actuate_csrf=${csrf}`,
98
+ 'x-csrf-token': csrf,
99
+ 'content-type': 'application/json',
100
+ };
101
+ }
102
+ async function seedNotification(db, userId, override = {}) {
103
+ return db.documentNotification.create({
104
+ data: {
105
+ userId,
106
+ kind: 'comment_reply',
107
+ documentId: 'doc-1',
108
+ commentId: 'c-1',
109
+ payload: { actorId: 'someone', actorName: 'Someone', excerpt: 'hi' },
110
+ readAt: null,
111
+ ...override,
112
+ },
113
+ });
114
+ }
115
+ describe('notifications REST endpoints (Phase 3 slice 6)', () => {
116
+ let db;
117
+ let handler;
118
+ beforeEach(() => {
119
+ process.env.CMS_SECRET = SECRET;
120
+ db = createMockDB();
121
+ initDB(db);
122
+ handler = handleActuateAPI({ prismaClient: db });
123
+ });
124
+ it('rejects unauthenticated GET', async () => {
125
+ const res = await handler(new Request('https://example.com/api/cms/notifications'));
126
+ expect(res.status).toBe(401);
127
+ });
128
+ it('lists only the current user notifications', async () => {
129
+ await seedNotification(db, 'user-1', {
130
+ payload: { actorId: 'a', actorName: 'A', excerpt: 'mine' },
131
+ });
132
+ await seedNotification(db, 'user-2', {
133
+ payload: { actorId: 'a', actorName: 'A', excerpt: 'theirs' },
134
+ });
135
+ const headers = await sessionHeaders('EDITOR', 'user-1');
136
+ const res = await handler(new Request('https://example.com/api/cms/notifications', {
137
+ headers: { cookie: headers.cookie },
138
+ }));
139
+ expect(res.status).toBe(200);
140
+ const body = (await res.json());
141
+ expect(body.data).toHaveLength(1);
142
+ expect(body.data[0].payload.excerpt).toBe('mine');
143
+ });
144
+ it('returns the unread badge count', async () => {
145
+ await seedNotification(db, 'user-1', {
146
+ payload: { actorId: 'a', actorName: 'A', excerpt: 'a' },
147
+ });
148
+ await seedNotification(db, 'user-1', {
149
+ payload: { actorId: 'a', actorName: 'A', excerpt: 'b' },
150
+ readAt: new Date(),
151
+ });
152
+ const headers = await sessionHeaders('EDITOR', 'user-1');
153
+ const res = await handler(new Request('https://example.com/api/cms/notifications/unread-count', {
154
+ headers: { cookie: headers.cookie },
155
+ }));
156
+ expect(res.status).toBe(200);
157
+ const body = (await res.json());
158
+ expect(body.data.count).toBe(1);
159
+ });
160
+ it('marks a single notification read (idempotent)', async () => {
161
+ const n = await seedNotification(db, 'user-1');
162
+ const headers = await sessionHeaders('EDITOR', 'user-1');
163
+ const first = await handler(new Request(`https://example.com/api/cms/notifications/${n.id}/read`, {
164
+ method: 'POST',
165
+ headers,
166
+ }));
167
+ expect(first.status).toBe(200);
168
+ const firstBody = (await first.json());
169
+ expect(firstBody.data.readAt).not.toBeNull();
170
+ // Second mark — still 200 with the same readAt.
171
+ const second = await handler(new Request(`https://example.com/api/cms/notifications/${n.id}/read`, {
172
+ method: 'POST',
173
+ headers,
174
+ }));
175
+ expect(second.status).toBe(200);
176
+ });
177
+ it("forbids marking another user's notification", async () => {
178
+ const n = await seedNotification(db, 'user-1');
179
+ const headers = await sessionHeaders('EDITOR', 'user-2');
180
+ const res = await handler(new Request(`https://example.com/api/cms/notifications/${n.id}/read`, {
181
+ method: 'POST',
182
+ headers,
183
+ }));
184
+ expect(res.status).toBe(403);
185
+ });
186
+ it('marks all unread notifications read in one shot', async () => {
187
+ for (let i = 0; i < 3; i++) {
188
+ await seedNotification(db, 'user-1', {
189
+ payload: { actorId: 'a', actorName: 'A', excerpt: String(i) },
190
+ });
191
+ }
192
+ const headers = await sessionHeaders('EDITOR', 'user-1');
193
+ const res = await handler(new Request('https://example.com/api/cms/notifications/read-all', {
194
+ method: 'POST',
195
+ headers,
196
+ }));
197
+ expect(res.status).toBe(200);
198
+ const body = (await res.json());
199
+ expect(body.data.count).toBe(3);
200
+ const after = await handler(new Request('https://example.com/api/cms/notifications/unread-count', {
201
+ headers: { cookie: headers.cookie },
202
+ }));
203
+ expect((await after.json()).data.count).toBe(0);
204
+ });
205
+ it('honours includeRead=true', async () => {
206
+ await seedNotification(db, 'user-1', {
207
+ payload: { actorId: 'a', actorName: 'A', excerpt: 'unread' },
208
+ });
209
+ await seedNotification(db, 'user-1', {
210
+ payload: { actorId: 'a', actorName: 'A', excerpt: 'read' },
211
+ readAt: new Date(),
212
+ });
213
+ const headers = await sessionHeaders('EDITOR', 'user-1');
214
+ const onlyUnread = await handler(new Request('https://example.com/api/cms/notifications', {
215
+ headers: { cookie: headers.cookie },
216
+ }));
217
+ expect((await onlyUnread.json()).data.map((n) => n.payload.excerpt)).toEqual(['unread']);
218
+ const all = await handler(new Request('https://example.com/api/cms/notifications?includeRead=true', {
219
+ headers: { cookie: headers.cookie },
220
+ }));
221
+ const allExcerpts = (await all.json()).data.map((n) => n.payload.excerpt);
222
+ expect(allExcerpts).toEqual(expect.arrayContaining(['unread', 'read']));
223
+ });
224
+ });
225
+ //# sourceMappingURL=notifications-routes.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"notifications-routes.test.js","sourceRoot":"","sources":["../../../src/__tests__/api/notifications-routes.test.ts"],"names":[],"mappings":"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"}
@@ -254,6 +254,59 @@ describe('GET /resolve?preview=<token>', () => {
254
254
  expect(response.status).toBe(404);
255
255
  });
256
256
  });
257
+ describe('GET /preview/stream', () => {
258
+ it('rejects when no token is supplied', async () => {
259
+ const db = createMockDB({ docs: [] });
260
+ initDB(db);
261
+ const handler = handleActuateAPI({ prismaClient: db, config: cmsConfig });
262
+ const response = await handler(new Request('https://example.com/api/cms/preview/stream', {
263
+ headers: { Accept: 'text/event-stream' },
264
+ }));
265
+ expect(response.status).toBe(401);
266
+ });
267
+ it('rejects an invalid token', async () => {
268
+ const db = createMockDB({ docs: [] });
269
+ initDB(db);
270
+ const handler = handleActuateAPI({ prismaClient: db, config: cmsConfig });
271
+ const response = await handler(new Request('https://example.com/api/cms/preview/stream?token=garbage', {
272
+ headers: { Accept: 'text/event-stream' },
273
+ }));
274
+ expect(response.status).toBe(401);
275
+ });
276
+ it('opens an SSE stream and emits a ready event for a valid token', async () => {
277
+ const { key, record } = await makeKey({ admin: true });
278
+ const docs = [
279
+ {
280
+ id: 'doc-1',
281
+ collection: 'posts',
282
+ slug: 'live',
283
+ status: 'DRAFT',
284
+ data: { title: 'Live' },
285
+ publishedAt: null,
286
+ },
287
+ ];
288
+ const db = createMockDB({ apiKey: record, docs });
289
+ initDB(db);
290
+ const handler = handleActuateAPI({ prismaClient: db, config: cmsConfig });
291
+ const tokenRes = await handler(new Request('https://example.com/api/cms/preview/token', {
292
+ method: 'POST',
293
+ headers: { Authorization: `Bearer ${key}`, 'Content-Type': 'application/json' },
294
+ body: JSON.stringify({ collection: 'posts', documentId: 'doc-1' }),
295
+ }));
296
+ const token = (await tokenRes.json()).data.token;
297
+ const streamRes = await handler(new Request(`https://example.com/api/cms/preview/stream?token=${encodeURIComponent(token)}`, {
298
+ headers: { Accept: 'text/event-stream' },
299
+ }));
300
+ expect(streamRes.status).toBe(200);
301
+ expect(streamRes.headers.get('Content-Type')).toMatch(/text\/event-stream/);
302
+ const reader = streamRes.body?.getReader();
303
+ expect(reader).toBeDefined();
304
+ const { value } = await reader.read();
305
+ expect(value).toBeDefined();
306
+ expect(new TextDecoder().decode(value)).toContain('event: ready');
307
+ await reader.cancel().catch(() => undefined);
308
+ });
309
+ });
257
310
  describe('POST /collections/:slug/:id/schedule', () => {
258
311
  it('sets scheduledAt and flips status to SCHEDULED', async () => {
259
312
  const { key, record } = await makeKey({ admin: true });