@terreno/api 0.9.3 → 0.10.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.
@@ -0,0 +1,3 @@
1
+ [test]
2
+ root = "./src"
3
+ preload = []
@@ -341,16 +341,10 @@ function setupServer(options) {
341
341
  }
342
342
  // Convenience method to execute cronjobs with an always-running server.
343
343
  function cronjob(name, schedule, callback) {
344
- var _cronSchedule = schedule;
345
- if (schedule === "hourly") {
346
- _cronSchedule = "0 * * * *";
347
- }
348
- else if (schedule === "minutely") {
349
- _cronSchedule = "* * * * *";
350
- }
351
- logger_1.logger.info("Adding cronjob ".concat(name, ", running at: ").concat(schedule));
344
+ var cronSchedule = schedule === "hourly" ? "0 * * * *" : schedule === "minutely" ? "* * * * *" : schedule;
345
+ logger_1.logger.info("Adding cronjob ".concat(name, ", running at: ").concat(cronSchedule));
352
346
  try {
353
- new cron_1.default.CronJob(schedule, callback, null, true, "America/Chicago");
347
+ new cron_1.default.CronJob(cronSchedule, callback, null, true, "America/Chicago");
354
348
  }
355
349
  catch (error) {
356
350
  throw new Error("Failed to create cronjob: ".concat(error));
@@ -383,16 +383,13 @@ var tests_1 = require("./tests");
383
383
  var callback = function () { };
384
384
  (0, bun_test_1.expect)(function () { return (0, expressServer_1.cronjob)("test-invalid", "invalid-cron", callback); }).toThrow("Failed to create cronjob");
385
385
  });
