@terreno/announcements 57.4.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.
- package/LICENSE +21 -0
- package/README.md +50 -0
- package/biome.jsonc +20 -0
- package/bunfig.toml +2 -0
- package/dist/announcementsApp.d.ts +9 -0
- package/dist/announcementsApp.js +820 -0
- package/dist/help.d.ts +30 -0
- package/dist/help.js +72 -0
- package/dist/helpRoutes.d.ts +8 -0
- package/dist/helpRoutes.js +233 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.js +34 -0
- package/dist/models/announcement.d.ts +3 -0
- package/dist/models/announcement.js +214 -0
- package/dist/models/announcementAcknowledgement.d.ts +2 -0
- package/dist/models/announcementAcknowledgement.js +41 -0
- package/dist/models/announcementClickEvent.d.ts +3 -0
- package/dist/models/announcementClickEvent.js +56 -0
- package/dist/models/announcementImpression.d.ts +3 -0
- package/dist/models/announcementImpression.js +50 -0
- package/dist/overview.d.ts +14 -0
- package/dist/overview.js +310 -0
- package/dist/pending.d.ts +64 -0
- package/dist/pending.js +242 -0
- package/dist/types.d.ts +135 -0
- package/dist/types.js +2 -0
- package/package.json +57 -0
- package/src/announcementsApp.ts +722 -0
- package/src/help.ts +98 -0
- package/src/helpRoutes.ts +160 -0
- package/src/index.ts +45 -0
- package/src/models/announcement.ts +189 -0
- package/src/models/announcementAcknowledgement.ts +50 -0
- package/src/models/announcementClickEvent.ts +68 -0
- package/src/models/announcementImpression.ts +62 -0
- package/src/overview.ts +228 -0
- package/src/pending.ts +286 -0
- package/src/tests/announcementsApp.test.ts +844 -0
- package/src/tests/bunSetup.ts +9 -0
- package/src/tests/help.test.ts +29 -0
- package/src/tests/helpRoutes.test.ts +291 -0
- package/src/tests/index.test.ts +57 -0
- package/src/tests/overview.test.ts +351 -0
- package/src/tests/pending.test.ts +262 -0
- package/src/types.ts +157 -0
- package/tsconfig.json +33 -0
|
@@ -0,0 +1,351 @@
|
|
|
1
|
+
import {afterEach, beforeEach, describe, it} from "bun:test";
|
|
2
|
+
import {
|
|
3
|
+
addAuthRoutes,
|
|
4
|
+
apiErrorMiddleware,
|
|
5
|
+
apiUnauthorizedMiddleware,
|
|
6
|
+
setupAuth,
|
|
7
|
+
type UserModel as UserModelType,
|
|
8
|
+
} from "@terreno/api";
|
|
9
|
+
import {authAsUser, getBaseServer, setupDb, UserModel} from "@terreno/api/testing";
|
|
10
|
+
import {assert} from "chai";
|
|
11
|
+
import type express from "express";
|
|
12
|
+
import {DateTime} from "luxon";
|
|
13
|
+
import supertest from "supertest";
|
|
14
|
+
import type TestAgent from "supertest/lib/agent";
|
|
15
|
+
import {AnnouncementsApp} from "../announcementsApp";
|
|
16
|
+
import {Announcement} from "../models/announcement";
|
|
17
|
+
import {AnnouncementAcknowledgement} from "../models/announcementAcknowledgement";
|
|
18
|
+
import {AnnouncementClickEvent} from "../models/announcementClickEvent";
|
|
19
|
+
import {AnnouncementImpression} from "../models/announcementImpression";
|
|
20
|
+
import type {AnnouncementsOptions} from "../types";
|
|
21
|
+
|
|
22
|
+
const buildApp = (options?: AnnouncementsOptions): express.Application => {
|
|
23
|
+
const app = getBaseServer();
|
|
24
|
+
setupAuth(app, UserModel as unknown as UserModelType);
|
|
25
|
+
addAuthRoutes(app, UserModel as unknown as UserModelType);
|
|
26
|
+
new AnnouncementsApp(options).register(app);
|
|
27
|
+
app.use(apiUnauthorizedMiddleware);
|
|
28
|
+
app.use(apiErrorMiddleware);
|
|
29
|
+
return app;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
describe("GET /announcements/overview", () => {
|
|
33
|
+
let app: express.Application;
|
|
34
|
+
let adminAgent: TestAgent;
|
|
35
|
+
let userAgent: TestAgent;
|
|
36
|
+
let userId: string;
|
|
37
|
+
|
|
38
|
+
beforeEach(async () => {
|
|
39
|
+
const [, notAdminUser] = await setupDb();
|
|
40
|
+
userId = notAdminUser._id.toString();
|
|
41
|
+
await Announcement.deleteMany({});
|
|
42
|
+
await AnnouncementAcknowledgement.deleteMany({});
|
|
43
|
+
await AnnouncementImpression.deleteMany({});
|
|
44
|
+
await AnnouncementClickEvent.deleteMany({});
|
|
45
|
+
|
|
46
|
+
app = buildApp({defaultAcknowledgementPolicy: "dismiss-only"});
|
|
47
|
+
adminAgent = await authAsUser(app, "admin");
|
|
48
|
+
userAgent = await authAsUser(app, "notAdmin");
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
afterEach(async () => {
|
|
52
|
+
await Announcement.deleteMany({});
|
|
53
|
+
await AnnouncementAcknowledgement.deleteMany({});
|
|
54
|
+
await AnnouncementImpression.deleteMany({});
|
|
55
|
+
await AnnouncementClickEvent.deleteMany({});
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it("returns exact per-row metrics and totals across announcements and event versions", async () => {
|
|
59
|
+
const highPriority = await Announcement.create({
|
|
60
|
+
acknowledgementPolicy: "required",
|
|
61
|
+
audienceType: "staff",
|
|
62
|
+
body: "High priority body",
|
|
63
|
+
displayMode: "modal",
|
|
64
|
+
expiresAt: DateTime.utc().plus({days: 30}).toJSDate(),
|
|
65
|
+
priority: 10,
|
|
66
|
+
publishedAt: DateTime.utc().minus({hours: 1}).toJSDate(),
|
|
67
|
+
status: "published",
|
|
68
|
+
title: "High priority",
|
|
69
|
+
version: 2,
|
|
70
|
+
});
|
|
71
|
+
const lowPriority = await Announcement.create({
|
|
72
|
+
body: "Draft body",
|
|
73
|
+
displayMode: "banner",
|
|
74
|
+
priority: 1,
|
|
75
|
+
status: "draft",
|
|
76
|
+
title: "Draft item",
|
|
77
|
+
version: 1,
|
|
78
|
+
});
|
|
79
|
+
const archived = await Announcement.create({
|
|
80
|
+
body: "Archived body",
|
|
81
|
+
displayMode: "feed",
|
|
82
|
+
priority: 5,
|
|
83
|
+
publishedAt: DateTime.utc().minus({days: 10}).toJSDate(),
|
|
84
|
+
status: "archived",
|
|
85
|
+
title: "Archived item",
|
|
86
|
+
version: 3,
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
await AnnouncementImpression.create([
|
|
90
|
+
{
|
|
91
|
+
announcementId: highPriority._id,
|
|
92
|
+
platform: "web",
|
|
93
|
+
userId,
|
|
94
|
+
version: 1,
|
|
95
|
+
viewedAt: DateTime.utc().toJSDate(),
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
announcementId: highPriority._id,
|
|
99
|
+
platform: "web",
|
|
100
|
+
userId,
|
|
101
|
+
version: 2,
|
|
102
|
+
viewedAt: DateTime.utc().toJSDate(),
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
announcementId: archived._id,
|
|
106
|
+
platform: "ios",
|
|
107
|
+
userId,
|
|
108
|
+
version: 3,
|
|
109
|
+
viewedAt: DateTime.utc().toJSDate(),
|
|
110
|
+
},
|
|
111
|
+
]);
|
|
112
|
+
await AnnouncementAcknowledgement.create([
|
|
113
|
+
{
|
|
114
|
+
acknowledgedAt: DateTime.utc().toJSDate(),
|
|
115
|
+
announcementId: highPriority._id,
|
|
116
|
+
userId,
|
|
117
|
+
version: 1,
|
|
118
|
+
},
|
|
119
|
+
{
|
|
120
|
+
acknowledgedAt: DateTime.utc().toJSDate(),
|
|
121
|
+
announcementId: highPriority._id,
|
|
122
|
+
userId,
|
|
123
|
+
version: 2,
|
|
124
|
+
},
|
|
125
|
+
]);
|
|
126
|
+
await AnnouncementClickEvent.create([
|
|
127
|
+
{
|
|
128
|
+
action: "primaryAction",
|
|
129
|
+
announcementId: highPriority._id,
|
|
130
|
+
clickedAt: DateTime.utc().toJSDate(),
|
|
131
|
+
platform: "web",
|
|
132
|
+
userId,
|
|
133
|
+
version: 2,
|
|
134
|
+
},
|
|
135
|
+
{
|
|
136
|
+
action: "primaryAction",
|
|
137
|
+
announcementId: archived._id,
|
|
138
|
+
clickedAt: DateTime.utc().toJSDate(),
|
|
139
|
+
platform: "web",
|
|
140
|
+
userId,
|
|
141
|
+
version: 3,
|
|
142
|
+
},
|
|
143
|
+
{
|
|
144
|
+
action: "primaryAction",
|
|
145
|
+
announcementId: archived._id,
|
|
146
|
+
clickedAt: DateTime.utc().toJSDate(),
|
|
147
|
+
platform: "web",
|
|
148
|
+
userId,
|
|
149
|
+
version: 3,
|
|
150
|
+
},
|
|
151
|
+
]);
|
|
152
|
+
|
|
153
|
+
const res = await adminAgent.get("/announcements/overview?page=1&limit=20").expect(200);
|
|
154
|
+
|
|
155
|
+
assert.strictEqual(res.body.page, 1);
|
|
156
|
+
assert.strictEqual(res.body.limit, 20);
|
|
157
|
+
assert.strictEqual(res.body.total, 3);
|
|
158
|
+
assert.strictEqual(res.body.more, false);
|
|
159
|
+
|
|
160
|
+
assert.strictEqual(res.body.totals.announcements, 3);
|
|
161
|
+
assert.strictEqual(res.body.totals.published, 1);
|
|
162
|
+
assert.strictEqual(res.body.totals.draft, 1);
|
|
163
|
+
assert.strictEqual(res.body.totals.archived, 1);
|
|
164
|
+
assert.strictEqual(res.body.totals.impressions, 3);
|
|
165
|
+
assert.strictEqual(res.body.totals.acknowledgements, 2);
|
|
166
|
+
assert.strictEqual(res.body.totals.clicks, 3);
|
|
167
|
+
|
|
168
|
+
assert.lengthOf(res.body.data, 3);
|
|
169
|
+
assert.strictEqual(res.body.data[0]._id, highPriority._id.toString());
|
|
170
|
+
assert.strictEqual(res.body.data[0].title, "High priority");
|
|
171
|
+
assert.strictEqual(res.body.data[0].status, "published");
|
|
172
|
+
assert.strictEqual(res.body.data[0].displayMode, "modal");
|
|
173
|
+
assert.strictEqual(res.body.data[0].audienceType, "staff");
|
|
174
|
+
assert.strictEqual(res.body.data[0].acknowledgementPolicy, "required");
|
|
175
|
+
assert.strictEqual(res.body.data[0].priority, 10);
|
|
176
|
+
assert.strictEqual(res.body.data[0].version, 2);
|
|
177
|
+
assert.isOk(res.body.data[0].publishedAt);
|
|
178
|
+
assert.isOk(res.body.data[0].expiresAt);
|
|
179
|
+
assert.deepStrictEqual(res.body.data[0].metrics, {
|
|
180
|
+
acknowledgements: 2,
|
|
181
|
+
clicks: 1,
|
|
182
|
+
impressions: 2,
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
const draftRow = res.body.data.find(
|
|
186
|
+
(row: {_id: string}) => row._id === lowPriority._id.toString()
|
|
187
|
+
);
|
|
188
|
+
assert.isOk(draftRow);
|
|
189
|
+
assert.strictEqual(draftRow.displayMode, "banner");
|
|
190
|
+
assert.strictEqual(draftRow.status, "draft");
|
|
191
|
+
assert.isUndefined(draftRow.publishedAt);
|
|
192
|
+
assert.deepStrictEqual(draftRow.metrics, {
|
|
193
|
+
acknowledgements: 0,
|
|
194
|
+
clicks: 0,
|
|
195
|
+
impressions: 0,
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
const archivedRow = res.body.data.find(
|
|
199
|
+
(row: {_id: string}) => row._id === archived._id.toString()
|
|
200
|
+
);
|
|
201
|
+
assert.isOk(archivedRow);
|
|
202
|
+
assert.strictEqual(archivedRow.displayMode, "feed");
|
|
203
|
+
assert.strictEqual(archivedRow.status, "archived");
|
|
204
|
+
assert.deepStrictEqual(archivedRow.metrics, {
|
|
205
|
+
acknowledgements: 0,
|
|
206
|
+
clicks: 2,
|
|
207
|
+
impressions: 1,
|
|
208
|
+
});
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
it("paginates deterministically by priority desc then publishedAt desc", async () => {
|
|
212
|
+
await Announcement.create({
|
|
213
|
+
body: "A",
|
|
214
|
+
priority: 5,
|
|
215
|
+
publishedAt: DateTime.utc().minus({days: 2}).toJSDate(),
|
|
216
|
+
status: "published",
|
|
217
|
+
title: "Older high",
|
|
218
|
+
version: 1,
|
|
219
|
+
});
|
|
220
|
+
await Announcement.create({
|
|
221
|
+
body: "B",
|
|
222
|
+
priority: 5,
|
|
223
|
+
publishedAt: DateTime.utc().minus({days: 1}).toJSDate(),
|
|
224
|
+
status: "published",
|
|
225
|
+
title: "Newer high",
|
|
226
|
+
version: 1,
|
|
227
|
+
});
|
|
228
|
+
await Announcement.create({
|
|
229
|
+
body: "C",
|
|
230
|
+
priority: 1,
|
|
231
|
+
publishedAt: DateTime.utc().minus({hours: 1}).toJSDate(),
|
|
232
|
+
status: "published",
|
|
233
|
+
title: "Low priority",
|
|
234
|
+
version: 1,
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
const pageOne = await adminAgent.get("/announcements/overview?page=1&limit=2").expect(200);
|
|
238
|
+
assert.strictEqual(pageOne.body.total, 3);
|
|
239
|
+
assert.strictEqual(pageOne.body.more, true);
|
|
240
|
+
assert.lengthOf(pageOne.body.data, 2);
|
|
241
|
+
assert.strictEqual(pageOne.body.data[0].title, "Newer high");
|
|
242
|
+
assert.strictEqual(pageOne.body.data[1].title, "Older high");
|
|
243
|
+
|
|
244
|
+
const pageTwo = await adminAgent.get("/announcements/overview?page=2&limit=2").expect(200);
|
|
245
|
+
assert.strictEqual(pageTwo.body.more, false);
|
|
246
|
+
assert.lengthOf(pageTwo.body.data, 1);
|
|
247
|
+
assert.strictEqual(pageTwo.body.data[0].title, "Low priority");
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
it("defaults limit to 20, caps at 100, and returns an empty overview", async () => {
|
|
251
|
+
const defaultRes = await adminAgent.get("/announcements/overview").expect(200);
|
|
252
|
+
assert.strictEqual(defaultRes.body.limit, 20);
|
|
253
|
+
assert.strictEqual(defaultRes.body.page, 1);
|
|
254
|
+
assert.strictEqual(defaultRes.body.total, 0);
|
|
255
|
+
assert.strictEqual(defaultRes.body.more, false);
|
|
256
|
+
assert.lengthOf(defaultRes.body.data, 0);
|
|
257
|
+
assert.deepStrictEqual(defaultRes.body.totals, {
|
|
258
|
+
acknowledgements: 0,
|
|
259
|
+
announcements: 0,
|
|
260
|
+
archived: 0,
|
|
261
|
+
clicks: 0,
|
|
262
|
+
draft: 0,
|
|
263
|
+
impressions: 0,
|
|
264
|
+
published: 0,
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
const cappedRes = await adminAgent.get("/announcements/overview?page=0&limit=500").expect(200);
|
|
268
|
+
assert.strictEqual(cappedRes.body.limit, 100);
|
|
269
|
+
assert.strictEqual(cappedRes.body.page, 1);
|
|
270
|
+
|
|
271
|
+
const invalidRes = await adminAgent
|
|
272
|
+
.get("/announcements/overview?page=not-a-number&limit=also-invalid")
|
|
273
|
+
.expect(200);
|
|
274
|
+
assert.strictEqual(invalidRes.body.limit, 20);
|
|
275
|
+
assert.strictEqual(invalidRes.body.page, 1);
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
it("excludes soft-deleted announcements and event rows from totals and metrics", async () => {
|
|
279
|
+
const live = await Announcement.create({
|
|
280
|
+
body: "Live",
|
|
281
|
+
publishedAt: DateTime.utc().toJSDate(),
|
|
282
|
+
status: "published",
|
|
283
|
+
title: "Live",
|
|
284
|
+
version: 1,
|
|
285
|
+
});
|
|
286
|
+
const deletedAnnouncement = await Announcement.create({
|
|
287
|
+
body: "Deleted",
|
|
288
|
+
publishedAt: DateTime.utc().toJSDate(),
|
|
289
|
+
status: "published",
|
|
290
|
+
title: "Deleted",
|
|
291
|
+
version: 1,
|
|
292
|
+
});
|
|
293
|
+
await Announcement.updateOne({_id: deletedAnnouncement._id}, {deleted: true});
|
|
294
|
+
|
|
295
|
+
await AnnouncementImpression.create({
|
|
296
|
+
announcementId: live._id,
|
|
297
|
+
platform: "web",
|
|
298
|
+
userId,
|
|
299
|
+
version: 1,
|
|
300
|
+
viewedAt: DateTime.utc().toJSDate(),
|
|
301
|
+
});
|
|
302
|
+
const deletedImpression = await AnnouncementImpression.create({
|
|
303
|
+
announcementId: live._id,
|
|
304
|
+
platform: "web",
|
|
305
|
+
userId,
|
|
306
|
+
version: 1,
|
|
307
|
+
viewedAt: DateTime.utc().toJSDate(),
|
|
308
|
+
});
|
|
309
|
+
await AnnouncementImpression.updateOne({_id: deletedImpression._id}, {deleted: true});
|
|
310
|
+
|
|
311
|
+
const res = await adminAgent.get("/announcements/overview").expect(200);
|
|
312
|
+
assert.strictEqual(res.body.total, 1);
|
|
313
|
+
assert.strictEqual(res.body.totals.announcements, 1);
|
|
314
|
+
assert.strictEqual(res.body.totals.impressions, 1);
|
|
315
|
+
assert.strictEqual(res.body.data[0].metrics.impressions, 1);
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
it("resolves omitted acknowledgementPolicy from plugin default", async () => {
|
|
319
|
+
const defaultApp = buildApp({defaultAcknowledgementPolicy: "required"});
|
|
320
|
+
const defaultAdmin = await authAsUser(defaultApp, "admin");
|
|
321
|
+
|
|
322
|
+
await Announcement.create({
|
|
323
|
+
body: "Uses default",
|
|
324
|
+
publishedAt: DateTime.utc().toJSDate(),
|
|
325
|
+
status: "published",
|
|
326
|
+
title: "Default policy",
|
|
327
|
+
version: 1,
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
const res = await defaultAdmin.get("/announcements/overview").expect(200);
|
|
331
|
+
assert.strictEqual(res.body.data[0].acknowledgementPolicy, "required");
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
it("returns 403 for non-admin users", async () => {
|
|
335
|
+
const res = await userAgent.get("/announcements/overview").expect(403);
|
|
336
|
+
assert.strictEqual(res.body.title, "Admin access required");
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
it("allows consumers to use an RBAC permission method instead of legacy admin flag", async () => {
|
|
340
|
+
const rbacApp = buildApp({
|
|
341
|
+
adminOverviewPermissions: [(): boolean => true],
|
|
342
|
+
});
|
|
343
|
+
const rbacUser = await authAsUser(rbacApp, "notAdmin");
|
|
344
|
+
|
|
345
|
+
await rbacUser.get("/announcements/overview").expect(200);
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
it("returns 401 for unauthenticated requests", async () => {
|
|
349
|
+
await supertest(app).get("/announcements/overview").expect(401);
|
|
350
|
+
});
|
|
351
|
+
});
|
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
import {describe, expect, it} from "bun:test";
|
|
2
|
+
import {assert} from "chai";
|
|
3
|
+
import {DateTime} from "luxon";
|
|
4
|
+
import mongoose from "mongoose";
|
|
5
|
+
import {
|
|
6
|
+
isAnnouncementPendingForUser,
|
|
7
|
+
isAnnouncementVisibleNow,
|
|
8
|
+
matchAudienceByType,
|
|
9
|
+
passesMinBuildNumber,
|
|
10
|
+
requiresAcknowledgementForAnnouncement,
|
|
11
|
+
resolveAcknowledgementPolicy,
|
|
12
|
+
resolveAudienceType,
|
|
13
|
+
resolveDisplayMode,
|
|
14
|
+
selectPendingAnnouncements,
|
|
15
|
+
} from "../pending";
|
|
16
|
+
import type {AnnouncementDocument} from "../types";
|
|
17
|
+
|
|
18
|
+
const makeAnnouncement = (overrides: Partial<AnnouncementDocument> = {}): AnnouncementDocument => ({
|
|
19
|
+
_id: new mongoose.Types.ObjectId(),
|
|
20
|
+
audience: {},
|
|
21
|
+
body: "Body",
|
|
22
|
+
created: new Date(),
|
|
23
|
+
expiresAt: undefined,
|
|
24
|
+
platforms: ["ios", "android", "web"],
|
|
25
|
+
priority: 0,
|
|
26
|
+
publishAt: undefined,
|
|
27
|
+
publishedAt: DateTime.utc().minus({days: 1}).toJSDate(),
|
|
28
|
+
status: "published",
|
|
29
|
+
title: "Title",
|
|
30
|
+
updated: new Date(),
|
|
31
|
+
version: 1,
|
|
32
|
+
...overrides,
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
describe("pending helpers", () => {
|
|
36
|
+
it("resolves acknowledgement policy from field, default, and legacy boolean", () => {
|
|
37
|
+
expect(
|
|
38
|
+
resolveAcknowledgementPolicy({
|
|
39
|
+
announcement: makeAnnouncement({acknowledgementPolicy: "required"}),
|
|
40
|
+
})
|
|
41
|
+
).toBe("required");
|
|
42
|
+
expect(
|
|
43
|
+
resolveAcknowledgementPolicy({
|
|
44
|
+
announcement: makeAnnouncement({acknowledgementPolicy: "dismiss-only"}),
|
|
45
|
+
})
|
|
46
|
+
).toBe("dismiss-only");
|
|
47
|
+
expect(
|
|
48
|
+
resolveAcknowledgementPolicy({
|
|
49
|
+
announcement: makeAnnouncement({}),
|
|
50
|
+
defaultAcknowledgementPolicy: "required",
|
|
51
|
+
})
|
|
52
|
+
).toBe("required");
|
|
53
|
+
expect(
|
|
54
|
+
resolveAcknowledgementPolicy({
|
|
55
|
+
announcement: makeAnnouncement({}),
|
|
56
|
+
})
|
|
57
|
+
).toBe("dismiss-only");
|
|
58
|
+
|
|
59
|
+
const legacyRequired = makeAnnouncement({}) as AnnouncementDocument & {
|
|
60
|
+
requiresAcknowledgement: boolean;
|
|
61
|
+
};
|
|
62
|
+
legacyRequired.requiresAcknowledgement = true;
|
|
63
|
+
expect(resolveAcknowledgementPolicy({announcement: legacyRequired})).toBe("required");
|
|
64
|
+
|
|
65
|
+
const legacyFalse = makeAnnouncement({}) as AnnouncementDocument & {
|
|
66
|
+
requiresAcknowledgement: boolean;
|
|
67
|
+
};
|
|
68
|
+
legacyFalse.requiresAcknowledgement = false;
|
|
69
|
+
expect(
|
|
70
|
+
resolveAcknowledgementPolicy({
|
|
71
|
+
announcement: legacyFalse,
|
|
72
|
+
defaultAcknowledgementPolicy: "required",
|
|
73
|
+
})
|
|
74
|
+
).toBe("required");
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it("maps resolved policy to requiresAcknowledgement boolean", () => {
|
|
78
|
+
expect(
|
|
79
|
+
requiresAcknowledgementForAnnouncement({
|
|
80
|
+
announcement: makeAnnouncement({acknowledgementPolicy: "required"}),
|
|
81
|
+
})
|
|
82
|
+
).toBe(true);
|
|
83
|
+
expect(
|
|
84
|
+
requiresAcknowledgementForAnnouncement({
|
|
85
|
+
announcement: makeAnnouncement({acknowledgementPolicy: "dismiss-only"}),
|
|
86
|
+
})
|
|
87
|
+
).toBe(false);
|
|
88
|
+
expect(
|
|
89
|
+
requiresAcknowledgementForAnnouncement({
|
|
90
|
+
announcement: makeAnnouncement({}),
|
|
91
|
+
defaultAcknowledgementPolicy: "required",
|
|
92
|
+
})
|
|
93
|
+
).toBe(true);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it("filters announcements outside the publish window", () => {
|
|
97
|
+
const future = makeAnnouncement({
|
|
98
|
+
publishAt: DateTime.utc().plus({days: 1}).toJSDate(),
|
|
99
|
+
});
|
|
100
|
+
const expired = makeAnnouncement({
|
|
101
|
+
expiresAt: DateTime.utc().minus({hours: 1}).toJSDate(),
|
|
102
|
+
});
|
|
103
|
+
expect(isAnnouncementVisibleNow({announcement: future})).toBe(false);
|
|
104
|
+
expect(isAnnouncementVisibleNow({announcement: expired})).toBe(false);
|
|
105
|
+
expect(isAnnouncementVisibleNow({announcement: makeAnnouncement()})).toBe(true);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it("returns pending announcements until acknowledged or impressed", () => {
|
|
109
|
+
const announcement = makeAnnouncement({acknowledgementPolicy: "required"});
|
|
110
|
+
const announcementId = announcement._id.toString();
|
|
111
|
+
|
|
112
|
+
expect(
|
|
113
|
+
isAnnouncementPendingForUser({
|
|
114
|
+
acknowledgements: [],
|
|
115
|
+
announcement,
|
|
116
|
+
impressions: [],
|
|
117
|
+
})
|
|
118
|
+
).toBe(true);
|
|
119
|
+
|
|
120
|
+
expect(
|
|
121
|
+
isAnnouncementPendingForUser({
|
|
122
|
+
acknowledgements: [{announcementId, version: 1}],
|
|
123
|
+
announcement,
|
|
124
|
+
impressions: [],
|
|
125
|
+
})
|
|
126
|
+
).toBe(false);
|
|
127
|
+
|
|
128
|
+
const dismissOnly = makeAnnouncement({acknowledgementPolicy: "dismiss-only"});
|
|
129
|
+
const dismissId = dismissOnly._id.toString();
|
|
130
|
+
expect(
|
|
131
|
+
isAnnouncementPendingForUser({
|
|
132
|
+
acknowledgements: [],
|
|
133
|
+
announcement: dismissOnly,
|
|
134
|
+
impressions: [{announcementId: dismissId, version: 1}],
|
|
135
|
+
})
|
|
136
|
+
).toBe(false);
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it("sorts by priority then publishedAt", () => {
|
|
140
|
+
const low = makeAnnouncement({
|
|
141
|
+
acknowledgementPolicy: "required",
|
|
142
|
+
priority: 1,
|
|
143
|
+
publishedAt: DateTime.utc().minus({days: 1}).toJSDate(),
|
|
144
|
+
});
|
|
145
|
+
const high = makeAnnouncement({
|
|
146
|
+
acknowledgementPolicy: "required",
|
|
147
|
+
priority: 10,
|
|
148
|
+
publishedAt: DateTime.utc().minus({days: 2}).toJSDate(),
|
|
149
|
+
});
|
|
150
|
+
const pending = selectPendingAnnouncements({
|
|
151
|
+
acknowledgements: [],
|
|
152
|
+
announcements: [low, high],
|
|
153
|
+
impressions: [],
|
|
154
|
+
platform: "web",
|
|
155
|
+
});
|
|
156
|
+
expect(pending[0]?._id.toString()).toBe(high._id.toString());
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it("defaults missing displayMode to modal and audienceType to all", () => {
|
|
160
|
+
const legacy = makeAnnouncement({});
|
|
161
|
+
assert.strictEqual(resolveDisplayMode(legacy), "modal");
|
|
162
|
+
assert.strictEqual(resolveAudienceType(legacy), "all");
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
it("matchAudienceByType honors staff, patient, and all", () => {
|
|
166
|
+
const staffUser = {admin: true};
|
|
167
|
+
const patientUser = {admin: false};
|
|
168
|
+
const isStaff = (user: {admin?: boolean}) => user.admin === true;
|
|
169
|
+
|
|
170
|
+
assert.isTrue(
|
|
171
|
+
matchAudienceByType({
|
|
172
|
+
announcement: makeAnnouncement({audienceType: "all"}),
|
|
173
|
+
isStaff,
|
|
174
|
+
user: patientUser,
|
|
175
|
+
})
|
|
176
|
+
);
|
|
177
|
+
assert.isTrue(
|
|
178
|
+
matchAudienceByType({
|
|
179
|
+
announcement: makeAnnouncement({audienceType: "staff"}),
|
|
180
|
+
isStaff,
|
|
181
|
+
user: staffUser,
|
|
182
|
+
})
|
|
183
|
+
);
|
|
184
|
+
assert.isFalse(
|
|
185
|
+
matchAudienceByType({
|
|
186
|
+
announcement: makeAnnouncement({audienceType: "staff"}),
|
|
187
|
+
isStaff,
|
|
188
|
+
user: patientUser,
|
|
189
|
+
})
|
|
190
|
+
);
|
|
191
|
+
assert.isTrue(
|
|
192
|
+
matchAudienceByType({
|
|
193
|
+
announcement: makeAnnouncement({audienceType: "patient"}),
|
|
194
|
+
isStaff,
|
|
195
|
+
user: patientUser,
|
|
196
|
+
})
|
|
197
|
+
);
|
|
198
|
+
assert.isFalse(
|
|
199
|
+
matchAudienceByType({
|
|
200
|
+
announcement: makeAnnouncement({audienceType: "patient"}),
|
|
201
|
+
isStaff,
|
|
202
|
+
user: staffUser,
|
|
203
|
+
})
|
|
204
|
+
);
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
it("passesMinBuildNumber hides only when version is present and below min", () => {
|
|
208
|
+
const gated = makeAnnouncement({minBuildNumber: 10});
|
|
209
|
+
|
|
210
|
+
assert.isTrue(passesMinBuildNumber({announcement: gated}));
|
|
211
|
+
assert.isTrue(passesMinBuildNumber({announcement: gated, queryVersion: 10}));
|
|
212
|
+
assert.isTrue(passesMinBuildNumber({announcement: gated, queryVersion: 11}));
|
|
213
|
+
assert.isFalse(passesMinBuildNumber({announcement: gated, queryVersion: 9}));
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
it("selectPendingAnnouncements omits feed displayMode", () => {
|
|
217
|
+
const modal = makeAnnouncement({
|
|
218
|
+
acknowledgementPolicy: "required",
|
|
219
|
+
displayMode: "modal",
|
|
220
|
+
title: "Modal item",
|
|
221
|
+
});
|
|
222
|
+
const feedOnly = makeAnnouncement({
|
|
223
|
+
acknowledgementPolicy: "required",
|
|
224
|
+
displayMode: "feed",
|
|
225
|
+
title: "Feed only",
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
const pending = selectPendingAnnouncements({
|
|
229
|
+
acknowledgements: [],
|
|
230
|
+
announcements: [modal, feedOnly],
|
|
231
|
+
impressions: [],
|
|
232
|
+
platform: "web",
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
assert.lengthOf(pending, 1);
|
|
236
|
+
assert.strictEqual(pending[0]?._id.toString(), modal._id.toString());
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
it("selectPendingAnnouncements applies minBuildNumber when version is present", () => {
|
|
240
|
+
const visible = makeAnnouncement({
|
|
241
|
+
acknowledgementPolicy: "required",
|
|
242
|
+
minBuildNumber: 10,
|
|
243
|
+
title: "Visible",
|
|
244
|
+
});
|
|
245
|
+
const hidden = makeAnnouncement({
|
|
246
|
+
acknowledgementPolicy: "required",
|
|
247
|
+
minBuildNumber: 20,
|
|
248
|
+
title: "Hidden",
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
const pending = selectPendingAnnouncements({
|
|
252
|
+
acknowledgements: [],
|
|
253
|
+
announcements: [visible, hidden],
|
|
254
|
+
impressions: [],
|
|
255
|
+
platform: "web",
|
|
256
|
+
queryVersion: 15,
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
assert.lengthOf(pending, 1);
|
|
260
|
+
assert.strictEqual(pending[0]?._id.toString(), visible._id.toString());
|
|
261
|
+
});
|
|
262
|
+
});
|