@terreno/api 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (119) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +170 -0
  3. package/biome.jsonc +22 -0
  4. package/bunfig.toml +4 -0
  5. package/dist/api.d.ts +227 -0
  6. package/dist/api.js +1024 -0
  7. package/dist/api.test.d.ts +1 -0
  8. package/dist/api.test.js +2143 -0
  9. package/dist/auth.d.ts +50 -0
  10. package/dist/auth.js +512 -0
  11. package/dist/auth.test.d.ts +1 -0
  12. package/dist/auth.test.js +778 -0
  13. package/dist/errors.d.ts +75 -0
  14. package/dist/errors.js +216 -0
  15. package/dist/example.d.ts +1 -0
  16. package/dist/example.js +118 -0
  17. package/dist/expressServer.d.ts +35 -0
  18. package/dist/expressServer.js +436 -0
  19. package/dist/index.d.ts +14 -0
  20. package/dist/index.js +30 -0
  21. package/dist/logger.d.ts +23 -0
  22. package/dist/logger.js +249 -0
  23. package/dist/middleware.d.ts +10 -0
  24. package/dist/middleware.js +52 -0
  25. package/dist/notifiers/googleChatNotifier.d.ts +5 -0
  26. package/dist/notifiers/googleChatNotifier.js +130 -0
  27. package/dist/notifiers/googleChatNotifier.test.d.ts +1 -0
  28. package/dist/notifiers/googleChatNotifier.test.js +260 -0
  29. package/dist/notifiers/index.d.ts +3 -0
  30. package/dist/notifiers/index.js +19 -0
  31. package/dist/notifiers/slackNotifier.d.ts +5 -0
  32. package/dist/notifiers/slackNotifier.js +130 -0
  33. package/dist/notifiers/slackNotifier.test.d.ts +1 -0
  34. package/dist/notifiers/slackNotifier.test.js +259 -0
  35. package/dist/notifiers/zoomNotifier.d.ts +34 -0
  36. package/dist/notifiers/zoomNotifier.js +181 -0
  37. package/dist/notifiers/zoomNotifier.test.d.ts +1 -0
  38. package/dist/notifiers/zoomNotifier.test.js +370 -0
  39. package/dist/openApi.d.ts +60 -0
  40. package/dist/openApi.js +441 -0
  41. package/dist/openApi.test.d.ts +1 -0
  42. package/dist/openApi.test.js +445 -0
  43. package/dist/openApiBuilder.d.ts +419 -0
  44. package/dist/openApiBuilder.js +424 -0
  45. package/dist/openApiBuilder.test.d.ts +1 -0
  46. package/dist/openApiBuilder.test.js +509 -0
  47. package/dist/openApiEtag.d.ts +7 -0
  48. package/dist/openApiEtag.js +38 -0
  49. package/dist/permissions.d.ts +26 -0
  50. package/dist/permissions.js +331 -0
  51. package/dist/permissions.test.d.ts +1 -0
  52. package/dist/permissions.test.js +413 -0
  53. package/dist/plugins.d.ts +67 -0
  54. package/dist/plugins.js +315 -0
  55. package/dist/plugins.test.d.ts +1 -0
  56. package/dist/plugins.test.js +639 -0
  57. package/dist/populate.d.ts +14 -0
  58. package/dist/populate.js +315 -0
  59. package/dist/populate.test.d.ts +1 -0
  60. package/dist/populate.test.js +133 -0
  61. package/dist/response.d.ts +0 -0
  62. package/dist/response.js +1 -0
  63. package/dist/tests/bunSetup.d.ts +1 -0
  64. package/dist/tests/bunSetup.js +297 -0
  65. package/dist/tests/index.d.ts +1 -0
  66. package/dist/tests/index.js +17 -0
  67. package/dist/tests.d.ts +99 -0
  68. package/dist/tests.js +273 -0
  69. package/dist/transformers.d.ts +25 -0
  70. package/dist/transformers.js +217 -0
  71. package/dist/transformers.test.d.ts +1 -0
  72. package/dist/transformers.test.js +370 -0
  73. package/dist/utils.d.ts +11 -0
  74. package/dist/utils.js +143 -0
  75. package/dist/utils.test.d.ts +1 -0
  76. package/dist/utils.test.js +14 -0
  77. package/index.ts +1 -0
  78. package/package.json +88 -0
  79. package/src/__snapshots__/openApi.test.ts.snap +4814 -0
  80. package/src/__snapshots__/openApiBuilder.test.ts.snap +1485 -0
  81. package/src/api.test.ts +1661 -0
  82. package/src/api.ts +1036 -0
  83. package/src/auth.test.ts +550 -0
  84. package/src/auth.ts +408 -0
  85. package/src/errors.ts +225 -0
  86. package/src/example.ts +99 -0
  87. package/src/express.d.ts +5 -0
  88. package/src/expressServer.ts +387 -0
  89. package/src/index.ts +14 -0
  90. package/src/logger.ts +190 -0
  91. package/src/middleware.ts +18 -0
  92. package/src/notifiers/googleChatNotifier.test.ts +114 -0
  93. package/src/notifiers/googleChatNotifier.ts +47 -0
  94. package/src/notifiers/index.ts +3 -0
  95. package/src/notifiers/slackNotifier.test.ts +113 -0
  96. package/src/notifiers/slackNotifier.ts +55 -0
  97. package/src/notifiers/zoomNotifier.test.ts +207 -0
  98. package/src/notifiers/zoomNotifier.ts +111 -0
  99. package/src/openApi.test.ts +331 -0
  100. package/src/openApi.ts +494 -0
  101. package/src/openApiBuilder.test.ts +442 -0
  102. package/src/openApiBuilder.ts +636 -0
  103. package/src/openApiEtag.ts +40 -0
  104. package/src/permissions.test.ts +219 -0
  105. package/src/permissions.ts +228 -0
  106. package/src/plugins.test.ts +390 -0
  107. package/src/plugins.ts +289 -0
  108. package/src/populate.test.ts +65 -0
  109. package/src/populate.ts +258 -0
  110. package/src/response.ts +0 -0
  111. package/src/tests/bunSetup.ts +234 -0
  112. package/src/tests/index.ts +1 -0
  113. package/src/tests.ts +218 -0
  114. package/src/transformers.test.ts +202 -0
  115. package/src/transformers.ts +170 -0
  116. package/src/utils.test.ts +14 -0
  117. package/src/utils.ts +47 -0
  118. package/tsconfig.json +60 -0
  119. package/types.d.ts +17 -0
