@terreno/api 0.9.3 → 0.11.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 (52) hide show
  1. package/bunfig.toml +5 -2
  2. package/bunfig.unit.toml +3 -0
  3. package/dist/auth.test.js +257 -0
  4. package/dist/consentApp.test.js +245 -0
  5. package/dist/expressServer.js +3 -9
  6. package/dist/expressServer.test.js +4 -7
  7. package/dist/githubAuth.test.js +380 -0
  8. package/dist/logger.test.d.ts +1 -0
  9. package/dist/logger.test.js +143 -0
  10. package/dist/notifiers/googleChatNotifier.test.js +37 -0
  11. package/dist/openApi.js +2 -2
  12. package/dist/openApi.test.js +125 -0
  13. package/dist/openApiBuilder.d.ts +1 -0
  14. package/dist/openApiBuilder.js +13 -2
  15. package/dist/openApiBuilder.test.js +66 -0
  16. package/dist/openApiEtag.test.js +8 -0
  17. package/dist/openApiValidator.test.js +309 -0
  18. package/dist/permissions.middleware.test.d.ts +1 -0
  19. package/dist/permissions.middleware.test.js +341 -0
  20. package/dist/plugins.d.ts +8 -8
  21. package/dist/plugins.js +38 -32
  22. package/dist/populate.test.js +99 -0
  23. package/dist/syncConsents.js +2 -2
  24. package/dist/syncConsents.test.js +273 -0
  25. package/dist/tests/bunSetup.js +27 -22
  26. package/dist/tests.d.ts +3 -3
  27. package/dist/tests.js +78 -82
  28. package/dist/utils.d.ts +2 -2
  29. package/dist/utils.js +7 -7
  30. package/package.json +2 -1
  31. package/src/__snapshots__/openApi.test.ts.snap +48 -0
  32. package/src/auth.test.ts +147 -0
  33. package/src/consentApp.test.ts +162 -0
  34. package/src/expressServer.test.ts +4 -11
  35. package/src/expressServer.ts +4 -8
  36. package/src/githubAuth.test.ts +307 -1
  37. package/src/logger.test.ts +149 -0
  38. package/src/notifiers/googleChatNotifier.test.ts +24 -0
  39. package/src/openApi.test.ts +157 -1
  40. package/src/openApi.ts +6 -2
  41. package/src/openApiBuilder.test.ts +81 -0
  42. package/src/openApiBuilder.ts +17 -2
  43. package/src/openApiEtag.test.ts +11 -0
  44. package/src/openApiValidator.test.ts +410 -0
  45. package/src/permissions.middleware.test.ts +197 -0
  46. package/src/plugins.ts +32 -23
  47. package/src/populate.test.ts +78 -2
  48. package/src/syncConsents.test.ts +145 -0
  49. package/src/syncConsents.ts +1 -1
  50. package/src/tests/bunSetup.ts +14 -8
  51. package/src/tests.ts +8 -8
  52. package/src/utils.ts +4 -4
@@ -1,13 +1,21 @@
1
1
  import {afterEach, beforeEach, describe, expect, it} from "bun:test";
2
+ import type {ErrorObject} from "ajv";
2
3
 
3
4
  import {modelRouter} from "./api";
4
5
  import {addAuthRoutes, setupAuth} from "./auth";
5
6
  import {
6
7
  buildQuerySchemaFromFields,
7
8
  configureOpenApiValidator,
9
+ createModelValidators,
10
+ createValidator,
11
+ getOpenApiValidatorConfig,
12
+ getSchemaFromModel,
8
13
  isOpenApiValidatorConfigured,
9
14
  resetOpenApiValidatorConfig,
15
+ validateModelRequestBody,
16
+ validateQueryParams,
10
17
  validateRequestBody,
18
+ validateResponseData,
11
19
  } from "./openApiValidator";
12
20
  import {Permissions} from "./permissions";
13
21
  import {authAsUser, FoodModel, getBaseServer, RequiredModel, setupDb, UserModel} from "./tests";
@@ -237,5 +245,407 @@ describe("openApiValidator", () => {
237
245
  middleware(req, res, () => {});
238
246
  }).toThrow();
239
247
  });