386
- // Note: The "hourly" and "minutely" aliases have a bug - they convert the
387
- // schedule to a cron expression but then use the original schedule string.
388
- // This test documents that current (buggy) behavior.
389
- (0, bun_test_1.it)("hourly alias fails due to bug in implementation", function () {
386
+ (0, bun_test_1.it)("accepts hourly alias", function () {
390
387
  var callback = function () { };
391
- (0, bun_test_1.expect)(function () { return (0, expressServer_1.cronjob)("test-hourly-alias", "hourly", callback); }).toThrow("Failed to create cronjob");
388
+ (0, bun_test_1.expect)(function () { return (0, expressServer_1.cronjob)("test-hourly-alias", "hourly", callback); }).not.toThrow();
392
389
  });
393
- (0, bun_test_1.it)("minutely alias fails due to bug in implementation", function () {
390
+ (0, bun_test_1.it)("accepts minutely alias", function () {
394
391
  var callback = function () { };
395
- (0, bun_test_1.expect)(function () { return (0, expressServer_1.cronjob)("test-minutely-alias", "minutely", callback); }).toThrow("Failed to create cronjob");
392
+ (0, bun_test_1.expect)(function () { return (0, expressServer_1.cronjob)("test-minutely-alias", "minutely", callback); }).not.toThrow();
396
393
  });
397
394
  });
398
395
  (0, bun_test_1.describe)("createRouter routePathMiddleware", function () {
@@ -74,6 +74,9 @@ var expressServer_1 = require("./expressServer");
74
74
  var permissions_1 = require("./permissions");
75
75
  var tests_1 = require("./tests");
76
76
  function getMessageSummaryOpenApiMiddleware(options) {
77
+ if (!options.openApi) {
78
+ throw new Error("Expected openApi to be configured for test routes");
79
+ }
77
80
  return options.openApi.path({
78
81
  parameters: [
79
82
  {
@@ -45,6 +45,14 @@ var buildResponse = function () {
45
45
  (0, bun_test_1.expect)(next).toHaveBeenCalledTimes(1);
46
46
  (0, bun_test_1.expect)(res.json).toBe(originalJson);
47
47
  });
48
+ (0, bun_test_1.it)("skips GET requests for non-openapi.json paths", function () {
49
+ var req = buildRequest({ method: "GET", path: "/health" });
50
+ var _a = buildResponse(), res = _a.res, originalJson = _a.originalJson;
51
+ var next = (0, bun_test_1.mock)(function () { });
52
+ (0, openApiEtag_1.openApiEtagMiddleware)(req, res, next);
53
+ (0, bun_test_1.expect)(next).toHaveBeenCalledTimes(1);
54
+ (0, bun_test_1.expect)(res.json).toBe(originalJson);
55
+ });
48
56
  (0, bun_test_1.it)("sets ETag and returns json body when no matching If-None-Match header is provided", function () {
49
57
  var req = buildRequest();
50
58
  var _a = buildResponse(), res = _a.res, originalJson = _a.originalJson, set = _a.set, status = _a.status, end = _a.end;
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,341 @@
1
+ "use strict";
2
+ var __assign = (this && this.__assign) || function () {
3
+ __assign = Object.assign || function(t) {
4
+ for (var s, i = 1, n = arguments.length; i < n; i++) {
5
+ s = arguments[i];
6
+ for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p))
7
+ t[p] = s[p];
8
+ }
9
+ return t;
10
+ };
11
+ return __assign.apply(this, arguments);
12
+ };
13
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
14
+ if (k2 === undefined) k2 = k;
15
+ var desc = Object.getOwnPropertyDescriptor(m, k);
16
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
17
+ desc = { enumerable: true, get: function() { return m[k]; } };
18
+ }
19
+ Object.defineProperty(o, k2, desc);
20
+ }) : (function(o, m, k, k2) {
21
+ if (k2 === undefined) k2 = k;
22
+ o[k2] = m[k];
23
+ }));
24
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
25
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
26
+ }) : function(o, v) {
27
+ o["default"] = v;
28
+ });
29
+ var __importStar = (this && this.__importStar) || (function () {
30
+ var ownKeys = function(o) {
31
+ ownKeys = Object.getOwnPropertyNames || function (o) {
32
+ var ar = [];
33
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
34
+ return ar;
35
+ };
36
+ return ownKeys(o);
37
+ };
38
+ return function (mod) {
39
+ if (mod && mod.__esModule) return mod;
40
+ var result = {};
41
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
42
+ __setModuleDefault(result, mod);
43
+ return result;
44
+ };
45
+ })();
46
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
47
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
48
+ return new (P || (P = Promise))(function (resolve, reject) {
49
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
50
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
51
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
52
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
53
+ });
54
+ };
55
+ var __generator = (this && this.__generator) || function (thisArg, body) {
56
+ var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype);
57
+ return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
58
+ function verb(n) { return function (v) { return step([n, v]); }; }
59
+ function step(op) {
60
+ if (f) throw new TypeError("Generator is already executing.");
61
+ while (g && (g = 0, op[0] && (_ = 0)), _) try {
62
+ if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
63
+ if (y = 0, t) op = [op[0] & 2, t.value];
64
+ switch (op[0]) {
65
+ case 0: case 1: t = op; break;
66
+ case 4: _.label++; return { value: op[1], done: false };
67
+ case 5: _.label++; y = op[1]; op = [0]; continue;
68
+ case 7: op = _.ops.pop(); _.trys.pop(); continue;
69
+ default:
70
+ if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
71
+ if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
72
+ if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
73
+ if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
74
+ if (t[2]) _.ops.pop();
75
+ _.trys.pop(); continue;
76
+ }
77
+ op = body.call(thisArg, _);
78
+ } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
79
+ if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
80
+ }
81
+ };
82
+ var __read = (this && this.__read) || function (o, n) {
83
+ var m = typeof Symbol === "function" && o[Symbol.iterator];
84
+ if (!m) return o;
85
+ var i = m.call(o), r, ar = [], e;
86
+ try {
87
+ while ((n === void 0 || n-- > 0) && !(r = i.next()).done) ar.push(r.value);
88
+ }
89
+ catch (error) { e = { error: error }; }
90
+ finally {
91
+ try {
92
+ if (r && !r.done && (m = i["return"])) m.call(i);
93
+ }
94
+ finally { if (e) throw e.error; }
95
+ }
96
+ return ar;
97
+ };
98
+ Object.defineProperty(exports, "__esModule", { value: true });
99
+ var bun_test_1 = require("bun:test");
100
+ var Sentry = __importStar(require("@sentry/bun"));
101
+ var errors_1 = require("./errors");
102
+ var permissions_1 = require("./permissions");
103
+ (0, bun_test_1.describe)("permissionMiddleware", function () {
104
+ var allPermissions = {
105
+ create: [permissions_1.Permissions.IsAny],
106
+ delete: [permissions_1.Permissions.IsAny],
107
+ list: [permissions_1.Permissions.IsAny],
108
+ read: [permissions_1.Permissions.IsAny],
109
+ update: [permissions_1.Permissions.IsAny],
110
+ };
111
+ var buildReq = function (overrides) {
112
+ if (overrides === void 0) { overrides = {}; }
113
+ return __assign({ method: "GET", params: {}, user: { id: "user-1" } }, overrides);
114
+ };
115
+ (0, bun_test_1.it)("calls next immediately for OPTIONS requests", function () { return __awaiter(void 0, void 0, void 0, function () {
116
+ var model, middleware, next;
117
+ return __generator(this, function (_a) {
118
+ switch (_a.label) {
119
+ case 0:
120
+ model = {
121
+ collection: { findOne: (0, bun_test_1.mock)(function () { return __awaiter(void 0, void 0, void 0, function () { return __generator(this, function (_a) {
122
+ return [2 /*return*/, null];
123
+ }); }); }) },
124
+ findById: (0, bun_test_1.mock)(function () { return ({ exec: (0, bun_test_1.mock)(function () { return __awaiter(void 0, void 0, void 0, function () { return __generator(this, function (_a) {
125
+ return [2 /*return*/, null];
126
+ }); }); }) }); }),
127
+ modelName: "MockModel",
128
+ };
129
+ middleware = (0, permissions_1.permissionMiddleware)(model, { permissions: allPermissions });
130
+ next = (0, bun_test_1.mock)(function () { });
131
+ return [4 /*yield*/, middleware(buildReq({ method: "OPTIONS" }), {}, next)];
132
+ case 1:
133
+ _a.sent();
134
+ (0, bun_test_1.expect)(next).toHaveBeenCalledTimes(1);
135
+ (0, bun_test_1.expect)(next.mock.calls[0]).toEqual([]);
136
+ (0, bun_test_1.expect)(model.findById).toHaveBeenCalledTimes(0);
137
+ return [2 /*return*/];
138
+ }
139
+ });
140
+ }); });
141
+ (0, bun_test_1.it)("returns APIError for unsupported HTTP methods", function () { return __awaiter(void 0, void 0, void 0, function () {
142
+ var model, middleware, next, _a, error;
143
+ return __generator(this, function (_b) {
144
+ switch (_b.label) {
145
+ case 0:
146
+ model = {
147
+ collection: { findOne: (0, bun_test_1.mock)(function () { return __awaiter(void 0, void 0, void 0, function () { return __generator(this, function (_a) {
148
+ return [2 /*return*/, null];
149
+ }); }); }) },
150
+ findById: (0, bun_test_1.mock)(function () { return ({ exec: (0, bun_test_1.mock)(function () { return __awaiter(void 0, void 0, void 0, function () { return __generator(this, function (_a) {
151
+ return [2 /*return*/, null];
152
+ }); }); }) }); }),
153
+ modelName: "MockModel",
154
+ };
155
+ middleware = (0, permissions_1.permissionMiddleware)(model, { permissions: allPermissions });
156
+ next = (0, bun_test_1.mock)(function () { });
157
+ return [4 /*yield*/, middleware(buildReq({ method: "TRACE" }), {}, next)];
158
+ case 1:
159
+ _b.sent();
160
+ (0, bun_test_1.expect)(next).toHaveBeenCalledTimes(1);
161
+ _a = __read(next.mock.calls[0], 1), error = _a[0];
162
+ (0, bun_test_1.expect)(error).toBeInstanceOf(errors_1.APIError);
163
+ (0, bun_test_1.expect)(error.status).toBe(405);
164
+ (0, bun_test_1.expect)(error.title).toContain("Method TRACE not allowed");
165
+ return [2 /*return*/];
166
+ }
167
+ });
168
+ }); });
169
+ (0, bun_test_1.it)("wraps query execution failures in a 500 APIError", function () { return __awaiter(void 0, void 0, void 0, function () {
170
+ var exec, model, middleware, next, _a, error;
171
+ return __generator(this, function (_b) {
172
+ switch (_b.label) {
173
+ case 0:
174
+ exec = (0, bun_test_1.mock)(function () { return __awaiter(void 0, void 0, void 0, function () {
175
+ return __generator(this, function (_a) {
176
+ throw new Error("query failed");
177
+ });
178
+ }); });
179
+ model = {
180
+ collection: { findOne: (0, bun_test_1.mock)(function () { return __awaiter(void 0, void 0, void 0, function () { return __generator(this, function (_a) {
181
+ return [2 /*return*/, null];
182
+ }); }); }) },
183
+ findById: (0, bun_test_1.mock)(function () { return ({ exec: exec }); }),
184
+ modelName: "MockModel",
185
+ };
186
+ middleware = (0, permissions_1.permissionMiddleware)(model, { permissions: allPermissions });
187
+ next = (0, bun_test_1.mock)(function () { });
188
+ return [4 /*yield*/, middleware(buildReq({ method: "GET", params: { id: "507f1f77bcf86cd799439011" } }), {}, next)];
189
+ case 1:
190
+ _b.sent();
191
+ (0, bun_test_1.expect)(exec).toHaveBeenCalledTimes(1);
192
+ _a = __read(next.mock.calls[0], 1), error = _a[0];
193
+ (0, bun_test_1.expect)(error).toBeInstanceOf(errors_1.APIError);
194
+ (0, bun_test_1.expect)(error.status).toBe(500);
195
+ (0, bun_test_1.expect)(error.title).toContain("GET failed on 507f1f77bcf86cd799439011");
196
+ return [2 /*return*/];
197
+ }
198
+ });
199
+ }); });
200
+ (0, bun_test_1.it)("captures sentry message when document does not exist", function () { return __awaiter(void 0, void 0, void 0, function () {
201
+ var captureMessageSpy, model, middleware, next, _a, error;
202
+ return __generator(this, function (_b) {
203
+ switch (_b.label) {
204
+ case 0:
205
+ captureMessageSpy = (0, bun_test_1.spyOn)(Sentry, "captureMessage");
206
+ model = {
207
+ collection: { findOne: (0, bun_test_1.mock)(function () { return __awaiter(void 0, void 0, void 0, function () { return __generator(this, function (_a) {
208
+ return [2 /*return*/, null];
209
+ }); }); }) },
210
+ findById: (0, bun_test_1.mock)(function () { return ({ exec: (0, bun_test_1.mock)(function () { return __awaiter(void 0, void 0, void 0, function () { return __generator(this, function (_a) {
211
+ return [2 /*return*/, null];
212
+ }); }); }) }); }),
213
+ modelName: "MockModel",
214
+ };
215
+ middleware = (0, permissions_1.permissionMiddleware)(model, { permissions: allPermissions });
216
+ next = (0, bun_test_1.mock)(function () { });
217
+ return [4 /*yield*/, middleware(buildReq({ method: "GET", params: { id: "507f1f77bcf86cd799439011" } }), {}, next)];
218
+ case 1:
219
+ _b.sent();
220
+ (0, bun_test_1.expect)(captureMessageSpy).toHaveBeenCalledWith("Document 507f1f77bcf86cd799439011 not found for model MockModel");
221
+ _a = __read(next.mock.calls[0], 1), error = _a[0];
222
+ (0, bun_test_1.expect)(error).toBeInstanceOf(errors_1.APIError);
223
+ (0, bun_test_1.expect)(error.status).toBe(404);
224
+ (0, bun_test_1.expect)(error.meta).toBeUndefined();
225
+ captureMessageSpy.mockRestore();
226
+ return [2 /*return*/];
227
+ }
228
+ });
229
+ }); });
230
+ (0, bun_test_1.it)("returns hidden reason metadata when document is deleted", function () { return __awaiter(void 0, void 0, void 0, function () {
231
+ var model, middleware, next, _a, error;
232
+ return __generator(this, function (_b) {
233
+ switch (_b.label) {
234
+ case 0:
235
+ model = {
236
+ collection: { findOne: (0, bun_test_1.mock)(function () { return __awaiter(void 0, void 0, void 0, function () { return __generator(this, function (_a) {
237
+ return [2 /*return*/, ({ deleted: true })];
238
+ }); }); }) },
239
+ findById: (0, bun_test_1.mock)(function () { return ({ exec: (0, bun_test_1.mock)(function () { return __awaiter(void 0, void 0, void 0, function () { return __generator(this, function (_a) {
240
+ return [2 /*return*/, null];
241
+ }); }); }) }); }),
242
+ modelName: "MockModel",
243
+ };
244
+ middleware = (0, permissions_1.permissionMiddleware)(model, { permissions: allPermissions });
245
+ next = (0, bun_test_1.mock)(function () { });
246
+ return [4 /*yield*/, middleware(buildReq({ method: "GET", params: { id: "507f1f77bcf86cd799439011" } }), {}, next)];
247
+ case 1:
248
+ _b.sent();
249
+ _a = __read(next.mock.calls[0], 1), error = _a[0];
250
+ (0, bun_test_1.expect)(error).toBeInstanceOf(errors_1.APIError);
251
+ (0, bun_test_1.expect)(error.status).toBe(404);
252
+ (0, bun_test_1.expect)(error.meta).toEqual({ deleted: "true" });
253
+ (0, bun_test_1.expect)(error.disableExternalErrorTracking).toBe(true);
254
+ return [2 /*return*/];
255
+ }
256
+ });
257
+ }); });
258
+ (0, bun_test_1.it)("returns hidden reason metadata when document is disabled", function () { return __awaiter(void 0, void 0, void 0, function () {
259
+ var model, middleware, next, _a, error;
260
+ return __generator(this, function (_b) {
261
+ switch (_b.label) {
262
+ case 0:
263
+ model = {
264
+ collection: { findOne: (0, bun_test_1.mock)(function () { return __awaiter(void 0, void 0, void 0, function () { return __generator(this, function (_a) {
265
+ return [2 /*return*/, ({ disabled: true })];
266
+ }); }); }) },
267
+ findById: (0, bun_test_1.mock)(function () { return ({ exec: (0, bun_test_1.mock)(function () { return __awaiter(void 0, void 0, void 0, function () { return __generator(this, function (_a) {
268
+ return [2 /*return*/, null];
269
+ }); }); }) }); }),
270
+ modelName: "MockModel",
271
+ };
272
+ middleware = (0, permissions_1.permissionMiddleware)(model, { permissions: allPermissions });
273
+ next = (0, bun_test_1.mock)(function () { });
274
+ return [4 /*yield*/, middleware(buildReq({ method: "GET", params: { id: "507f1f77bcf86cd799439011" } }), {}, next)];
275
+ case 1:
276
+ _b.sent();
277
+ _a = __read(next.mock.calls[0], 1), error = _a[0];
278
+ (0, bun_test_1.expect)(error).toBeInstanceOf(errors_1.APIError);
279
+ (0, bun_test_1.expect)(error.status).toBe(404);
280
+ (0, bun_test_1.expect)(error.meta).toEqual({ disabled: "true" });
281
+ (0, bun_test_1.expect)(error.disableExternalErrorTracking).toBe(true);
282
+ return [2 /*return*/];
283
+ }
284
+ });
285
+ }); });
286
+ (0, bun_test_1.it)("returns hidden reason metadata when document is archived", function () { return __awaiter(void 0, void 0, void 0, function () {
287
+ var model, middleware, next, _a, error;
288
+ return __generator(this, function (_b) {
289
+ switch (_b.label) {
290
+ case 0:
291
+ model = {
292
+ collection: { findOne: (0, bun_test_1.mock)(function () { return __awaiter(void 0, void 0, void 0, function () { return __generator(this, function (_a) {
293
+ return [2 /*return*/, ({ archived: true })];
294
+ }); }); }) },
295
+ findById: (0, bun_test_1.mock)(function () { return ({ exec: (0, bun_test_1.mock)(function () { return __awaiter(void 0, void 0, void 0, function () { return __generator(this, function (_a) {
296
+ return [2 /*return*/, null];
297
+ }); }); }) }); }),
298
+ modelName: "MockModel",
299
+ };
300
+ middleware = (0, permissions_1.permissionMiddleware)(model, { permissions: allPermissions });
301
+ next = (0, bun_test_1.mock)(function () { });
302
+ return [4 /*yield*/, middleware(buildReq({ method: "GET", params: { id: "507f1f77bcf86cd799439011" } }), {}, next)];
303
+ case 1:
304
+ _b.sent();
305
+ _a = __read(next.mock.calls[0], 1), error = _a[0];
306
+ (0, bun_test_1.expect)(error).toBeInstanceOf(errors_1.APIError);
307
+ (0, bun_test_1.expect)(error.status).toBe(404);
308
+ (0, bun_test_1.expect)(error.meta).toEqual({ archived: "true" });
309
+ (0, bun_test_1.expect)(error.disableExternalErrorTracking).toBe(true);
310
+ return [2 /*return*/];
311
+ }
312
+ });
313
+ }); });
314
+ (0, bun_test_1.it)("returns plain not found when hidden document has no reason", function () { return __awaiter(void 0, void 0, void 0, function () {
315
+ var model, middleware, next, _a, error;
316
+ return __generator(this, function (_b) {
317
+ switch (_b.label) {
318
+ case 0:
319
+ model = {
320
+ collection: { findOne: (0, bun_test_1.mock)(function () { return __awaiter(void 0, void 0, void 0, function () { return __generator(this, function (_a) {
321
+ return [2 /*return*/, ({ foo: "bar" })];
322
+ }); }); }) },
323
+ findById: (0, bun_test_1.mock)(function () { return ({ exec: (0, bun_test_1.mock)(function () { return __awaiter(void 0, void 0, void 0, function () { return __generator(this, function (_a) {
324
+ return [2 /*return*/, null];
325
+ }); }); }) }); }),
326
+ modelName: "MockModel",
327
+ };
328
+ middleware = (0, permissions_1.permissionMiddleware)(model, { permissions: allPermissions });
329
+ next = (0, bun_test_1.mock)(function () { });
330
+ return [4 /*yield*/, middleware(buildReq({ method: "GET", params: { id: "507f1f77bcf86cd799439011" } }), {}, next)];
331
+ case 1:
332
+ _b.sent();
333
+ _a = __read(next.mock.calls[0], 1), error = _a[0];
334
+ (0, bun_test_1.expect)(error).toBeInstanceOf(errors_1.APIError);
335
+ (0, bun_test_1.expect)(error.status).toBe(404);
336
+ (0, bun_test_1.expect)(error.meta).toBeUndefined();
337
+ return [2 /*return*/];
338
+ }
339
+ });
340
+ }); });
341
+ });
@@ -176,7 +176,7 @@ var syncConsents = function (definitions_1) {
176
176
  args_1[_i - 1] = arguments[_i];
177
177
  }
