@terreno/api 0.0.11 → 0.0.13

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.
@@ -0,0 +1,690 @@
1
+ import {beforeEach, describe, expect, it} from "bun:test";
2
+ import type express from "express";
3
+ import supertest from "supertest";
4
+ import type TestAgent from "supertest/lib/agent";
5
+
6
+ import {modelRouter} from "./api";
7
+ import {addAuthRoutes, setupAuth} from "./auth";
8
+ import {Permissions} from "./permissions";
9
+ import {authAsUser, type Food, FoodModel, getBaseServer, setupDb, UserModel} from "./tests";
10
+ import {AdminOwnerTransformer} from "./transformers";
11
+
12
+ describe("model array operations", () => {
13
+ let _server: TestAgent;
14
+ let app: express.Application;
15
+ let admin: any;
16
+ let spinach: Food;
17
+ let apple: Food;
18
+ let agent: TestAgent;
19
+
20
+ beforeEach(async () => {
21
+ process.env.REFRESH_TOKEN_SECRET = "testsecret1234";
22
+
23
+ [admin] = await setupDb();
24
+
25
+ [spinach, apple] = await Promise.all([
26
+ FoodModel.create({
27
+ calories: 1,
28
+ created: new Date("2021-12-03T00:00:20.000Z"),
29
+ hidden: false,
30
+ name: "Spinach",
31
+ ownerId: admin._id,
32
+ source: {
33
+ name: "Brand",
34
+ },
35
+ }),
36
+ FoodModel.create({
37
+ calories: 100,
38
+ categories: [
39
+ {
40
+ name: "Fruit",
41
+ show: true,
42
+ },
43
+ {
44
+ name: "Popular",
45
+ show: false,
46
+ },
47
+ ],
48
+ created: new Date("2021-12-03T00:00:30.000Z"),
49
+ hidden: false,
50
+ name: "Apple",
51
+ ownerId: admin._id,
52
+ tags: ["healthy", "cheap"],
53
+ }),
54
+ ]);
55
+
56
+ app = getBaseServer();
57
+ setupAuth(app, UserModel as any);
58
+ addAuthRoutes(app, UserModel as any);
59
+ app.use(
60
+ "/food",
61
+ modelRouter(FoodModel, {
62
+ allowAnonymous: true,
63
+ permissions: {
64
+ create: [Permissions.IsAdmin],
65
+ delete: [Permissions.IsAdmin],
66
+ list: [Permissions.IsAdmin],
67
+ read: [Permissions.IsAdmin],
68
+ update: [Permissions.IsAdmin],
69
+ },
70
+ queryFields: ["hidden", "calories", "created", "source.name"],
71
+ sort: {created: "descending"},
72
+ })
73
+ );
74
+ _server = supertest(app);
75
+ agent = await authAsUser(app, "admin");
76
+ });
77
+
78
+ it("add array sub-schema item", async () => {
79
+ // Incorrect way, should have "categories" as a top level key.
80
+ let res = await agent
81
+ .post(`/food/${apple._id}/categories`)
82
+ .send({name: "Good Seller", show: false})
83
+ .expect(400);
84
+ expect(res.body.title).toBe(
85
+ "Malformed body, array operations should have a single, top level key, got: name,show"
86
+ );
87
+
88
+ res = await agent
89
+ .post(`/food/${apple._id}/categories`)
90
+ .send({categories: {name: "Good Seller", show: false}})
91
+ .expect(200);
92
+ expect(res.body.data.categories).toHaveLength(3);
93
+ expect(res.body.data.categories[2].name).toBe("Good Seller");
94
+
95
+ res = await agent
96
+ .post(`/food/${spinach._id}/categories`)
97
+ .send({categories: {name: "Good Seller", show: false}})
98
+ .expect(200);
99
+ expect(res.body.data.categories).toHaveLength(1);
100
+ });
101
+
102
+ it("update array sub-schema item", async () => {
103
+ let res = await agent
104
+ .patch(`/food/${apple._id}/categories/xyz`)
105
+ .send({categories: {name: "Good Seller", show: false}})
106
+ .expect(404);
107
+ expect(res.body.title).toBe("Could not find categories/xyz");
108
+ res = await agent
109
+ .patch(`/food/${apple._id}/categories/${apple.categories[1]._id}`)
110
+ .send({categories: {name: "Good Seller", show: false}})
111
+ .expect(200);
112
+ expect(res.body.data.categories).toHaveLength(2);
113
+ expect(res.body.data.categories[1].name).toBe("Good Seller");
114
+ });
115
+
116
+ it("delete array sub-schema item", async () => {
117
+ let res = await agent.delete(`/food/${apple._id}/categories/xyz`).expect(404);
118
+ expect(res.body.title).toBe("Could not find categories/xyz");
119
+ res = await agent
120
+ .delete(`/food/${apple._id}/categories/${apple.categories[0]._id}`)
121
+ .expect(200);
122
+ expect(res.body.data.categories).toHaveLength(1);
123
+ expect(res.body.data.categories[0].name).toBe("Popular");
124
+ });
125
+
126
+ it("add array item", async () => {
127
+ let res = await agent.post(`/food/${apple._id}/tags`).send({tags: "popular"}).expect(200);
128
+ expect(res.body.data.tags).toHaveLength(3);
129
+ expect(res.body.data.tags).toEqual(["healthy", "cheap", "popular"]);
130
+
131
+ res = await agent.post(`/food/${spinach._id}/tags`).send({tags: "popular"}).expect(200);
132
+ expect(res.body.data.tags).toEqual(["popular"]);
133
+ });
134
+
135
+ it("update array item", async () => {
136
+ let res = await agent
137
+ .patch(`/food/${apple._id}/tags/xyz`)
138
+ .send({tags: "unhealthy"})
139
+ .expect(404);
140
+ expect(res.body.title).toBe("Could not find tags/xyz");
141
+ res = await agent
142
+ .patch(`/food/${apple._id}/tags/healthy`)
143
+ .send({tags: "unhealthy"})
144
+ .expect(200);
145
+ expect(res.body.data.tags).toEqual(["unhealthy", "cheap"]);
146
+ });
147
+
148
+ it("delete array item", async () => {
149
+ let res = await agent.delete(`/food/${apple._id}/tags/xyz`).expect(404);
150
+ expect(res.body.title).toBe("Could not find tags/xyz");
151
+ res = await agent.delete(`/food/${apple._id}/tags/healthy`).expect(200);
152
+ expect(res.body.data.tags).toEqual(["cheap"]);
153
+ });
154
+
155
+ it("updates timestamps on array subdocuments", async () => {
156
+ // Create a food with categories that have timestamps
157
+ const foodWithTimestamps = await FoodModel.create({
158
+ calories: 100,
159
+ categories: [
160
+ {
161
+ name: "Category 1",
162
+ show: true,
163
+ updated: new Date("2024-01-01T00:00:00.000Z"),
164
+ },
165
+ {
166
+ name: "Category 2",
167
+ show: true,
168
+ updated: new Date("2024-01-01T00:00:00.000Z"),
169
+ },
170
+ ],
171
+ created: new Date(),
172
+ name: "Food with Timestamps",
173
+ ownerId: admin._id,
174
+ });
175
+
176
+ const firstCategoryId = foodWithTimestamps.categories?.[0]?._id?.toString();
177
+ const secondCategoryId = foodWithTimestamps.categories?.[1]?._id?.toString();
178
+
179
+ if (!firstCategoryId || !secondCategoryId) {
180
+ throw new Error("Failed to create food with categories");
181
+ }
182
+
183
+ // Wait a moment to ensure timestamp difference
184
+ await new Promise((resolve) => setTimeout(resolve, 100));
185
+
186
+ // Update one of the categories
187
+ const res = await agent
188
+ .patch(`/food/${foodWithTimestamps._id}/categories/${firstCategoryId}`)
189
+ .send({categories: {name: "Updated Category"}})
190
+ .expect(200);
191
+
192
+ // Verify the updated category has a newer timestamp
193
+ const updatedCategory = res.body.data.categories.find((c: any) => c._id === firstCategoryId);
194
+ const unchangedCategory = res.body.data.categories.find((c: any) => c._id === secondCategoryId);
195
+
196
+ if (!updatedCategory || !unchangedCategory) {
197
+ throw new Error("Failed to find categories in response");
198
+ }
199
+
200
+ expect(updatedCategory.updated).not.toBe(updatedCategory.created);
201
+ expect(unchangedCategory.updated).toBe(unchangedCategory.created);
202
+ expect(updatedCategory.name).toBe("Updated Category");
203
+ // Unchanged.
204
+ expect(updatedCategory.show).toBe(true);
205
+ expect(unchangedCategory.show).toBe(true);
206
+ });
207
+
208
+ it("array operations call postUpdate with different copy of document", async () => {
209
+ let postUpdateDoc: any;
210
+ let postUpdatePrevDoc: any;
211
+ let postUpdateCalled = false;
212
+
213
+ app = getBaseServer();
214
+ setupAuth(app, UserModel as any);
215
+ addAuthRoutes(app, UserModel as any);
216
+ app.use(
217
+ "/food",
218
+ modelRouter(FoodModel, {
219
+ allowAnonymous: true,
220
+ permissions: {
221
+ create: [Permissions.IsAdmin],
222
+ delete: [Permissions.IsAdmin],
223
+ list: [Permissions.IsAdmin],
224
+ read: [Permissions.IsAdmin],
225
+ update: [Permissions.IsAdmin],
226
+ },
227
+ postUpdate: async (doc: any, _cleanedBody: any, _request: any, prevValue: any) => {
228
+ postUpdateDoc = doc;
229
+ postUpdatePrevDoc = prevValue;
230
+ postUpdateCalled = true;
231
+ },
232
+ })
233
+ );
234
+ _server = supertest(app);
235
+ agent = await authAsUser(app, "admin");
236
+
237
+ // Test POST operation (add to array)
238
+ await agent
239
+ .post(`/food/${apple._id}/categories`)
240
+ .send({categories: {name: "New Category", show: true}})
241
+ .expect(200);
242
+
243
+ expect(postUpdateCalled).toBe(true);
244
+ expect(postUpdateDoc).toBeDefined();
245
+ expect(postUpdatePrevDoc).toBeDefined();
246
+
247
+ // Verify they are different object references
248
+ expect(postUpdateDoc).not.toBe(postUpdatePrevDoc);
249
+
250
+ // Verify the content is different (new category added)
251
+ expect(postUpdateDoc.categories).toHaveLength(3);
252
+ expect(postUpdatePrevDoc.categories).toHaveLength(2);
253
+
254
+ // Reset for next test
255
+ postUpdateCalled = false;
256
+ postUpdateDoc = undefined;
257
+ postUpdatePrevDoc = undefined;
258
+
259
+ // Test PATCH operation (update array item)
260
+ const categoryId = apple.categories[0]._id;
261
+ if (!categoryId) {
262
+ throw new Error("Category ID is undefined");
263
+ }
264
+ await agent
265
+ .patch(`/food/${apple._id}/categories/${categoryId}`)
266
+ .send({categories: {name: "Updated Category", show: false}})
267
+ .expect(200);
268
+
269
+ expect(postUpdateCalled).toBe(true);
270
+ expect(postUpdateDoc).toBeDefined();
271
+ expect(postUpdatePrevDoc).toBeDefined();
272
+
273
+ // Verify they are different object references
274
+ expect(postUpdateDoc).not.toBe(postUpdatePrevDoc);
275
+
276
+ // Verify the content is different (category updated)
277
+ const updatedCategory = postUpdateDoc.categories.find(
278
+ (c: any) => c._id.toString() === categoryId.toString()
279
+ );
280
+ const prevCategory = postUpdatePrevDoc.categories.find(
281
+ (c: any) => c._id.toString() === categoryId.toString()
282
+ );
283
+
284
+ expect(updatedCategory.name).toBe("Updated Category");
285
+ expect(prevCategory.name).toBe("Fruit");
286
+
287
+ // Reset for next test
288
+ postUpdateCalled = false;
289
+ postUpdateDoc = undefined;
290
+ postUpdatePrevDoc = undefined;
291
+
292
+ // Test DELETE operation (remove from array)
293
+ await agent.delete(`/food/${apple._id}/categories/${categoryId}`).expect(200);
294
+
295
+ expect(postUpdateCalled).toBe(true);
296
+ expect(postUpdateDoc).toBeDefined();
297
+ expect(postUpdatePrevDoc).toBeDefined();
298
+
299
+ // Verify they are different object references
300
+ expect(postUpdateDoc).not.toBe(postUpdatePrevDoc);
301
+
302
+ // Verify the content is different (category removed)
303
+ const remainingCategories = postUpdateDoc.categories.filter(
304
+ (c: any) => c._id.toString() === categoryId.toString()
305
+ );
306
+ const prevCategories = postUpdatePrevDoc.categories.filter(
307
+ (c: any) => c._id.toString() === categoryId.toString()
308
+ );
309
+
310
+ expect(remainingCategories).toHaveLength(0);
311
+ expect(prevCategories).toHaveLength(1);
312
+ });
313
+
314
+ it("array operations with string arrays call postUpdate with different copy", async () => {
315
+ let postUpdateDoc: any;
316
+ let postUpdatePrevDoc: any;
317
+ let postUpdateCalled = false;
318
+
319
+ app = getBaseServer();
320
+ setupAuth(app, UserModel as any);
321
+ addAuthRoutes(app, UserModel as any);
322
+ app.use(
323
+ "/food",
324
+ modelRouter(FoodModel, {
325
+ allowAnonymous: true,
326
+ permissions: {
327
+ create: [Permissions.IsAdmin],
328
+ delete: [Permissions.IsAdmin],
329
+ list: [Permissions.IsAdmin],
330
+ read: [Permissions.IsAdmin],
331
+ update: [Permissions.IsAdmin],
332
+ },
333
+ postUpdate: async (doc: any, _cleanedBody: any, _request: any, prevValue: any) => {
334
+ postUpdateDoc = doc;
335
+ postUpdatePrevDoc = prevValue;
336
+ postUpdateCalled = true;
337
+ },
338
+ })
339
+ );
340
+ _server = supertest(app);
341
+ agent = await authAsUser(app, "admin");
342
+
343
+ // Test POST operation with string array (add tag)
344
+ await agent.post(`/food/${apple._id}/tags`).send({tags: "organic"}).expect(200);
345
+
346
+ expect(postUpdateCalled).toBe(true);
347
+ expect(postUpdateDoc).toBeDefined();
348
+ expect(postUpdatePrevDoc).toBeDefined();
349
+
350
+ // Verify they are different object references
351
+ expect(postUpdateDoc).not.toBe(postUpdatePrevDoc);
352
+
353
+ // Verify the content is different (new tag added)
354
+ expect(postUpdateDoc.tags).toHaveLength(3);
355
+ expect(postUpdatePrevDoc.tags).toHaveLength(2);
356
+ expect(postUpdateDoc.tags).toContain("organic");
357
+ expect(postUpdatePrevDoc.tags).not.toContain("organic");
358
+
359
+ // Reset for next test
360
+ postUpdateCalled = false;
361
+ postUpdateDoc = undefined;
362
+ postUpdatePrevDoc = undefined;
363
+
364
+ // Test PATCH operation with string array (update tag)
365
+ await agent.patch(`/food/${apple._id}/tags/healthy`).send({tags: "super-healthy"}).expect(200);
366
+
367
+ expect(postUpdateCalled).toBe(true);
368
+ expect(postUpdateDoc).not.toBe(postUpdatePrevDoc);
369
+
370
+ // Verify the content is different (tag updated)
371
+ expect(postUpdateDoc.tags).toContain("super-healthy");
372
+ expect(postUpdatePrevDoc.tags).toContain("healthy");
373
+ expect(postUpdateDoc.tags).not.toContain("healthy");
374
+ expect(postUpdatePrevDoc.tags).not.toContain("super-healthy");
375
+ });
376
+ });
377
+
378
+ describe("array operation errors", () => {
379
+ let _server: TestAgent;
380
+ let app: express.Application;
381
+ let admin: any;
382
+ let apple: Food;
383
+ let agent: TestAgent;
384
+
385
+ beforeEach(async () => {
386
+ [admin] = await setupDb();
387
+
388
+ apple = await FoodModel.create({
389
+ calories: 100,
390
+ categories: [
391
+ {name: "Fruit", show: true},
392
+ {name: "Popular", show: false},
393
+ ],
394
+ created: new Date("2021-12-03T00:00:30.000Z"),
395
+ hidden: false,
396
+ name: "Apple",
397
+ ownerId: admin._id,
398
+ tags: ["healthy", "cheap"],
399
+ });
400
+
401
+ app = getBaseServer();
402
+ setupAuth(app, UserModel as any);
403
+ addAuthRoutes(app, UserModel as any);
404
+ });
405
+
406
+ it("array operation preUpdate returning undefined throws error", async () => {
407
+ app.use(
408
+ "/food",
409
+ modelRouter(FoodModel, {
410
+ allowAnonymous: true,
411
+ permissions: {
412
+ create: [Permissions.IsAdmin],
413
+ delete: [Permissions.IsAdmin],
414
+ list: [Permissions.IsAdmin],
415
+ read: [Permissions.IsAdmin],
416
+ update: [Permissions.IsAdmin],
417
+ },
418
+ preUpdate: () => undefined as any,
419
+ })
420
+ );
421
+ _server = supertest(app);
422
+ agent = await authAsUser(app, "admin");
423
+
424
+ const res = await agent.post(`/food/${apple._id}/tags`).send({tags: "organic"}).expect(403);
425
+ expect(res.body.title).toBe("Update not allowed");
426
+ expect(res.body.detail).toBe("A body must be returned from preUpdate");
427
+ });
428
+
429
+ it("array operation preUpdate returning null throws error", async () => {
430
+ app.use(
431
+ "/food",
432
+ modelRouter(FoodModel, {
433
+ allowAnonymous: true,
434
+ permissions: {
435
+ create: [Permissions.IsAdmin],
436
+ delete: [Permissions.IsAdmin],
437
+ list: [Permissions.IsAdmin],
438
+ read: [Permissions.IsAdmin],
439
+ update: [Permissions.IsAdmin],
440
+ },
441
+ preUpdate: () => null,
442
+ })
443
+ );
444
+ _server = supertest(app);
445
+ agent = await authAsUser(app, "admin");
446
+
447
+ const res = await agent.post(`/food/${apple._id}/tags`).send({tags: "organic"}).expect(403);
448
+ expect(res.body.title).toBe("Update not allowed");
449
+ });
450
+
451
+ it("array operation preUpdate error is handled", async () => {
452
+ app.use(
453
+ "/food",
454
+ modelRouter(FoodModel, {
455
+ allowAnonymous: true,
456
+ permissions: {
457
+ create: [Permissions.IsAdmin],
458
+ delete: [Permissions.IsAdmin],
459
+ list: [Permissions.IsAdmin],
460
+ read: [Permissions.IsAdmin],
461
+ update: [Permissions.IsAdmin],
462
+ },
463
+ preUpdate: () => {
464
+ throw new Error("preUpdate array failed");
465
+ },
466
+ })
467
+ );
468
+ _server = supertest(app);
469
+ agent = await authAsUser(app, "admin");
470
+
471
+ const res = await agent.post(`/food/${apple._id}/tags`).send({tags: "organic"}).expect(400);
472
+ expect(res.body.title).toContain("preUpdate hook error");
473
+ });
474
+
475
+ it("array operation postUpdate error is handled", async () => {
476
+ app.use(
477
+ "/food",
478
+ modelRouter(FoodModel, {
479
+ allowAnonymous: true,
480
+ permissions: {
481
+ create: [Permissions.IsAdmin],
482
+ delete: [Permissions.IsAdmin],
483
+ list: [Permissions.IsAdmin],
484
+ read: [Permissions.IsAdmin],
485
+ update: [Permissions.IsAdmin],
486
+ },
487
+ postUpdate: () => {
488
+ throw new Error("postUpdate array failed");
489
+ },
490
+ })
491
+ );
492
+ _server = supertest(app);
493
+ agent = await authAsUser(app, "admin");
494
+
495
+ const res = await agent.post(`/food/${apple._id}/tags`).send({tags: "organic"}).expect(400);
496
+ expect(res.body.title).toContain("PATCH Post Update error");
497
+ });
498
+
499
+ it("array operation denied without update permission", async () => {
500
+ app.use(
501
+ "/food",
502
+ modelRouter(FoodModel, {
503
+ allowAnonymous: true,
504
+ permissions: {
505
+ create: [Permissions.IsAdmin],
506
+ delete: [Permissions.IsAdmin],
507
+ list: [Permissions.IsAny],
508
+ read: [Permissions.IsAny],
509
+ update: [Permissions.IsAdmin],
510
+ },
511
+ })
512
+ );
513
+ _server = supertest(app);
514
+ agent = await authAsUser(app, "notAdmin");
515
+
516
+ const res = await agent.post(`/food/${apple._id}/tags`).send({tags: "organic"}).expect(405);
517
+ expect(res.body.title).toContain("Access to PATCH");
518
+ });
519
+
520
+ it("array operation on non-existent document returns 404", async () => {
521
+ app.use(
522
+ "/food",
523
+ modelRouter(FoodModel, {
524
+ allowAnonymous: true,
525
+ permissions: {
526
+ create: [Permissions.IsAdmin],
527
+ delete: [Permissions.IsAdmin],
528
+ list: [Permissions.IsAdmin],
529
+ read: [Permissions.IsAdmin],
530
+ update: [Permissions.IsAdmin],
531
+ },
532
+ })
533
+ );
534
+ _server = supertest(app);
535
+ agent = await authAsUser(app, "admin");
536
+
537
+ const fakeId = "000000000000000000000000";
538
+ const res = await agent.post(`/food/${fakeId}/tags`).send({tags: "organic"}).expect(404);
539
+ expect(res.body.title).toContain("Could not find document to PATCH");
540
+ });
541
+
542
+ it("array operation denied when user cannot update specific doc", async () => {
543
+ // Create food owned by admin, then try to update as notAdmin
544
+ app.use(
545
+ "/food",
546
+ modelRouter(FoodModel, {
547
+ allowAnonymous: true,
548
+ permissions: {
549
+ create: [Permissions.IsAuthenticated],
550
+ delete: [Permissions.IsAuthenticated],
551
+ list: [Permissions.IsAuthenticated],
552
+ read: [Permissions.IsAuthenticated],
553
+ update: [Permissions.IsOwner],
554
+ },
555
+ })
556
+ );
557
+ _server = supertest(app);
558
+ // Login as notAdmin and try to update admin's food (apple)
559
+ agent = await authAsUser(app, "notAdmin");
560
+
561
+ const res = await agent.post(`/food/${apple._id}/tags`).send({tags: "organic"}).expect(403);
562
+ expect(res.body.title).toContain("Patch not allowed");
563
+ });
564
+
565
+ it("array operation transform error is handled", async () => {
566
+ app.use(
567
+ "/food",
568
+ modelRouter(FoodModel, {
569
+ allowAnonymous: true,
570
+ permissions: {
571
+ create: [Permissions.IsAdmin],
572
+ delete: [Permissions.IsAdmin],
573
+ list: [Permissions.IsAdmin],
574
+ read: [Permissions.IsAdmin],
575
+ update: [Permissions.IsAdmin],
576
+ },
577
+ transformer: AdminOwnerTransformer({
578
+ adminWriteFields: ["name"],
579
+ }),
580
+ })
581
+ );
582
+ _server = supertest(app);
583
+ agent = await authAsUser(app, "admin");
584
+
585
+ // Try to update tags field, which is not in the allowed write fields
586
+ const res = await agent.post(`/food/${apple._id}/tags`).send({tags: "organic"}).expect(403);
587
+ expect(res.body.title).toContain("cannot write fields");
588
+ });
589
+ });
590
+
591
+ describe("array operation with undefined preUpdate return", () => {
592
+ let _server: TestAgent;
593
+ let app: express.Application;
594
+ let admin: any;
595
+ let apple: Food;
596
+ let agent: TestAgent;
597
+
598
+ beforeEach(async () => {
599
+ [admin] = await setupDb();
600
+
601
+ apple = await FoodModel.create({
602
+ calories: 100,
603
+ categories: [
604
+ {name: "Fruit", show: true},
605
+ {name: "Popular", show: false},
606
+ ],
607
+ created: new Date("2021-12-03T00:00:30.000Z"),
608
+ hidden: false,
609
+ name: "Apple",
610
+ ownerId: admin._id,
611
+ tags: ["healthy", "cheap"],
612
+ });
613
+
614
+ app = getBaseServer();
615
+ setupAuth(app, UserModel as any);
616
+ addAuthRoutes(app, UserModel as any);
617
+ });
618
+
619
+ it("array operation preUpdate returning undefined for array POST throws error", async () => {
620
+ app.use(
621
+ "/food",
622
+ modelRouter(FoodModel, {
623
+ allowAnonymous: true,
624
+ permissions: {
625
+ create: [Permissions.IsAdmin],
626
+ delete: [Permissions.IsAdmin],
627
+ list: [Permissions.IsAdmin],
628
+ read: [Permissions.IsAdmin],
629
+ update: [Permissions.IsAdmin],
630
+ },
631
+ preUpdate: () => undefined as any,
632
+ })
633
+ );
634
+ _server = supertest(app);
635
+ agent = await authAsUser(app, "admin");
636
+
637
+ const res = await agent.post(`/food/${apple._id}/tags`).send({tags: "organic"}).expect(403);
638
+ expect(res.body.title).toBe("Update not allowed");
639
+ expect(res.body.detail).toBe("A body must be returned from preUpdate");
640
+ });
641
+
642
+ it("array operation preUpdate returning null for array PATCH throws error", async () => {
643
+ app.use(
644
+ "/food",
645
+ modelRouter(FoodModel, {
646
+ allowAnonymous: true,
647
+ permissions: {
648
+ create: [Permissions.IsAdmin],
649
+ delete: [Permissions.IsAdmin],
650
+ list: [Permissions.IsAdmin],
651
+ read: [Permissions.IsAdmin],
652
+ update: [Permissions.IsAdmin],
653
+ },
654
+ preUpdate: () => null,
655
+ })
656
+ );
657
+ _server = supertest(app);
658
+ agent = await authAsUser(app, "admin");
659
+
660
+ const res = await agent
661
+ .patch(`/food/${apple._id}/tags/healthy`)
662
+ .send({tags: "unhealthy"})
663
+ .expect(403);
664
+ expect(res.body.title).toBe("Update not allowed");
665
+ });
666
+
667
+ it("array operation preUpdate error for array DELETE is handled", async () => {
668
+ app.use(
669
+ "/food",
670
+ modelRouter(FoodModel, {
671
+ allowAnonymous: true,
672
+ permissions: {
673
+ create: [Permissions.IsAdmin],
674
+ delete: [Permissions.IsAdmin],
675
+ list: [Permissions.IsAdmin],
676
+ read: [Permissions.IsAdmin],
677
+ update: [Permissions.IsAdmin],
678
+ },
679
+ preUpdate: () => {
680
+ throw new Error("preUpdate error during delete");
681
+ },
682
+ })
683
+ );
684
+ _server = supertest(app);
685
+ agent = await authAsUser(app, "admin");
686
+
687
+ const res = await agent.delete(`/food/${apple._id}/tags/healthy`).expect(400);
688
+ expect(res.body.title).toContain("preUpdate hook error");
689
+ });
690
+ });