248
+
249
+ it("calls onError callback instead of throwing when provided at option level", () => {
250
+ configureOpenApiValidator();
251
+
252
+ let capturedErrors: ErrorObject[] = [];
253
+ const middleware = validateRequestBody(
254
+ {name: {required: true, type: "string"}},
255
+ {
256
+ onError: (errors) => {
257
+ capturedErrors = errors;
258
+ },
259
+ }
260
+ );
261
+
262
+ let nextCalled = false;
263
+ const req = {body: {}, method: "POST", path: "/test"} as any;
264
+ const res = {} as any;
265
+ middleware(req, res, () => {
266
+ nextCalled = true;
267
+ });
268
+
269
+ expect(nextCalled).toBe(true);
270
+ expect(capturedErrors.length).toBeGreaterThan(0);
271
+ });
272
+
273
+ it("calls global onValidationError when no per-route handler", () => {
274
+ let capturedErrors: ErrorObject[] = [];
275
+ configureOpenApiValidator({
276
+ onValidationError: (errors) => {
277
+ capturedErrors = errors;
278
+ },
279
+ });
280
+
281
+ const middleware = validateRequestBody({name: {required: true, type: "string"}});
282
+
283
+ let nextCalled = false;
284
+ const req = {body: {}, method: "POST", path: "/test"} as any;
285
+ const res = {} as any;
286
+ middleware(req, res, () => {
287
+ nextCalled = true;
288
+ });
289
+
290
+ expect(nextCalled).toBe(true);
291
+ expect(capturedErrors.length).toBeGreaterThan(0);
292
+ });
293
+
294
+ it("skips validation when enabled=false on options", () => {
295
+ configureOpenApiValidator();
296
+
297
+ const middleware = validateRequestBody(
298
+ {name: {required: true, type: "string"}},
299
+ {enabled: false}
300
+ );
301
+
302
+ let nextCalled = false;
303
+ const req = {body: {}, method: "POST", path: "/test"} as any;
304
+ const res = {} as any;
305
+ middleware(req, res, () => {
306
+ nextCalled = true;
307
+ });
308
+
309
+ expect(nextCalled).toBe(true);
310
+ });
311
+
312
+ it("respects validateRequests=false in global config", () => {
313
+ configureOpenApiValidator({validateRequests: false});
314
+
315
+ const middleware = validateRequestBody({name: {required: true, type: "string"}});
316
+
317
+ let nextCalled = false;
318
+ const req = {body: {}, method: "POST", path: "/test"} as any;
319
+ const res = {} as any;
320
+ middleware(req, res, () => {
321
+ nextCalled = true;
322
+ });
323
+
324
+ expect(nextCalled).toBe(true);
325
+ });
326
+ });
327
+
328
+ describe("validateQueryParams middleware", () => {
329
+ it("is a no-op when not configured", () => {
330
+ resetOpenApiValidatorConfig();
331
+
332
+ const middleware = validateQueryParams({
333
+ page: {type: "number"},
334
+ });
335
+
336
+ let nextCalled = false;
337
+ const req = {method: "GET", path: "/test", query: {}} as any;
338
+ const res = {} as any;
339
+ middleware(req, res, () => {
340
+ nextCalled = true;
341
+ });
342
+
343
+ expect(nextCalled).toBe(true);
344
+ });
345
+
346
+ it("coerces types when configured", () => {
347
+ configureOpenApiValidator({coerceTypes: true});
348
+
349
+ const middleware = validateQueryParams({
350
+ page: {type: "number"},
351
+ });
352
+
353
+ const req = {method: "GET", path: "/test", query: {page: "3"}} as any;
354
+ const res = {} as any;
355
+ middleware(req, res, () => {});
356
+
357
+ expect(req.query.page).toBe(3);
358
+ });
359
+
360
+ it("throws validation error when query does not match", () => {
361
+ configureOpenApiValidator({coerceTypes: false});
362
+
363
+ const middleware = validateQueryParams({
364
+ page: {required: true, type: "number"},
365
+ });
366
+
367
+ const req = {method: "GET", path: "/test", query: {}} as any;
368
+ const res = {} as any;
369
+ // Required top-level property missing triggers an error
370
+ // We need required: [] at schema level via required: true on property. Confirm via calling.
371
+ expect(() => {
372
+ middleware(req, res, () => {});
373
+ }).toThrow();
374
+ });
375
+
376
+ it("uses onError callback for query validation", () => {
377
+ let captured: any[] = [];
378
+ configureOpenApiValidator({coerceTypes: false});
379
+
380
+ const middleware = validateQueryParams(
381
+ {page: {required: true, type: "number"}},
382
+ {
383
+ onError: (errors) => {
384
+ captured = errors;
385
+ },
386
+ }
387
+ );
388
+
389
+ let nextCalled = false;
390
+ const req = {method: "GET", path: "/test", query: {}} as any;
391
+ const res = {} as any;
392
+ middleware(req, res, () => {
393
+ nextCalled = true;
394
+ });
395
+
396
+ expect(nextCalled).toBe(true);
397
+ expect(captured.length).toBeGreaterThan(0);
398
+ });
399
+
400
+ it("skips query validation when enabled=false", () => {
401
+ configureOpenApiValidator();
402
+
403
+ const middleware = validateQueryParams(
404
+ {page: {required: true, type: "number"}},
405
+ {enabled: false}
406
+ );
407
+
408
+ let nextCalled = false;
409
+ const req = {method: "GET", path: "/test", query: {}} as any;
410
+ const res = {} as any;
411
+ middleware(req, res, () => {
412
+ nextCalled = true;
413
+ });
414
+
415
+ expect(nextCalled).toBe(true);
416
+ });
417
+ });
418
+
419
+ describe("validateResponseData", () => {
420
+ it("returns valid when validateResponses is disabled", () => {
421
+ configureOpenApiValidator({validateResponses: false});
422
+ const result = validateResponseData({foo: "bar"}, {name: {type: "string"}});
423
+ expect(result.valid).toBe(true);
424
+ });
425
+
426
+ it("validates response shape when validateResponses is enabled", () => {
427
+ configureOpenApiValidator({validateResponses: true});
428
+ const result = validateResponseData(
429
+ {name: "Apple"},
430
+ {name: {required: true, type: "string"}}
431
+ );
432
+ expect(result.valid).toBe(true);
433
+ });
434
+
435
+ it("returns errors for invalid response shape", () => {
436
+ configureOpenApiValidator({coerceTypes: false, validateResponses: true});
437
+ const result = validateResponseData(
438
+ {name: 42 as any},
439
+ {name: {required: true, type: "string"}}
440
+ );
441
+ expect(result.valid).toBe(false);
442
+ expect(result.errors).toBeDefined();
443
+ });
444
+ });
445
+
446
+ describe("createValidator", () => {
447
+ it("runs body then query validation and calls next once both pass", () => {
448
+ configureOpenApiValidator({coerceTypes: true});
449
+
450
+ const middleware = createValidator({
451
+ body: {name: {required: true, type: "string"}},
452
+ query: {page: {type: "number"}},
453
+ });
454
+
455
+ let nextCalled = false;
456
+ const req = {
457
+ body: {name: "ok"},
458
+ method: "POST",
459
+ path: "/test",
460
+ query: {page: "2"},
461
+ } as any;
462
+ const res = {} as any;
463
+ middleware(req, res, () => {
464
+ nextCalled = true;
465
+ });
466
+
467
+ expect(nextCalled).toBe(true);
468
+ expect(req.query.page).toBe(2);
469
+ });
470
+
471
+ it("skips when neither body nor query schemas are provided", () => {
472
+ configureOpenApiValidator();
473
+
474
+ const middleware = createValidator({});
475
+
476
+ let nextCalled = false;
477
+ const req = {body: {}, method: "POST", path: "/test", query: {}} as any;
478
+ const res = {} as any;
479
+ middleware(req, res, () => {
480
+ nextCalled = true;
481
+ });
482
+
483
+ expect(nextCalled).toBe(true);
484
+ });
485
+
486
+ it("runs only query validation when only query provided", () => {
487
+ configureOpenApiValidator({coerceTypes: true});
488
+
489
+ const middleware = createValidator({
490
+ query: {page: {type: "number"}},
491
+ });
492
+
493
+ let nextCalled = false;
494
+ const req = {method: "GET", path: "/test", query: {page: "5"}} as any;
495
+ const res = {} as any;
496
+ middleware(req, res, () => {
497
+ nextCalled = true;
498
+ });
499
+
500
+ expect(nextCalled).toBe(true);
501
+ expect(req.query.page).toBe(5);
502
+ });
503
+
504
+ it("propagates body validation error via next", () => {
505
+ let capturedErrors: ErrorObject[] = [];
506
+ configureOpenApiValidator({
507
+ onValidationError: (errors) => {
508
+ capturedErrors = errors;
509
+ },
510
+ });
511
+
512
+ const middleware = createValidator({
513
+ body: {name: {required: true, type: "string"}},
514
+ query: {page: {type: "number"}},
515
+ });
516
+
517
+ const req = {body: {}, method: "POST", path: "/test", query: {}} as any;
518
+ const res = {} as any;
519
+
520
+ let nextCalled = false;
521
+ middleware(req, res, () => {
522
+ nextCalled = true;
523
+ });
524
+
525
+ expect(capturedErrors.length).toBeGreaterThan(0);
526
+ expect(nextCalled).toBe(true);
527
+ });
528
+ });
529
+
530
+ describe("createModelValidators", () => {
531
+ beforeEach(() => {
532
+ configureOpenApiValidator();
533
+ });
534
+
535
+ it("returns create and update middleware", () => {
536
+ const validators = createModelValidators(RequiredModel);
537
+ expect(typeof validators.create).toBe("function");
538
+ expect(typeof validators.update).toBe("function");
539
+ });
540
+
541
+ it("create validator rejects body with wrong type", () => {
542
+ configureOpenApiValidator({coerceTypes: false});
543
+ const {create} = createModelValidators(RequiredModel);
544
+
545
+ const req = {body: {name: 123}, method: "POST", path: "/required"} as any;
546
+ const res = {} as any;
547
+ expect(() => {
548
+ create(req, res, () => {});
549
+ }).toThrow();
550
+ });
551
+
552
+ it("update validator allows a partial body", () => {
553
+ const {update} = createModelValidators(RequiredModel);
554
+
555
+ let nextCalled = false;
556
+ const req = {body: {about: "info"}, method: "PATCH", path: "/required"} as any;
557
+ const res = {} as any;
558
+ update(req, res, () => {
559
+ nextCalled = true;
560
+ });
561
+
562
+ expect(nextCalled).toBe(true);
563
+ });
564
+
565
+ it("supports onAdditionalPropertiesRemoved option", () => {
566
+ configureOpenApiValidator({removeAdditional: true});
567
+ const removedProps: string[] = [];
568
+ const {create} = createModelValidators(RequiredModel, {
569
+ onAdditionalPropertiesRemoved: (props) => {
570
+ removedProps.push(...props);
571
+ },
572
+ });
573
+
574
+ // Extra property gets stripped and hook is called
575
+ const req = {
576
+ body: {extra: "strip me", name: "Apple"},
577
+ method: "POST",
578
+ path: "/required",
579
+ } as any;
580
+ create(req, {} as any, () => {});
581
+ expect(removedProps).toContain("extra");
582
+ });
583
+
584
+ it("supports onError option", () => {
585
+ configureOpenApiValidator({coerceTypes: false});
586
+ let errorHandled: any[] = [];
587
+ const {create} = createModelValidators(RequiredModel, {
588
+ onError: (errors) => {
589
+ errorHandled = errors;
590
+ },
591
+ });
592
+
593
+ // Wrong type triggers error handler
594
+ const req = {body: {name: 42}, method: "POST", path: "/required"} as any;
595
+ create(req, {} as any, () => {});
596
+ expect(errorHandled.length).toBeGreaterThan(0);
597
+ });
598
+ });
599
+
600
+ describe("validateModelRequestBody", () => {
601
+ beforeEach(() => {
602
+ configureOpenApiValidator();
603
+ });
604
+
605
+ it("creates a validator from a Mongoose model", () => {
606
+ const middleware = validateModelRequestBody(RequiredModel);
607
+
608
+ const req = {body: {}, method: "POST", path: "/required"} as any;
609
+ const res = {} as any;
610
+ expect(() => {
611
+ middleware(req, res, () => {});
612
+ }).toThrow();
613
+ });
614
+
615
+ it("respects excludeFields option", () => {
616
+ const middleware = validateModelRequestBody(RequiredModel, {
617
+ excludeFields: ["name"],
618
+ });
619
+
620
+ // Without 'name' required, empty body passes
621
+ let nextCalled = false;
622
+ const req = {body: {}, method: "POST", path: "/required"} as any;
623
+ const res = {} as any;
624
+ middleware(req, res, () => {
625
+ nextCalled = true;
626
+ });
627
+
628
+ expect(nextCalled).toBe(true);
629
+ });
630
+ });
631
+
632
+ describe("getSchemaFromModel", () => {
633
+ it("returns properties for a Mongoose model", () => {
634
+ const schema = getSchemaFromModel(RequiredModel);
635
+ expect(schema.name).toBeDefined();
636
+ expect(schema.about).toBeDefined();
637
+ });
638
+ });
639
+
640
+ describe("getOpenApiValidatorConfig", () => {
641
+ it("returns a shallow copy of the config", () => {
642
+ configureOpenApiValidator({removeAdditional: false});
643
+ const config = getOpenApiValidatorConfig();
644
+ expect(config.removeAdditional).toBe(false);
645
+
646
+ // Mutating the returned copy should not affect internal state
647
+ (config as any).removeAdditional = true;
648
+ expect(getOpenApiValidatorConfig().removeAdditional).toBe(false);
649
+ });
240
650
  });
