@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.
- package/CLAUDE.md +107 -0
- package/biome.jsonc +1 -1
- package/bunfig.toml +3 -2
- package/dist/api.arrayOperations.test.d.ts +1 -0
- package/dist/api.arrayOperations.test.js +868 -0
- package/dist/api.d.ts +3 -14
- package/dist/api.errors.test.d.ts +1 -0
- package/dist/api.errors.test.js +175 -0
- package/dist/api.hooks.test.d.ts +1 -0
- package/dist/api.hooks.test.js +891 -0
- package/dist/api.js +44 -68
- package/dist/api.query.test.d.ts +1 -0
- package/dist/api.query.test.js +805 -0
- package/dist/api.test.js +691 -1678
- package/dist/auth.test.js +135 -0
- package/dist/expressServer.test.d.ts +1 -0
- package/dist/expressServer.test.js +669 -0
- package/dist/notifiers/slackNotifier.d.ts +2 -1
- package/dist/notifiers/slackNotifier.js +20 -13
- package/dist/permissions.d.ts +1 -1
- package/dist/permissions.js +17 -25
- package/dist/permissions.test.js +57 -0
- package/dist/populate.test.js +52 -0
- package/dist/tests.d.ts +9 -27
- package/dist/utils.test.js +235 -7
- package/package.json +3 -2
- package/src/api.arrayOperations.test.ts +690 -0
- package/src/api.errors.test.ts +156 -0
- package/src/api.hooks.test.ts +704 -0
- package/src/api.query.test.ts +538 -0
- package/src/api.test.ts +510 -1301
- package/src/api.ts +19 -61
- package/src/auth.test.ts +72 -0
- package/src/expressServer.test.ts +579 -0
- package/src/notifiers/slackNotifier.ts +28 -17
- package/src/permissions.test.ts +70 -1
- package/src/permissions.ts +4 -14
- package/src/populate.test.ts +58 -0
- package/src/utils.test.ts +214 -9
|
@@ -0,0 +1,579 @@
|
|
|
1
|
+
import {afterEach, beforeEach, describe, expect, it} from "bun:test";
|
|
2
|
+
import express from "express";
|
|
3
|
+
import supertest from "supertest";
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
createRouter,
|
|
7
|
+
createRouterWithAuth,
|
|
8
|
+
cronjob,
|
|
9
|
+
logRequests,
|
|
10
|
+
setupEnvironment,
|
|
11
|
+
setupServer,
|
|
12
|
+
} from "./expressServer";
|
|
13
|
+
import {UserModel} from "./tests";
|
|
14
|
+
|
|
15
|
+
describe("expressServer", () => {
|
|
16
|
+
describe("setupEnvironment", () => {
|
|
17
|
+
const originalEnv = process.env;
|
|
18
|
+
|
|
19
|
+
beforeEach(() => {
|
|
20
|
+
// Reset env to a clean state with required values
|
|
21
|
+
process.env = {
|
|
22
|
+
...originalEnv,
|
|
23
|
+
REFRESH_TOKEN_SECRET: "test-refresh-secret",
|
|
24
|
+
SESSION_SECRET: "test-session-secret",
|
|
25
|
+
TOKEN_ISSUER: "test-issuer",
|
|
26
|
+
TOKEN_SECRET: "test-secret",
|
|
27
|
+
};
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
afterEach(() => {
|
|
31
|
+
process.env = originalEnv;
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it("throws error when TOKEN_ISSUER is not set", () => {
|
|
35
|
+
process.env.TOKEN_ISSUER = "";
|
|
36
|
+
expect(() => setupEnvironment()).toThrow("TOKEN_ISSUER must be set in env.");
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it("throws error when TOKEN_SECRET is not set", () => {
|
|
40
|
+
process.env.TOKEN_SECRET = "";
|
|
41
|
+
expect(() => setupEnvironment()).toThrow("TOKEN_SECRET must be set.");
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it("throws error when REFRESH_TOKEN_SECRET is not set", () => {
|
|
45
|
+
process.env.REFRESH_TOKEN_SECRET = "";
|
|
46
|
+
expect(() => setupEnvironment()).toThrow("REFRESH_TOKEN_SECRET must be set.");
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it("throws error when SESSION_SECRET is not set", () => {
|
|
50
|
+
process.env.SESSION_SECRET = "";
|
|
51
|
+
expect(() => setupEnvironment()).toThrow("SESSION_SECRET must be set.");
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it("does not throw when all required env vars are set", () => {
|
|
55
|
+
expect(() => setupEnvironment()).not.toThrow();
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
describe("logRequests", () => {
|
|
60
|
+
it("logs request with admin user type", () => {
|
|
61
|
+
const req = {
|
|
62
|
+
body: {},
|
|
63
|
+
method: "GET",
|
|
64
|
+
url: "/test",
|
|
65
|
+
user: {admin: true, id: "admin-123"},
|
|
66
|
+
};
|
|
67
|
+
const res = {
|
|
68
|
+
locals: {},
|
|
69
|
+
on: () => {},
|
|
70
|
+
};
|
|
71
|
+
let nextCalled = false;
|
|
72
|
+
const next = () => {
|
|
73
|
+
nextCalled = true;
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
logRequests(req, res, next);
|
|
77
|
+
expect(nextCalled).toBe(true);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it("logs request with test user type", () => {
|
|
81
|
+
const req = {
|
|
82
|
+
body: {},
|
|
83
|
+
method: "GET",
|
|
84
|
+
url: "/test",
|
|
85
|
+
user: {id: "test-123", testUser: true},
|
|
86
|
+
};
|
|
87
|
+
const res = {
|
|
88
|
+
locals: {},
|
|
89
|
+
on: () => {},
|
|
90
|
+
};
|
|
91
|
+
let nextCalled = false;
|
|
92
|
+
const next = () => {
|
|
93
|
+
nextCalled = true;
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
logRequests(req, res, next);
|
|
97
|
+
expect(nextCalled).toBe(true);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it("logs request with custom user type", () => {
|
|
101
|
+
const req = {
|
|
102
|
+
body: {},
|
|
103
|
+
method: "GET",
|
|
104
|
+
url: "/test",
|
|
105
|
+
user: {id: "user-123", type: "CustomType"},
|
|
106
|
+
};
|
|
107
|
+
const res = {
|
|
108
|
+
locals: {},
|
|
109
|
+
on: () => {},
|
|
110
|
+
};
|
|
111
|
+
let nextCalled = false;
|
|
112
|
+
const next = () => {
|
|
113
|
+
nextCalled = true;
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
logRequests(req, res, next);
|
|
117
|
+
expect(nextCalled).toBe(true);
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it("masks password in body", () => {
|
|
121
|
+
const req = {
|
|
122
|
+
body: {password: "secret123", username: "testuser"},
|
|
123
|
+
method: "POST",
|
|
124
|
+
url: "/login",
|
|
125
|
+
};
|
|
126
|
+
const res = {
|
|
127
|
+
locals: {},
|
|
128
|
+
on: () => {},
|
|
129
|
+
};
|
|
130
|
+
let nextCalled = false;
|
|
131
|
+
const next = () => {
|
|
132
|
+
nextCalled = true;
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
logRequests(req, res, next);
|
|
136
|
+
expect(nextCalled).toBe(true);
|
|
137
|
+
// Original body should not be modified
|
|
138
|
+
expect(req.body.password).toBe("secret123");
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it("triggers onFinished callback with route info", async () => {
|
|
142
|
+
const app = express();
|
|
143
|
+
app.use(logRequests);
|
|
144
|
+
app.get("/test", (req: any, res) => {
|
|
145
|
+
req.route = {path: "/test"};
|
|
146
|
+
req.routeMount = "/api";
|
|
147
|
+
res.json({ok: true});
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
await supertest(app).get("/test").expect(200);
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
it("triggers onFinished callback without route (for 404)", async () => {
|
|
154
|
+
const app = express();
|
|
155
|
+
app.use(logRequests);
|
|
156
|
+
// No routes defined, so it will 404
|
|
157
|
+
|
|
158
|
+
await supertest(app).get("/nonexistent").expect(404);
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
it("logs slow GET requests when enabled", async () => {
|
|
162
|
+
const app = express();
|
|
163
|
+
// Store logging options
|
|
164
|
+
app.use((_req, res, next) => {
|
|
165
|
+
res.locals.loggingOptions = {
|
|
166
|
+
logSlowRequests: true,
|
|
167
|
+
logSlowRequestsReadMs: 1, // Very low threshold to trigger slow request warning
|
|
168
|
+
};
|
|
169
|
+
next();
|
|
170
|
+
});
|
|
171
|
+
app.use(logRequests);
|
|
172
|
+
app.get("/slow", async (_req, res) => {
|
|
173
|
+
// Add small delay to exceed threshold
|
|
174
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
175
|
+
res.json({ok: true});
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
await supertest(app).get("/slow").expect(200);
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
it("logs slow write requests when enabled", async () => {
|
|
182
|
+
const app = express();
|
|
183
|
+
app.use(express.json());
|
|
184
|
+
app.use((_req, res, next) => {
|
|
185
|
+
res.locals.loggingOptions = {
|
|
186
|
+
logSlowRequests: true,
|
|
187
|
+
logSlowRequestsWriteMs: 1, // Very low threshold
|
|
188
|
+
};
|
|
189
|
+
next();
|
|
190
|
+
});
|
|
191
|
+
app.use(logRequests);
|
|
192
|
+
app.post("/slow", async (_req, res) => {
|
|
193
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
194
|
+
res.json({ok: true});
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
await supertest(app).post("/slow").send({data: "test"}).expect(200);
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
it("handles request with route path only (no routeMount)", async () => {
|
|
201
|
+
const app = express();
|
|
202
|
+
app.use(logRequests);
|
|
203
|
+
app.get("/test", (req: any, res) => {
|
|
204
|
+
req.route = {path: "/test"};
|
|
205
|
+
// No routeMount set
|
|
206
|
+
res.json({ok: true});
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
await supertest(app).get("/test").expect(200);
|
|
210
|
+
});
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
describe("createRouter", () => {
|
|
214
|
+
it("creates router with root path and adds routes", () => {
|
|
215
|
+
let routesCalled = false;
|
|
216
|
+
const addRoutes = (router: any) => {
|
|
217
|
+
routesCalled = true;
|
|
218
|
+
router.get("/test", (_req: any, res: any) => res.send("ok"));
|
|
219
|
+
};
|
|
220
|
+
|
|
221
|
+
const result = createRouter("/api", addRoutes);
|
|
222
|
+
|
|
223
|
+
expect(result[0]).toBe("/api");
|
|
224
|
+
expect(routesCalled).toBe(true);
|
|
225
|
+
expect(result.length).toBe(2); // [path, router]
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
it("creates router with middleware", () => {
|
|
229
|
+
const middleware1 = (_req: any, _res: any, next: any) => next();
|
|
230
|
+
const middleware2 = (_req: any, _res: any, next: any) => next();
|
|
231
|
+
const addRoutes = () => {};
|
|
232
|
+
|
|
233
|
+
const result = createRouter("/api", addRoutes, [middleware1, middleware2]);
|
|
234
|
+
|
|
235
|
+
expect(result[0]).toBe("/api");
|
|
236
|
+
expect(result.length).toBe(4); // [path, middleware1, middleware2, router]
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
it("routePathMiddleware sets routeMount on request", () => {
|
|
240
|
+
const addRoutes = (router: any) => {
|
|
241
|
+
router.get("/test", (req: any, res: any) => {
|
|
242
|
+
res.json({routeMount: req.routeMount});
|
|
243
|
+
});
|
|
244
|
+
};
|
|
245
|
+
|
|
246
|
+
const result = createRouter("/api", addRoutes);
|
|
247
|
+
const app = express();
|
|
248
|
+
app.use(...(result as [string, ...any[]]));
|
|
249
|
+
|
|
250
|
+
// The routePathMiddleware is internal, but we can verify the router works
|
|
251
|
+
expect(result[0]).toBe("/api");
|
|
252
|
+
});
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
describe("createRouterWithAuth", () => {
|
|
256
|
+
it("creates router with passport authentication middleware", () => {
|
|
257
|
+
let routesCalled = false;
|
|
258
|
+
const addRoutes = (router: any) => {
|
|
259
|
+
routesCalled = true;
|
|
260
|
+
router.get("/protected", (_req: any, res: any) => res.send("ok"));
|
|
261
|
+
};
|
|
262
|
+
|
|
263
|
+
const result = createRouterWithAuth("/secure", addRoutes);
|
|
264
|
+
|
|
265
|
+
expect(result[0]).toBe("/secure");
|
|
266
|
+
expect(routesCalled).toBe(true);
|
|
267
|
+
// Should have path + passport middleware + router = 3 elements minimum
|
|
268
|
+
expect(result.length).toBeGreaterThanOrEqual(2);
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
it("includes additional middleware", () => {
|
|
272
|
+
const customMiddleware = (_req: any, _res: any, next: any) => next();
|
|
273
|
+
const addRoutes = () => {};
|
|
274
|
+
|
|
275
|
+
const result = createRouterWithAuth("/secure", addRoutes, [customMiddleware]);
|
|
276
|
+
|
|
277
|
+
expect(result[0]).toBe("/secure");
|
|
278
|
+
// path + passport + customMiddleware + router
|
|
279
|
+
expect(result.length).toBeGreaterThanOrEqual(3);
|
|
280
|
+
});
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
describe("cronjob", () => {
|
|
284
|
+
it("accepts custom cron schedule for hourly", () => {
|
|
285
|
+
const callback = () => {};
|
|
286
|
+
|
|
287
|
+
// Every hour at minute 0
|
|
288
|
+
expect(() => cronjob("test-hourly", "0 * * * *", callback)).not.toThrow();
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
it("accepts custom cron schedule for minutely", () => {
|
|
292
|
+
const callback = () => {};
|
|
293
|
+
|
|
294
|
+
// Every minute
|
|
295
|
+
expect(() => cronjob("test-minutely", "* * * * *", callback)).not.toThrow();
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
it("accepts custom cron schedule", () => {
|
|
299
|
+
const callback = () => {};
|
|
300
|
+
|
|
301
|
+
// Every day at midnight
|
|
302
|
+
expect(() => cronjob("test-custom", "0 0 * * *", callback)).not.toThrow();
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
it("throws error for invalid cron schedule", () => {
|
|
306
|
+
const callback = () => {};
|
|
307
|
+
|
|
308
|
+
expect(() => cronjob("test-invalid", "invalid-cron", callback)).toThrow(
|
|
309
|
+
"Failed to create cronjob"
|
|
310
|
+
);
|
|
311
|
+
});
|
|
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", () => {
|
|
317
|
+
const callback = () => {};
|
|
318
|
+
expect(() => cronjob("test-hourly-alias", "hourly", callback)).toThrow(
|
|
319
|
+
"Failed to create cronjob"
|
|
320
|
+
);
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
it("minutely alias fails due to bug in implementation", () => {
|
|
324
|
+
const callback = () => {};
|
|
325
|
+
expect(() => cronjob("test-minutely-alias", "minutely", callback)).toThrow(
|
|
326
|
+
"Failed to create cronjob"
|
|
327
|
+
);
|
|
328
|
+
});
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
describe("createRouter routePathMiddleware", () => {
|
|
332
|
+
it("initializes routeMount array when not present", async () => {
|
|
333
|
+
const addRoutes = (router: any) => {
|
|
334
|
+
router.get("/test", (req: any, res: any) => {
|
|
335
|
+
res.json({routeMount: req.routeMount});
|
|
336
|
+
});
|
|
337
|
+
};
|
|
338
|
+
|
|
339
|
+
const result = createRouter("/api", addRoutes);
|
|
340
|
+
const app = express();
|
|
341
|
+
app.use(...(result as [string, ...any[]]));
|
|
342
|
+
|
|
343
|
+
const response = await supertest(app).get("/api/test").expect(200);
|
|
344
|
+
expect(response.body.routeMount).toEqual(["/api"]);
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
it("appends to existing routeMount array", async () => {
|
|
348
|
+
const addRoutes = (router: any) => {
|
|
349
|
+
router.get("/test", (req: any, res: any) => {
|
|
350
|
+
res.json({routeMount: req.routeMount});
|
|
351
|
+
});
|
|
352
|
+
};
|
|
353
|
+
|
|
354
|
+
// Create nested routers
|
|
355
|
+
const innerResult = createRouter("/inner", addRoutes);
|
|
356
|
+
const outerAddRoutes = (router: any) => {
|
|
357
|
+
router.use(...(innerResult as [string, ...any[]]));
|
|
358
|
+
};
|
|
359
|
+
const outerResult = createRouter("/outer", outerAddRoutes);
|
|
360
|
+
|
|
361
|
+
const app = express();
|
|
362
|
+
app.use(...(outerResult as [string, ...any[]]));
|
|
363
|
+
|
|
364
|
+
const response = await supertest(app).get("/outer/inner/test").expect(200);
|
|
365
|
+
expect(response.body.routeMount).toEqual(["/outer", "/inner"]);
|
|
366
|
+
});
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
describe("setupServer", () => {
|
|
370
|
+
const originalEnv = process.env;
|
|
371
|
+
|
|
372
|
+
beforeEach(() => {
|
|
373
|
+
process.env = {
|
|
374
|
+
...originalEnv,
|
|
375
|
+
REFRESH_TOKEN_SECRET: "test-refresh-secret",
|
|
376
|
+
SESSION_SECRET: "test-session-secret",
|
|
377
|
+
TOKEN_EXPIRES_IN: "1h",
|
|
378
|
+
TOKEN_ISSUER: "test-issuer",
|
|
379
|
+
TOKEN_SECRET: "test-secret",
|
|
380
|
+
};
|
|
381
|
+
});
|
|
382
|
+
|
|
383
|
+
afterEach(() => {
|
|
384
|
+
process.env = originalEnv;
|
|
385
|
+
});
|
|
386
|
+
|
|
387
|
+
it("creates server with skipListen option", () => {
|
|
388
|
+
const addRoutes = () => {};
|
|
389
|
+
|
|
390
|
+
const app = setupServer({
|
|
391
|
+
addRoutes,
|
|
392
|
+
skipListen: true,
|
|
393
|
+
userModel: UserModel as any,
|
|
394
|
+
});
|
|
395
|
+
|
|
396
|
+
expect(app).toBeDefined();
|
|
397
|
+
});
|
|
398
|
+
|
|
399
|
+
it("creates server with addMiddleware option", () => {
|
|
400
|
+
let middlewareCalled = false;
|
|
401
|
+
const addMiddleware = (app: any) => {
|
|
402
|
+
middlewareCalled = true;
|
|
403
|
+
app.use((_req: any, _res: any, next: any) => next());
|
|
404
|
+
};
|
|
405
|
+
const addRoutes = () => {};
|
|
406
|
+
|
|
407
|
+
const app = setupServer({
|
|
408
|
+
addMiddleware,
|
|
409
|
+
addRoutes,
|
|
410
|
+
skipListen: true,
|
|
411
|
+
userModel: UserModel as any,
|
|
412
|
+
});
|
|
413
|
+
|
|
414
|
+
expect(app).toBeDefined();
|
|
415
|
+
expect(middlewareCalled).toBe(true);
|
|
416
|
+
});
|
|
417
|
+
|
|
418
|
+
it("creates server with custom corsOrigin", () => {
|
|
419
|
+
const addRoutes = () => {};
|
|
420
|
+
|
|
421
|
+
const app = setupServer({
|
|
422
|
+
addRoutes,
|
|
423
|
+
corsOrigin: "https://example.com",
|
|
424
|
+
skipListen: true,
|
|
425
|
+
userModel: UserModel as any,
|
|
426
|
+
});
|
|
427
|
+
|
|
428
|
+
expect(app).toBeDefined();
|
|
429
|
+
});
|
|
430
|
+
|
|
431
|
+
it("creates server with authOptions", () => {
|
|
432
|
+
const addRoutes = () => {};
|
|
433
|
+
|
|
434
|
+
const app = setupServer({
|
|
435
|
+
addRoutes,
|
|
436
|
+
authOptions: {
|
|
437
|
+
generateJWTPayload: (user) => ({customField: "test", id: user._id}),
|
|
438
|
+
generateTokenExpiration: () => "2h",
|
|
439
|
+
},
|
|
440
|
+
skipListen: true,
|
|
441
|
+
userModel: UserModel as any,
|
|
442
|
+
});
|
|
443
|
+
|
|
444
|
+
expect(app).toBeDefined();
|
|
445
|
+
});
|
|
446
|
+
});
|
|
447
|
+
|
|
448
|
+
describe("logRequests edge cases", () => {
|
|
449
|
+
it("warns for request without route but with success status code", async () => {
|
|
450
|
+
const app = express();
|
|
451
|
+
app.use(logRequests);
|
|
452
|
+
// Middleware that sets statusCode < 400 but doesn't define route
|
|
453
|
+
app.use((_req: any, res) => {
|
|
454
|
+
res.status(200).json({ok: true});
|
|
455
|
+
});
|
|
456
|
+
|
|
457
|
+
await supertest(app).get("/no-route").expect(200);
|
|
458
|
+
});
|
|
459
|
+
|
|
460
|
+
it("handles request with routeMount as string (legacy)", async () => {
|
|
461
|
+
const app = express();
|
|
462
|
+
app.use(logRequests);
|
|
463
|
+
app.get("/test", (req: any, res) => {
|
|
464
|
+
req.route = {path: "/test"};
|
|
465
|
+
req.routeMount = "/api"; // String instead of array
|
|
466
|
+
res.json({ok: true});
|
|
467
|
+
});
|
|
468
|
+
|
|
469
|
+
await supertest(app).get("/test").expect(200);
|
|
470
|
+
});
|
|
471
|
+
});
|
|
472
|
+
|
|
473
|
+
describe("setupServer with full integration", () => {
|
|
474
|
+
const originalEnv = process.env;
|
|
475
|
+
|
|
476
|
+
beforeEach(() => {
|
|
477
|
+
process.env = {
|
|
478
|
+
...originalEnv,
|
|
479
|
+
REFRESH_TOKEN_SECRET: "test-refresh-secret",
|
|
480
|
+
SESSION_SECRET: "test-session-secret",
|
|
481
|
+
TOKEN_EXPIRES_IN: "1h",
|
|
482
|
+
TOKEN_ISSUER: "test-issuer",
|
|
483
|
+
TOKEN_SECRET: "test-secret",
|
|
484
|
+
};
|
|
485
|
+
});
|
|
486
|
+
|
|
487
|
+
afterEach(() => {
|
|
488
|
+
process.env = originalEnv;
|
|
489
|
+
});
|
|
490
|
+
|
|
491
|
+
it("sets Sentry transaction ID tag from header", async () => {
|
|
492
|
+
const addRoutes = (app: any) => {
|
|
493
|
+
app.get("/test", (_req: any, res: any) => {
|
|
494
|
+
res.json({ok: true});
|
|
495
|
+
});
|
|
496
|
+
};
|
|
497
|
+
|
|
498
|
+
const app = setupServer({
|
|
499
|
+
addRoutes,
|
|
500
|
+
skipListen: true,
|
|
501
|
+
userModel: UserModel as any,
|
|
502
|
+
});
|
|
503
|
+
|
|
504
|
+
await supertest(app).get("/test").set("X-Transaction-ID", "txn-123").expect(200);
|
|
505
|
+
});
|
|
506
|
+
|
|
507
|
+
it("sets Sentry session ID tag from header", async () => {
|
|
508
|
+
const addRoutes = (app: any) => {
|
|
509
|
+
app.get("/test", (_req: any, res: any) => {
|
|
510
|
+
res.json({ok: true});
|
|
511
|
+
});
|
|
512
|
+
};
|
|
513
|
+
|
|
514
|
+
const app = setupServer({
|
|
515
|
+
addRoutes,
|
|
516
|
+
skipListen: true,
|
|
517
|
+
userModel: UserModel as any,
|
|
518
|
+
});
|
|
519
|
+
|
|
520
|
+
await supertest(app).get("/test").set("X-Session-ID", "session-456").expect(200);
|
|
521
|
+
});
|
|
522
|
+
|
|
523
|
+
it("sets both transaction and session ID tags", async () => {
|
|
524
|
+
const addRoutes = (app: any) => {
|
|
525
|
+
app.get("/test", (_req: any, res: any) => {
|
|
526
|
+
res.json({ok: true});
|
|
527
|
+
});
|
|
528
|
+
};
|
|
529
|
+
|
|
530
|
+
const app = setupServer({
|
|
531
|
+
addRoutes,
|
|
532
|
+
skipListen: true,
|
|
533
|
+
userModel: UserModel as any,
|
|
534
|
+
});
|
|
535
|
+
|
|
536
|
+
await supertest(app)
|
|
537
|
+
.get("/test")
|
|
538
|
+
.set("X-Transaction-ID", "txn-123")
|
|
539
|
+
.set("X-Session-ID", "session-456")
|
|
540
|
+
.expect(200);
|
|
541
|
+
});
|
|
542
|
+
|
|
543
|
+
it("handles fallthrough error handler", async () => {
|
|
544
|
+
const addRoutes = (app: any) => {
|
|
545
|
+
app.get("/error", (_req: any, _res: any) => {
|
|
546
|
+
throw new Error("Unexpected error");
|
|
547
|
+
});
|
|
548
|
+
};
|
|
549
|
+
|
|
550
|
+
const app = setupServer({
|
|
551
|
+
addRoutes,
|
|
552
|
+
skipListen: true,
|
|
553
|
+
userModel: UserModel as any,
|
|
554
|
+
});
|
|
555
|
+
|
|
556
|
+
await supertest(app).get("/error").expect(500);
|
|
557
|
+
});
|
|
558
|
+
|
|
559
|
+
it("handles loggingOptions passed to setupServer", async () => {
|
|
560
|
+
const addRoutes = (app: any) => {
|
|
561
|
+
app.get("/test", (_req: any, res: any) => {
|
|
562
|
+
res.json({ok: true});
|
|
563
|
+
});
|
|
564
|
+
};
|
|
565
|
+
|
|
566
|
+
const app = setupServer({
|
|
567
|
+
addRoutes,
|
|
568
|
+
loggingOptions: {
|
|
569
|
+
logSlowRequests: true,
|
|
570
|
+
logSlowRequestsReadMs: 100,
|
|
571
|
+
},
|
|
572
|
+
skipListen: true,
|
|
573
|
+
userModel: UserModel as any,
|
|
574
|
+
});
|
|
575
|
+
|
|
576
|
+
await supertest(app).get("/test").expect(200);
|
|
577
|
+
});
|
|
578
|
+
});
|
|
579
|
+
});
|
|
@@ -4,33 +4,44 @@ import axios from "axios";
|
|
|
4
4
|
import {APIError} from "../errors";
|
|
5
5
|
import {logger} from "../logger";
|
|
6
6
|
// Convenience method to send data to a Slack webhook.
|
|
7
|
+
// If `url` is provided, it will be used directly instead of looking up from environment.
|
|
8
|
+
// DEPRECATED: Looking up webhook URLs from the SLACK_WEBHOOKS environment variable by channel name
|
|
9
|
+
// is deprecated and will be removed in a future version. Please pass the `url` parameter directly.
|
|
7
10
|
export async function sendToSlack(
|
|
8
11
|
text: string,
|
|
9
12
|
{
|
|
10
13
|
slackChannel,
|
|
11
14
|
shouldThrow = false,
|
|
12
15
|
env,
|
|
13
|
-
|
|
16
|
+
url,
|
|
17
|
+
}: {slackChannel?: string; shouldThrow?: boolean; env?: string; url?: string} = {}
|
|
14
18
|
) {
|
|
15
|
-
|
|
16
|
-
// as an object, so we can look them up by channel name.
|
|
17
|
-
const slackWebhooksString = process.env.SLACK_WEBHOOKS;
|
|
18
|
-
if (!slackWebhooksString) {
|
|
19
|
-
logger.debug("You must set SLACK_WEBHOOKS in the environment to use sendToSlack.");
|
|
20
|
-
return;
|
|
21
|
-
}
|
|
22
|
-
const slackWebhooks = JSON.parse(slackWebhooksString ?? "{}");
|
|
23
|
-
|
|
24
|
-
const channel = slackChannel ?? "default";
|
|
25
|
-
|
|
26
|
-
const slackWebhookUrl = slackWebhooks[channel] ?? slackWebhooks.default;
|
|
19
|
+
let slackWebhookUrl = url;
|
|
27
20
|
|
|
28
21
|
if (!slackWebhookUrl) {
|
|
29
|
-
|
|
30
|
-
|
|
22
|
+
logger.debug(
|
|
23
|
+
"DEPRECATED: Looking up webhook URLs from SLACK_WEBHOOKS environment variable is deprecated and will be removed in a future version. Please pass the url parameter directly."
|
|
31
24
|
);
|
|
32
|
-
|
|
33
|
-
|
|
25
|
+
// since Slack now requires a webhook for each channel, we need to store them in the environment
|
|
26
|
+
// as an object, so we can look them up by channel name.
|
|
27
|
+
const slackWebhooksString = process.env.SLACK_WEBHOOKS;
|
|
28
|
+
if (!slackWebhooksString) {
|
|
29
|
+
logger.debug("You must set SLACK_WEBHOOKS in the environment to use sendToSlack.");
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
const slackWebhooks = JSON.parse(slackWebhooksString ?? "{}");
|
|
33
|
+
|
|
34
|
+
const channel = slackChannel ?? "default";
|
|
35
|
+
|
|
36
|
+
slackWebhookUrl = slackWebhooks[channel] ?? slackWebhooks.default;
|
|
37
|
+
|
|
38
|
+
if (!slackWebhookUrl) {
|
|
39
|
+
Sentry.captureException(
|
|
40
|
+
new Error(`No webhook url set in env for ${channel}. Slack message not sent`)
|
|
41
|
+
);
|
|
42
|
+
logger.debug(`No webhook url set in env for ${channel}.`);
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
34
45
|
}
|
|
35
46
|
|
|
36
47
|
let formattedText = text;
|