178
178
  return __awaiter(void 0, __spreadArray([definitions_1], __read(args_1), false), void 0, function (definitions, options) {
179
- var _a, deactivateRemoved, _b, dryRun, result, slugs, activeForms, activeBySlug, slugs_1, slugs_1_1, slug, def, existing, newVersion, e_1_1, activeBySlug_1, activeBySlug_1_1, _c, slug, form, e_2_1, summary;
179
+ var _a, deactivateRemoved, _b, dryRun, result, slugs, activeForms, activeBySlug, slugs_1, slugs_1_1, slug, def, existing, newVersion, e_1_1, activeBySlug_1, activeBySlug_1_1, _c, slug, e_2_1, summary;
180
180
  var e_1, _d, e_2, _e;
181
181
  var _f, _g, _h, _j;
182
182
  if (options === void 0) { options = {}; }
@@ -291,7 +291,7 @@ var syncConsents = function (definitions_1) {
291
291
  _k.label = 16;
292
292
  case 16:
293
293
  if (!!activeBySlug_1_1.done) return [3 /*break*/, 20];
294
- _c = __read(activeBySlug_1_1.value, 2), slug = _c[0], form = _c[1];
294
+ _c = __read(activeBySlug_1_1.value, 1), slug = _c[0];
295
295
  if (!!definitions[slug]) return [3 /*break*/, 19];
296
296
  logger_1.logger.info("syncConsents: deactivating \"".concat(slug, "\""), { dryRun: dryRun });
297
297
  if (!!dryRun) return [3 /*break*/, 18];
@@ -70,30 +70,35 @@ var mongoose_1 = __importDefault(require("mongoose"));
70
70
  var winston_1 = __importDefault(require("winston"));
71
71
  var expressServer_1 = require("../expressServer");
72
72
  var logger_1 = require("../logger");
73
+ var shouldConnectToTestDb = process.env.BUN_TEST_DISABLE_DB !== "true";
73
74
  // Connect to MongoDB once for all tests
74
- (0, bun_test_1.beforeAll)(function () { return __awaiter(void 0, void 0, void 0, function () {
75
- return __generator(this, function (_a) {
76
- switch (_a.label) {
77
- case 0: return [4 /*yield*/, mongoose_1.default
78
- .connect("mongodb://127.0.0.1/terreno?&connectTimeoutMS=360000")
79
- .catch(logger_1.logger.catch)];
80
- case 1:
81
- _a.sent();
82
- return [2 /*return*/];
83
- }
84
- });
85
- }); });
75
+ if (shouldConnectToTestDb) {
76
+ (0, bun_test_1.beforeAll)(function () { return __awaiter(void 0, void 0, void 0, function () {
77
+ return __generator(this, function (_a) {
78
+ switch (_a.label) {
79
+ case 0: return [4 /*yield*/, mongoose_1.default
80
+ .connect("mongodb://127.0.0.1/terreno?&connectTimeoutMS=360000")
81
+ .catch(logger_1.logger.catch)];
82
+ case 1:
83
+ _a.sent();
84
+ return [2 /*return*/];
85
+ }
86
+ });
87
+ }); });
88
+ }
86
89
  // Close MongoDB connection after all tests
87
- (0, bun_test_1.afterAll)(function () { return __awaiter(void 0, void 0, void 0, function () {
88
- return __generator(this, function (_a) {
89
- switch (_a.label) {
90
- case 0: return [4 /*yield*/, mongoose_1.default.connection.close()];
91
- case 1:
92
- _a.sent();
93
- return [2 /*return*/];
94
- }
95
- });
96
- }); });
90
+ if (shouldConnectToTestDb) {
91
+ (0, bun_test_1.afterAll)(function () { return __awaiter(void 0, void 0, void 0, function () {
92
+ return __generator(this, function (_a) {
93
+ switch (_a.label) {
94
+ case 0: return [4 /*yield*/, mongoose_1.default.connection.close()];
95
+ case 1:
96
+ _a.sent();
97
+ return [2 /*return*/];
98
+ }
99
+ });
100
+ }); });
101
+ }
97
102
  var logs = [];
98
103
  var SHOW_ALL_LOGS = process.env.SHOW_ALL_TEST_LOGS === "true";
99
104
  // Create a custom stream that captures logs
package/package.json CHANGED
@@ -103,5 +103,5 @@
103
103
  "updateSnapshot": "bun test --update-snapshots"
104
104
  },
105
105
  "types": "dist/index.d.ts",
106
- "version": "0.9.3"
106
+ "version": "0.10.0"
107
107
  }
@@ -310,21 +310,14 @@ describe("expressServer", () => {
310
310
  );
311
311
  });
