@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/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
+ });