@@ -0,0 +1,550 @@
1
+ import {afterEach, beforeEach, describe, expect, it, setSystemTime} from "bun:test";
2
+ import type express from "express";
3
+ import type jwt from "jsonwebtoken";
4
+ import supertest from "supertest";
5
+ import type TestAgent from "supertest/lib/agent";
6
+
7
+ import {modelRouter} from "./api";
8
+ import {addAuthRoutes, addMeRoutes, generateTokens, setupAuth} from "./auth";
9
+ import {setupServer} from "./expressServer";
10
+ import {Permissions} from "./permissions";
11
+ import {type Food, FoodModel, getBaseServer, setupDb, UserModel} from "./tests";
12
+ import {AdminOwnerTransformer} from "./transformers";
13
+ import {timeout} from "./utils";
14
+
15
+ describe("auth tests", () => {
16
+ let app: express.Application;
17
+ let admin: any;
18
+ let notAdmin: any;
19
+ let agent: TestAgent;
20
+
21
+ beforeEach(async () => {
22
+ // Reset to real time - don't freeze time here as passport-local-mongoose
23
+ // lockout mechanism needs real time to progress
24
+ setSystemTime();
25
+ [admin, notAdmin] = await setupDb();
26
+
27
+ await Promise.all([
28
+ FoodModel.create({
29
+ calories: 1,
30
+ created: new Date(),
31
+ name: "Spinach",
32
+ ownerId: notAdmin._id,
33
+ }),
34
+ FoodModel.create({
35
+ calories: 100,
36
+ created: Date.now() - 10,
37
+ hidden: true,
38
+ name: "Apple",
39
+ ownerId: admin._id,
40
+ }),
41
+ FoodModel.create({
42
+ calories: 100,
43
+ created: Date.now() - 10,
44
+ name: "Carrots",
45
+ ownerId: admin._id,
46
+ }),
47
+ ]);
48
+
49
+ function addRoutes(router: express.Router): void {
50
+ router.use(
51
+ "/food",
52
+ modelRouter(FoodModel, {
53
+ allowAnonymous: true,
54
+ permissions: {
55
+ create: [Permissions.IsAuthenticated],
56
+ delete: [Permissions.IsAuthenticated],
57
+ list: [Permissions.IsAny],
58
+ read: [Permissions.IsAny],
59
+ update: [Permissions.IsAuthenticated],
60
+ },
61
+ queryFilter: (user?: {admin: boolean}) => {
62
+ if (!user?.admin) {
63
+ return {hidden: {$ne: true}};
64
+ }
65
+ return {};
66
+ },
67
+ transformer: AdminOwnerTransformer<Food>({
68
+ adminReadFields: ["name", "calories", "created", "ownerId"],
69
+ adminWriteFields: ["name", "calories", "created", "ownerId"],
70
+ anonReadFields: ["name"],
71
+ anonWriteFields: [],
72
+ authReadFields: ["name", "calories", "created"],
73
+ authWriteFields: ["name", "calories"],
74
+ ownerReadFields: ["name", "calories", "created", "ownerId"],
75
+ ownerWriteFields: ["name", "calories", "created"],
76
+ }),
77
+ })
78
+ );
79
+ }
80
+ app = setupServer({
81
+ addRoutes,
82
+ skipListen: true,
83
+ userModel: UserModel as any,
84
+ });
85
+ agent = supertest.agent(app);
86
+ });
87
+
88
+ afterEach(async () => {
89
+ setSystemTime();
90
+ });
91
+
92
+ it("completes token signup e2e", async () => {
93
+ let res = await agent
94
+ .post("/auth/signup")
95
+ .send({email: "new@example.com", password: "123"})
96
+ .expect(200);
97
+ let {userId, token, refreshToken} = res.body.data;
98
+ expect(userId).toBeDefined();
99
+ expect(token).toBeDefined();
100
+ expect(refreshToken).toBeDefined();
101
+
102
+ res = await agent
103
+ .post("/auth/login")
104
+ .send({email: "new@example.com", password: "123"})
105
+ .expect(200);
106
+ await agent.set("authorization", `Bearer ${res.body.data.token}`);
107
+
108
+ userId = res.body.data.userId;
109
+ token = res.body.data.token;
110
+ expect(userId).toBeDefined();
111
+ expect(token).toBeDefined();
112
+ expect(refreshToken).toBeDefined();
113
+
114
+ const food = await FoodModel.create({
115
+ calories: 1,
116
+ created: new Date(),
117
+ name: "Peas",
118
+ ownerId: userId,
119
+ });
120
+
121
+ const meRes = await agent.get("/auth/me").expect(200);
122
+ expect(meRes.body.data._id).toBeDefined();
123
+ expect(meRes.body.data.id).toBeDefined();
124
+ expect(meRes.body.data.hash).toBeUndefined();
125
+ expect(meRes.body.data.email).toBe("new@example.com");
126
+ expect(meRes.body.data.updated).toBeDefined();
127
+ expect(meRes.body.data.created).toBeDefined();
128
+ expect(meRes.body.data.admin).toBe(false);
129
+
130
+ const mePatchRes = await agent
131
+ .patch("/auth/me")
132
+ .send({email: "new2@example.com"})
133
+ .set("authorization", `Bearer ${token}`)
134
+ .expect(200);
135
+ expect(mePatchRes.body.data._id).toBeDefined();
136
+ expect(mePatchRes.body.data.id).toBeDefined();
137
+ expect(mePatchRes.body.data.hash).toBeUndefined();
138
+ expect(mePatchRes.body.data.email).toBe("new2@example.com");
139
+ expect(mePatchRes.body.data.updated).toBeDefined();
140
+ expect(mePatchRes.body.data.created).toBeDefined();
141
+ expect(mePatchRes.body.data.admin).toBe(false);
142
+
143
+ // Use token to see 2 foods + the one we just created
144
+ const getRes = await agent.get("/food").expect(200);
145
+
146
+ expect(getRes.body.data).toHaveLength(3);
147
+ expect(getRes.body.data.find((f: any) => f.name === "Peas")).toBeDefined();
148
+
149
+ const updateRes = await agent
150
+ .patch(`/food/${food._id}`)
151
+ .send({name: "PeasAndCarrots"})
152
+ .expect(200);
153
+ expect(updateRes.body.data.name).toBe("PeasAndCarrots");
154
+ });
155
+
156
+ it("signup with extra data", async () => {
157
+ const res = await agent
158
+ .post("/auth/signup")
159
+ .send({age: 25, email: "new@example.com", password: "123"})
160
+ .expect(200);
161
+ const {userId, token, refreshToken} = res.body.data;
162
+ expect(userId).toBeDefined();
163
+ expect(token).toBeDefined();
164
+ expect(refreshToken).toBeDefined();
165
+
166
+ const user = await UserModel.findOne({email: "new@example.com"});
167
+ expect(user?.age).toBe(25);
168
+ });
169
+
170
+ it("login failure", async () => {
171
+ let res = await agent
172
+ .post("/auth/login")
173
+ .send({email: "admin@example.com", password: "wrong"})
174
+ .expect(401);
175
+ expect(res.body).toEqual({
176
+ message: "Password or username is incorrect",
177
+ });
178
+ res = await agent
179
+ .post("/auth/login")
180
+ .send({email: "nope@example.com", password: "wrong"})
181
+ .expect(401);
182
+ // we don't really want to expose if a given email address has an account in our system or not
183
+ expect(res.body).toEqual({
184
+ message: "Password or username is incorrect",
185
+ });
186
+ });
187
+
188
+ it("case insensitive email", async () => {
189
+ const res = await agent
190
+ .post("/auth/login")
191
+ .send({email: "ADMIN@example.com", password: "securePassword"})
192
+ .expect(200);
193
+ expect(res.body.data.token).toBeDefined();
194
+ });
195
+
196
+ it("case insensitive email with emails with symbols", async () => {
197
+ const res = await agent
198
+ .post("/auth/login")
199
+ .send({email: "ADMIN+other@example.com", password: "otherPassword"})
200
+ .expect(200);
201
+ expect(res.body.data.token).toBeDefined();
202
+ });
203
+
204
+ it("completes token login e2e", async () => {
205
+ const res = await agent
206
+ .post("/auth/login")
207
+ .send({email: "admin@example.com", password: "securePassword"})
208
+ .expect(200);
209
+ const {userId, token} = res.body.data;
210
+ expect(userId).toBeDefined();
211
+ expect(token).toBeDefined();
212
+
213
+ await agent.set("authorization", `Bearer ${res.body.data.token}`);
214
+
215
+ const meRes = await agent.get("/auth/me").expect(200);
216
+ expect(meRes.body.data._id).toBeDefined();
217
+ expect(meRes.body.data.id).toBeDefined();
218
+ expect(meRes.body.data.hash).toBeUndefined();
219
+ expect(meRes.body.data.email).toBe("admin@example.com");
220
+ expect(meRes.body.data.updated).toBeDefined();
221
+ expect(meRes.body.data.created).toBeDefined();
222
+ expect(meRes.body.data.admin).toBe(true);
223
+
224
+ const mePatchRes = await agent
225
+ .patch("/auth/me")
226
+ .send({email: "admin2@example.com"})
227
+ .expect(200);
228
+ expect(mePatchRes.body.data._id).toBeDefined();
229
+ expect(mePatchRes.body.data.id).toBeDefined();
230
+ expect(mePatchRes.body.data.hash).toBeUndefined();
231
+ expect(mePatchRes.body.data.email).toBe("admin2@example.com");
232
+ expect(mePatchRes.body.data.updated).toBeDefined();
233
+ expect(mePatchRes.body.data.created).toBeDefined();
234
+ expect(mePatchRes.body.data.admin).toBe(true);
235
+
236
+ // Use token to see admin foods
237
+ const getRes = await agent.get("/food").expect(200);
238
+
239
+ expect(getRes.body.data).toHaveLength(3);
240
+ const food = getRes.body.data.find((f: any) => f.name === "Apple");
241
+ expect(food).toBeDefined();
242
+
243
+ const updateRes = await agent
244
+ .patch(`/food/${food.id}`)
245
+ .set("authorization", `Bearer ${token}`)
246
+ .send({name: "Apple Pie"})
247
+ .expect(200);
248
+ expect(updateRes.body.data.name).toBe("Apple Pie");
249
+ });
250
+
251
+ it("login successfully and tokens expire", async () => {
252
+ const res = await agent
253
+ .post("/auth/login")
254
+ .send({email: "admin@example.com", password: "securePassword"})
255
+ .expect(200);
256
+ const {userId, token} = res.body.data;
257
+ expect(userId).toBeDefined();
258
+ expect(token).toBeDefined();
259
+
260
+ await agent.set("authorization", `Bearer ${res.body.data.token}`);
261
+
262
+ await agent.get("/auth/me").expect(200);
263
+
264
+ // Advance time to past token expiration
265
+ setSystemTime(Date.now() + 1000 * 60 * 60 * 24 * 30);
266
+
267
+ await agent.get("/auth/me").expect(401);
268
+ });
269
+
270
+ it("locks out after failed password attempts", async () => {
271
+ let res = await agent
272
+ .post("/auth/login")
273
+ .send({email: "admin@example.com", password: "wrong"})
274
+ .expect(401);
275
+
276
+ expect(res.body).toEqual({
277
+ message: "Password or username is incorrect",
278
+ });
279
+ let user = await UserModel.findById(admin._id);
280
+ expect((user as any)?.attempts).toBe(1);
281
+ res = await agent
282
+ .post("/auth/login")
283
+ .send({email: "admin@example.com", password: "wrong"})
284
+ .expect(401);
285
+
286
+ expect(res.body).toEqual({
287
+ message: "Password or username is incorrect",
288
+ });
289
+ user = await UserModel.findById(admin._id);
290
+ expect((user as any)?.attempts).toBe(2);
291
+ res = await agent
292
+ .post("/auth/login")
293
+ .send({email: "admin@example.com", password: "wrong"})
294
+ .expect(401);
295
+
296
+ expect(res.body).toEqual({
297
+ message: "Account locked due to too many failed login attempts",
298
+ });
299
+ user = await UserModel.findById(admin._id);
300
+ expect((user as any)?.attempts).toBe(3);
301
+
302
+ // Logging in with correct password fails because account is locked
303
+ res = await agent
304
+ .post("/auth/login")
305
+ .send({email: "admin@example.com", password: "securePassword"})
306
+ .expect(401);
307
+
308
+ expect(res.body).toEqual({
309
+ message: "Account locked due to too many failed login attempts",
310
+ });
311
+ user = await UserModel.findById(admin._id);
312
+ // Not incremented
313
+ expect((user as any)?.attempts).toBe(3);
314
+ });
315
+
316
+ it("refresh token allows refresh of auth token", async () => {
317
+ // initial login
318
+ const initialLoginRes = await agent
319
+ .post("/auth/login")
320
+ .send({email: "ADMIN@example.com", password: "securePassword"})
321
+ .expect(200);
322
+ expect(initialLoginRes.body.data.token).toBeDefined();
323
+ expect(initialLoginRes.body.data.refreshToken).toBeDefined();
324
+ const initialToken = initialLoginRes.body.data.token;
325
+ await agent.set("authorization", `Bearer ${initialToken}`);
326
+
327
+ // get new auth token from refresh token
328
+ const refreshRes = await agent
329
+ .post("/auth/refresh_token")
330
+ .send({refreshToken: initialLoginRes.body.data.refreshToken})
331
+ .expect(200);
332
+ expect(refreshRes.body.data.token).toBeDefined();
333
+ expect(refreshRes.body.data.refreshToken).toBeDefined();
334
+ const newToken = refreshRes.body.data.token;
335
+ // note that new token will most likely be the same as the old token because
336
+ // an HMAC signature will always be the same for a header + payload combination that is equal.
337
+
338
+ // make sure new token works
339
+ await agent.set("authorization", `Bearer ${newToken}`);
340
+ const meRes = await agent.get("/auth/me").expect(200);
341
+ expect(meRes.body.data._id).toBeDefined();
342
+ });
343
+
344
+ it("disabled user fails", async () => {
345
+ // initial login
346
+ const initialLoginRes = await agent
347
+ .post("/auth/login")
348
+ .send({email: "ADMIN@example.com", password: "securePassword"})
349
+ .expect(200);
350
+ expect(initialLoginRes.body.data.token).toBeDefined();
351
+ expect(initialLoginRes.body.data.refreshToken).toBeDefined();
352
+ const initialToken = initialLoginRes.body.data.token;
353
+ await agent.set("authorization", `Bearer ${initialToken}`);
354
+ const meRes = await agent.get("/auth/me").expect(200);
355
+ expect(meRes.body.data._id).toBeDefined();
356
+
357
+ admin.disabled = true;
358
+ await admin.save();
359
+
360
+ const failRes = await agent.get("/auth/me").expect(401);
361
+ expect(failRes.body).toEqual({status: 401, title: "User is disabled"});
362
+ });
363
+
364
+ it("signup user with email that is already registered", async () => {
365
+ await agent
366
+ .post("/auth/signup")
367
+ .send({age: 25, email: "new@example.com", password: "123"})
368
+ .expect(200);
369
+
370
+ const res2 = await agent
371
+ .post("/auth/signup")
372
+ .send({age: 31, email: "new@example.com", password: "456"})
373
+ .expect(500);
374
+
375
+ await timeout(1000);
376
+ expect(res2.body.title).toBe("A user with the given username is already registered");
377
+ });
378
+ });
379
+
380
+ describe("custom auth options", () => {
381
+ let app: express.Application;
382
+ let admin: any;
383
+ let notAdmin: any;
384
+
385
+ beforeEach(async () => {
386
+ // Reset to real time - don't freeze time here as passport-local-mongoose
387
+ // lockout mechanism needs real time to progress
388
+ setSystemTime();
389
+ [admin, notAdmin] = await setupDb();
390
+
391
+ await Promise.all([
392
+ FoodModel.create({
393
+ calories: 1,
394
+ created: new Date(),
395
+ name: "Spinach",
396
+ ownerId: notAdmin._id,
397
+ }),
398
+ FoodModel.create({
399
+ calories: 100,
400
+ created: Date.now() - 10,
401
+ hidden: true,
402
+ name: "Apple",
403
+ ownerId: admin._id,
404
+ }),
405
+ FoodModel.create({
406
+ calories: 100,
407
+ created: Date.now() - 10,
408
+ name: "Carrots",
409
+ ownerId: admin._id,
410
+ }),
411
+ ]);
412
+ app = getBaseServer();
413
+ addAuthRoutes(app, UserModel as any, {
414
+ // custom refresh token logic based on admin or non admin
415
+ generateTokenExpiration: (user?: {admin: boolean}) => {
416
+ if (user?.admin) {
417
+ return "30d";
418
+ }
419
+ return "365d";
420
+ },
421
+ });
422
+ setupAuth(app, UserModel as any);
423
+ addMeRoutes(app, UserModel as any);
424
+ app.use(
425
+ "/food",
426
+ modelRouter(FoodModel, {
427
+ allowAnonymous: true,
428
+ permissions: {
429
+ create: [Permissions.IsAuthenticated],
430
+ delete: [Permissions.IsAuthenticated],
431
+ list: [Permissions.IsAny],
432
+ read: [Permissions.IsAny],
433
+ update: [Permissions.IsAuthenticated],
434
+ },
435
+ queryFilter: (user?: {admin: boolean}) => {
436
+ if (!user?.admin) {
437
+ return {hidden: {$ne: true}};
438
+ }
439
+ return {};
440
+ },
441
+ transformer: AdminOwnerTransformer<Food>({
442
+ adminReadFields: ["name", "calories", "created", "ownerId"],
443
+ adminWriteFields: ["name", "calories", "created", "ownerId"],
444
+ anonReadFields: ["name"],
445
+ anonWriteFields: [],
446
+ authReadFields: ["name", "calories", "created"],
447
+ authWriteFields: ["name", "calories"],
448
+ ownerReadFields: ["name", "calories", "created", "ownerId"],
449
+ ownerWriteFields: ["name", "calories", "created"],
450
+ }),
451
+ })
452
+ );
453
+ });
454
+
455
+ afterEach(async () => {
456
+ setSystemTime();
457
+ });
458
+
459
+ it("login successfully and tokens expire with custom token options", async () => {
460
+ // login admin and set token
461
+ const adminAgent = supertest.agent(app);
462
+ const res = await adminAgent
463
+ .post("/auth/login")
464
+ .send({email: "admin@example.com", password: "securePassword"})
465
+ .expect(200);
466
+
467
+ expect(res.body.data.userId).toBeDefined();
468
+ expect(res.body.data.token).toBeDefined();
469
+
470
+ await adminAgent.set("authorization", `Bearer ${res.body.data.token}`);
471
+
472
+ // login non-admin and set token
473
+ const notAdminAgent = supertest.agent(app);
474
+ const res2 = await notAdminAgent
475
+ .post("/auth/login")
476
+ .send({email: "notadmin@example.com", password: "password"})
477
+ .expect(200);
478
+
479
+ expect(res2.body.data.userId).toBeDefined();
480
+ expect(res2.body.data.token).toBeDefined();
481
+
482
+ await notAdminAgent.set("authorization", `Bearer ${res2.body.data.token}`);
483
+
484
+ // and check that tokens are working for both users
485
+ await adminAgent.get("/auth/me").expect(200);
486
+ await notAdminAgent.get("/auth/me").expect(200);
487
+
488
+ // Advance time by 30 days check that admin can no longer access with old token,
489
+ // and non-admin can due to custom times set as auth options
490
+ setSystemTime(Date.now() + 1000 * 60 * 60 * 24 * 30);
491
+ await adminAgent.get("/auth/me").expect(401);
492
+ await notAdminAgent.get("/auth/me").expect(200);
493
+
494
+ // Advance time by an additional 335 days to pass the 365 day expiration for non-admin
495
+ setSystemTime(Date.now() + 1000 * 60 * 60 * 24 * 365);
496
+
497
+ // ensure non-admin can no longer access
498
+ await notAdminAgent.get("/auth/me").expect(401);
499
+ });
500
+ });
501
+
502
+ describe("generateTokens", () => {
503
+ const OLD_ENV = process.env;
504
+
505
+ beforeEach(() => {
506
+ process.env = {...OLD_ENV};
507
+ process.env.TOKEN_SECRET = "secret";
508
+ process.env.REFRESH_TOKEN_SECRET = "refresh_secret";
509
+ });
510
+
511
+ afterEach(() => {
512
+ process.env = OLD_ENV;
513
+ });
514
+
515
+ it("generates a token and refresh token for a valid user", async () => {
516
+ const user = {_id: "12345"};
517
+ const {token, refreshToken} = await generateTokens(user);
518
+
519
+ expect(token).toBeDefined();
520
+ expect(refreshToken).toBeDefined();
521
+
522
+ // Verify token structure
523
+ const tokenParts = token?.split(".");
524
+ expect(tokenParts?.length).toBe(3);
525
+ });
526
+
527
+ it("throws an error if TOKEN_SECRET is missing", async () => {
528
+ process.env.TOKEN_SECRET = undefined;
529
+ const user = {_id: "12345"};
530
+
531
+ await expect(generateTokens(user)).rejects.toThrow("TOKEN_SECRET must be set in env.");
532
+ });
533
+
534
+ it("returns null tokens if user is missing", async () => {
535
+ const result = await generateTokens(undefined);
536
+ expect(result).toEqual({refreshToken: null, token: null});
537
+ });
538
+
539
+ it("respects custom expiration from authOptions", async () => {
540
+ const user = {_id: "12345"};
541
+ const authOptions = {
542
+ generateRefreshTokenExpiration: () => "7d" as jwt.SignOptions["expiresIn"],
543
+ generateTokenExpiration: () => "1h" as jwt.SignOptions["expiresIn"],
544
+ };
545
+ const {token, refreshToken} = await generateTokens(user, authOptions);
546
+
547
+ expect(token).toBeDefined();
548
+ expect(refreshToken).toBeDefined();
549
+ });
550
+ });