@terreno/api 0.0.11-beta.1 → 0.0.12
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/biome.jsonc +1 -1
- package/dist/api.arrayOperations.test.d.ts +1 -0
- package/dist/api.arrayOperations.test.js +868 -0
- 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.query.test.d.ts +1 -0
- package/dist/api.query.test.js +805 -0
- package/dist/api.test.js +310 -3182
- 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.test.js +57 -0
- package/dist/populate.test.js +52 -0
- package/dist/tests.d.ts +9 -27
- package/dist/utils.test.js +66 -0
- package/package.json +2 -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 +273 -2658
- 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/populate.test.ts +58 -0
- package/src/utils.test.ts +26 -1
package/src/auth.test.ts
CHANGED
|
@@ -548,3 +548,75 @@ describe("generateTokens", () => {
|
|
|
548
548
|
expect(refreshToken).toBeDefined();
|
|
549
549
|
});
|
|
550
550
|
});
|
|
551
|
+
|
|
552
|
+
describe("generateTokens edge cases", () => {
|
|
553
|
+
const OLD_ENV = process.env;
|
|
554
|
+
|
|
555
|
+
beforeEach(() => {
|
|
556
|
+
process.env = {...OLD_ENV};
|
|
557
|
+
process.env.TOKEN_SECRET = "secret";
|
|
558
|
+
process.env.REFRESH_TOKEN_SECRET = "refresh_secret";
|
|
559
|
+
});
|
|
560
|
+
|
|
561
|
+
afterEach(() => {
|
|
562
|
+
process.env = OLD_ENV;
|
|
563
|
+
});
|
|
564
|
+
|
|
565
|
+
it("returns null tokens when user is missing", async () => {
|
|
566
|
+
const result = await generateTokens(null);
|
|
567
|
+
expect(result.token).toBeNull();
|
|
568
|
+
expect(result.refreshToken).toBeNull();
|
|
569
|
+
});
|
|
570
|
+
|
|
571
|
+
it("returns null tokens when user has no _id", async () => {
|
|
572
|
+
const result = await generateTokens({email: "test@test.com"});
|
|
573
|
+
expect(result.token).toBeNull();
|
|
574
|
+
expect(result.refreshToken).toBeNull();
|
|
575
|
+
});
|
|
576
|
+
|
|
577
|
+
it("includes custom payload from generateJWTPayload option", async () => {
|
|
578
|
+
const jwtLib = await import("jsonwebtoken");
|
|
579
|
+
|
|
580
|
+
const user = {_id: "user-123"};
|
|
581
|
+
const result = await generateTokens(user, {
|
|
582
|
+
generateJWTPayload: (u) => ({customField: "customValue", userId: u._id}),
|
|
583
|
+
});
|
|
584
|
+
|
|
585
|
+
expect(result.token).toBeDefined();
|
|
586
|
+
const decoded = jwtLib.decode(result.token as string) as any;
|
|
587
|
+
expect(decoded.customField).toBe("customValue");
|
|
588
|
+
expect(decoded.id).toBe("user-123");
|
|
589
|
+
});
|
|
590
|
+
|
|
591
|
+
it("uses custom token expiration from generateTokenExpiration option", async () => {
|
|
592
|
+
const jwtLib = await import("jsonwebtoken");
|
|
593
|
+
|
|
594
|
+
const user = {_id: "user-123"};
|
|
595
|
+
const result = await generateTokens(user, {
|
|
596
|
+
generateTokenExpiration: () => "1h",
|
|
597
|
+
});
|
|
598
|
+
|
|
599
|
+
expect(result.token).toBeDefined();
|
|
600
|
+
const decoded = jwtLib.decode(result.token as string) as any;
|
|
601
|
+
// Check that exp is roughly 1 hour from now (within 5 seconds tolerance)
|
|
602
|
+
const expectedExp = Math.floor(Date.now() / 1000) + 3600;
|
|
603
|
+
expect(decoded.exp).toBeGreaterThan(expectedExp - 5);
|
|
604
|
+
expect(decoded.exp).toBeLessThan(expectedExp + 5);
|
|
605
|
+
});
|
|
606
|
+
|
|
607
|
+
it("uses custom refresh token expiration from generateRefreshTokenExpiration option", async () => {
|
|
608
|
+
const jwtLib = await import("jsonwebtoken");
|
|
609
|
+
|
|
610
|
+
const user = {_id: "user-123"};
|
|
611
|
+
const result = await generateTokens(user, {
|
|
612
|
+
generateRefreshTokenExpiration: () => "7d",
|
|
613
|
+
});
|
|
614
|
+
|
|
615
|
+
expect(result.refreshToken).toBeDefined();
|
|
616
|
+
const decoded = jwtLib.decode(result.refreshToken as string) as any;
|
|
617
|
+
// Check that exp is roughly 7 days from now
|
|
618
|
+
const expectedExp = Math.floor(Date.now() / 1000) + 7 * 24 * 3600;
|
|
619
|
+
expect(decoded.exp).toBeGreaterThan(expectedExp - 10);
|
|
620
|
+
expect(decoded.exp).toBeLessThan(expectedExp + 10);
|
|
621
|
+
});
|
|
622
|
+
});
|
|
@@ -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
|
+
});
|