241
651
  });
@@ -0,0 +1,197 @@
1
+ import {describe, expect, it, mock, spyOn} from "bun:test";
2
+ import * as Sentry from "@sentry/bun";
3
+ import type express from "express";
4
+
5
+ import {APIError} from "./errors";
6
+ import {Permissions, permissionMiddleware} from "./permissions";
7
+
8
+ describe("permissionMiddleware", () => {
9
+ const allPermissions = {
10
+ create: [Permissions.IsAny],
11
+ delete: [Permissions.IsAny],
12
+ list: [Permissions.IsAny],
13
+ read: [Permissions.IsAny],
14
+ update: [Permissions.IsAny],
15
+ };
16
+
17
+ const buildReq = (overrides: Record<string, unknown> = {}): express.Request => {
18
+ return {
19
+ method: "GET",
20
+ params: {},
21
+ user: {id: "user-1"},
22
+ ...overrides,
23
+ } as unknown as express.Request;
24
+ };
25
+
26
+ it("calls next immediately for OPTIONS requests", async () => {
27
+ const model = {
28
+ collection: {findOne: mock(async () => null)},
29
+ findById: mock(() => ({exec: mock(async () => null)})),
30
+ modelName: "MockModel",
31
+ } as any;
32
+ const middleware = permissionMiddleware(model, {permissions: allPermissions});
33
+ const next = mock(() => {});
34
+
35
+ await middleware(buildReq({method: "OPTIONS"}), {} as express.Response, next as any);
36
+
37
+ expect(next).toHaveBeenCalledTimes(1);
38
+ expect((next as any).mock.calls[0]).toEqual([]);
39
+ expect(model.findById).toHaveBeenCalledTimes(0);
40
+ });
41
+
42
+ it("returns APIError for unsupported HTTP methods", async () => {
43
+ const model = {
44
+ collection: {findOne: mock(async () => null)},
45
+ findById: mock(() => ({exec: mock(async () => null)})),
46
+ modelName: "MockModel",
47
+ } as any;
48
+ const middleware = permissionMiddleware(model, {permissions: allPermissions});
49
+ const next = mock(() => {});
50
+
51
+ await middleware(buildReq({method: "TRACE"}), {} as express.Response, next as any);
52
+
53
+ expect(next).toHaveBeenCalledTimes(1);
54
+ const [error] = (next as any).mock.calls[0];
55
+ expect(error).toBeInstanceOf(APIError);
56
+ expect(error.status).toBe(405);
57
+ expect(error.title).toContain("Method TRACE not allowed");
58
+ });
59
+
60
+ it("wraps query execution failures in a 500 APIError", async () => {
61
+ const exec = mock(async () => {
62
+ throw new Error("query failed");
63
+ });
64
+ const model = {
65
+ collection: {findOne: mock(async () => null)},
66
+ findById: mock(() => ({exec})),
67
+ modelName: "MockModel",
68
+ } as any;
69
+ const middleware = permissionMiddleware(model, {permissions: allPermissions});
70
+ const next = mock(() => {});
71
+
72
+ await middleware(
73
+ buildReq({method: "GET", params: {id: "507f1f77bcf86cd799439011"}}),
74
+ {} as express.Response,
75
+ next as any
76
+ );
77
+
78
+ expect(exec).toHaveBeenCalledTimes(1);
79
+ const [error] = (next as any).mock.calls[0];
80
+ expect(error).toBeInstanceOf(APIError);
81
+ expect(error.status).toBe(500);
82
+ expect(error.title).toContain("GET failed on 507f1f77bcf86cd799439011");
83
+ });
84
+
85
+ it("captures sentry message when document does not exist", async () => {
86
+ const captureMessageSpy = spyOn(Sentry, "captureMessage");
87
+ const model = {
88
+ collection: {findOne: mock(async () => null)},
89
+ findById: mock(() => ({exec: mock(async () => null)})),
90
+ modelName: "MockModel",
91
+ } as any;
92
+ const middleware = permissionMiddleware(model, {permissions: allPermissions});
93
+ const next = mock(() => {});
94
+
95
+ await middleware(
96
+ buildReq({method: "GET", params: {id: "507f1f77bcf86cd799439011"}}),
97
+ {} as express.Response,
98
+ next as any
99
+ );
100
+
101
+ expect(captureMessageSpy).toHaveBeenCalledWith(
102
+ "Document 507f1f77bcf86cd799439011 not found for model MockModel"
103
+ );
104
+ const [error] = (next as any).mock.calls[0];
105
+ expect(error).toBeInstanceOf(APIError);
106
+ expect(error.status).toBe(404);
107
+ expect(error.meta).toBeUndefined();
108
+ captureMessageSpy.mockRestore();
109
+ });
110
+
111
+ it("returns hidden reason metadata when document is deleted", async () => {
112
+ const model = {
113
+ collection: {findOne: mock(async () => ({deleted: true}))},
114
+ findById: mock(() => ({exec: mock(async () => null)})),
115
+ modelName: "MockModel",
116
+ } as any;
117
+ const middleware = permissionMiddleware(model, {permissions: allPermissions});
118
+ const next = mock(() => {});
119
+
120
+ await middleware(
121
+ buildReq({method: "GET", params: {id: "507f1f77bcf86cd799439011"}}),
122
+ {} as express.Response,
123
+ next as any
124
+ );
125
+
126
+ const [error] = (next as any).mock.calls[0];
127
+ expect(error).toBeInstanceOf(APIError);
128
+ expect(error.status).toBe(404);
129
+ expect(error.meta).toEqual({deleted: "true"});
130
+ expect(error.disableExternalErrorTracking).toBe(true);
131
+ });
132
+
133
+ it("returns hidden reason metadata when document is disabled", async () => {
134
+ const model = {
135
+ collection: {findOne: mock(async () => ({disabled: true}))},
136
+ findById: mock(() => ({exec: mock(async () => null)})),
137
+ modelName: "MockModel",
138
+ } as any;
139
+ const middleware = permissionMiddleware(model, {permissions: allPermissions});
140
+ const next = mock(() => {});
141
+
142
+ await middleware(
143
+ buildReq({method: "GET", params: {id: "507f1f77bcf86cd799439011"}}),
144
+ {} as express.Response,
145
+ next as any
146
+ );
147
+
148
+ const [error] = (next as any).mock.calls[0];
149
+ expect(error).toBeInstanceOf(APIError);
150
+ expect(error.status).toBe(404);
151
+ expect(error.meta).toEqual({disabled: "true"});
152
+ expect(error.disableExternalErrorTracking).toBe(true);
153
+ });
154
+
155
+ it("returns hidden reason metadata when document is archived", async () => {
156
+ const model = {
157
+ collection: {findOne: mock(async () => ({archived: true}))},
158
+ findById: mock(() => ({exec: mock(async () => null)})),
159
+ modelName: "MockModel",
160
+ } as any;
161
+ const middleware = permissionMiddleware(model, {permissions: allPermissions});
162
+ const next = mock(() => {});
163
+
164
+ await middleware(
165
+ buildReq({method: "GET", params: {id: "507f1f77bcf86cd799439011"}}),
166
+ {} as express.Response,
167
+ next as any
168
+ );
169
+
170
+ const [error] = (next as any).mock.calls[0];
171
+ expect(error).toBeInstanceOf(APIError);
172
+ expect(error.status).toBe(404);
173
+ expect(error.meta).toEqual({archived: "true"});
174
+ expect(error.disableExternalErrorTracking).toBe(true);
175
+ });
176
+
177
+ it("returns plain not found when hidden document has no reason", async () => {
178
+ const model = {
179
+ collection: {findOne: mock(async () => ({foo: "bar"}))},
180
+ findById: mock(() => ({exec: mock(async () => null)})),
181
+ modelName: "MockModel",
182
+ } as any;
183
+ const middleware = permissionMiddleware(model, {permissions: allPermissions});
184
+ const next = mock(() => {});
185
+
186
+ await middleware(
187
+ buildReq({method: "GET", params: {id: "507f1f77bcf86cd799439011"}}),
188
+ {} as express.Response,
189
+ next as any
190
+ );
191
+
192
+ const [error] = (next as any).mock.calls[0];
193
+ expect(error).toBeInstanceOf(APIError);
194
+ expect(error.status).toBe(404);
195
+ expect(error.meta).toBeUndefined();
196
+ });
197
+ });