312
312
 
313
- // Note: The "hourly" and "minutely" aliases have a bug - they convert the
314
- // schedule to a cron expression but then use the original schedule string.
315
- // This test documents that current (buggy) behavior.
316
- it("hourly alias fails due to bug in implementation", () => {
313
+ it("accepts hourly alias", () => {
317
314
  const callback = () => {};
318
- expect(() => cronjob("test-hourly-alias", "hourly", callback)).toThrow(
319
- "Failed to create cronjob"
320
- );
315
+ expect(() => cronjob("test-hourly-alias", "hourly", callback)).not.toThrow();
321
316
  });
322
317
 
323
- it("minutely alias fails due to bug in implementation", () => {
318
+ it("accepts minutely alias", () => {
324
319
  const callback = () => {};
325
- expect(() => cronjob("test-minutely-alias", "minutely", callback)).toThrow(
326
- "Failed to create cronjob"
327
- );
320
+ expect(() => cronjob("test-minutely-alias", "minutely", callback)).not.toThrow();
328
321
  });
329
322
  });
330
323
 
@@ -340,15 +340,11 @@ export function cronjob(
340
340
  schedule: "hourly" | "minutely" | string,
341
341
  callback: () => void
342
342
  ) {
343
- let _cronSchedule = schedule;
344
- if (schedule === "hourly") {
345
- _cronSchedule = "0 * * * *";
346
- } else if (schedule === "minutely") {
347
- _cronSchedule = "* * * * *";
348
- }
349
- logger.info(`Adding cronjob ${name}, running at: ${schedule}`);
343
+ const cronSchedule =
344
+ schedule === "hourly" ? "0 * * * *" : schedule === "minutely" ? "* * * * *" : schedule;
345
+ logger.info(`Adding cronjob ${name}, running at: ${cronSchedule}`);
350
346
  try {
351
- new cron.CronJob(schedule, callback, null, true, "America/Chicago");
347
+ new cron.CronJob(cronSchedule, callback, null, true, "America/Chicago");
352
348
  } catch (error) {
353
349
  throw new Error(`Failed to create cronjob: ${error}`);
354
350
  }
@@ -11,7 +11,11 @@ import {Permissions} from "./permissions";
11
11
  import {FoodModel, setupDb, UserModel} from "./tests";
12
12
 
13
13
  function getMessageSummaryOpenApiMiddleware(options: Partial<ModelRouterOptions<any>>): any {
14
- return options.openApi!.path({
14
+ if (!options.openApi) {
15
+ throw new Error("Expected openApi to be configured for test routes");
16
+ }
17
+
18
+ return options.openApi.path({
15
19
  parameters: [
16
20
  {
17
21
  in: "query",
@@ -61,6 +61,17 @@ describe("openApiEtagMiddleware", () => {
61
61
  expect(res.json).toBe(originalJson);
62
62
  });
63
63
 
64
+ it("skips GET requests for non-openapi.json paths", () => {
65
+ const req = buildRequest({method: "GET", path: "/health"});
66
+ const {res, originalJson} = buildResponse();
67
+ const next = mock(() => {}) as NextFunction;
68
+
69
+ openApiEtagMiddleware(req, res, next);
70
+
71
+ expect(next).toHaveBeenCalledTimes(1);
72
+ expect(res.json).toBe(originalJson);
73
+ });
74
+
64
75
  it("sets ETag and returns json body when no matching If-None-Match header is provided", () => {
65
76
  const req = buildRequest();
66
77
  const {res, originalJson, set, status, end} = buildResponse();
@@ -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
+ });
@@ -237,7 +237,7 @@ export const syncConsents = async (
237
237
 
238
238
  // Deactivate forms that are no longer in definitions
239
239
  if (deactivateRemoved) {
240
- for (const [slug, form] of activeBySlug) {
240
+ for (const [slug] of activeBySlug) {
241
241
  if (!definitions[slug]) {
242
242
  logger.info(`syncConsents: deactivating "${slug}"`, {dryRun});
243
243
  if (!dryRun) {
@@ -6,17 +6,23 @@ import winston from "winston";
6
6
  import {setupEnvironment} from "../expressServer";
7
7
  import {logger, winstonLogger} from "../logger";
8
8
 
9
+ const shouldConnectToTestDb = process.env.BUN_TEST_DISABLE_DB !== "true";
10
+
9
11
  // Connect to MongoDB once for all tests
10
- beforeAll(async () => {
11
- await mongoose
12
- .connect("mongodb://127.0.0.1/terreno?&connectTimeoutMS=360000")
13
- .catch(logger.catch);
14
- });
12
+ if (shouldConnectToTestDb) {
13
+ beforeAll(async () => {
14
+ await mongoose
15
+ .connect("mongodb://127.0.0.1/terreno?&connectTimeoutMS=360000")
16
+ .catch(logger.catch);
17
+ });
18
+ }
15
19
 
16
20
  // Close MongoDB connection after all tests
17
- afterAll(async () => {
18
- await mongoose.connection.close();
19
- });
21
+ if (shouldConnectToTestDb) {
22
+ afterAll(async () => {
23
+ await mongoose.connection.close();
24
+ });
25
+ }
20
26
 
21
27
  let logs: string[] = [];
22
28