@xenterprises/fastify-xauth-local 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,744 @@
1
+ /**
2
+ * xAuthLocal Tests
3
+ *
4
+ * Tests for the JWT authentication plugin with multi-config support.
5
+ */
6
+
7
+ import { describe, test, beforeEach, afterEach } from "node:test";
8
+ import assert from "node:assert";
9
+ import Fastify from "fastify";
10
+ import xAuthLocal, {
11
+ createJwtService,
12
+ createAuthMiddleware,
13
+ createRoleMiddleware,
14
+ isExcludedRoute,
15
+ hashPassword,
16
+ comparePassword,
17
+ } from "../src/xAuthLocal.js";
18
+
19
+ // Test secrets for HS256
20
+ const API_SECRET = "api-secret-key-that-is-long-enough-for-hs256";
21
+ const ADMIN_SECRET = "admin-secret-key-that-is-long-enough-for-hs256";
22
+
23
+ describe("xAuthLocal Plugin", () => {
24
+ let fastify;
25
+
26
+ beforeEach(() => {
27
+ fastify = Fastify();
28
+ });
29
+
30
+ afterEach(async () => {
31
+ await fastify.close();
32
+ });
33
+
34
+ describe("Configuration Validation", () => {
35
+ test("should throw if configs array is empty", async () => {
36
+ await assert.rejects(
37
+ async () => {
38
+ await fastify.register(xAuthLocal, { configs: [] });
39
+ await fastify.ready();
40
+ },
41
+ { message: /configs array is required/i }
42
+ );
43
+ });
44
+
45
+ test("should throw if configs is not provided", async () => {
46
+ await assert.rejects(
47
+ async () => {
48
+ await fastify.register(xAuthLocal, {});
49
+ await fastify.ready();
50
+ },
51
+ { message: /configs array is required/i }
52
+ );
53
+ });
54
+
55
+ test("should throw if config missing name", async () => {
56
+ await assert.rejects(
57
+ async () => {
58
+ await fastify.register(xAuthLocal, {
59
+ configs: [{ prefix: "/api", secret: API_SECRET }],
60
+ });
61
+ await fastify.ready();
62
+ },
63
+ { message: /must have a 'name'/i }
64
+ );
65
+ });
66
+
67
+ test("should throw if config missing prefix", async () => {
68
+ await assert.rejects(
69
+ async () => {
70
+ await fastify.register(xAuthLocal, {
71
+ configs: [{ name: "api", secret: API_SECRET }],
72
+ });
73
+ await fastify.ready();
74
+ },
75
+ { message: /must have a 'prefix'/i }
76
+ );
77
+ });
78
+
79
+ test("should throw if config missing secret/keys", async () => {
80
+ await assert.rejects(
81
+ async () => {
82
+ await fastify.register(xAuthLocal, {
83
+ configs: [{ name: "api", prefix: "/api" }],
84
+ });
85
+ await fastify.ready();
86
+ },
87
+ { message: /requires secret or publicKey/i }
88
+ );
89
+ });
90
+
91
+ test("should accept valid config", async () => {
92
+ await fastify.register(xAuthLocal, {
93
+ configs: [{ name: "api", prefix: "/api", secret: API_SECRET }],
94
+ });
95
+ await fastify.ready();
96
+
97
+ assert.ok(fastify.xauthlocal);
98
+ assert.ok(fastify.xauthlocal.configs.api);
99
+ });
100
+
101
+ test("should skip registration when active is false", async () => {
102
+ await fastify.register(xAuthLocal, {
103
+ active: false,
104
+ configs: [{ name: "api", prefix: "/api", secret: API_SECRET }],
105
+ });
106
+ await fastify.ready();
107
+
108
+ assert.strictEqual(fastify.xauthlocal, undefined);
109
+ });
110
+ });
111
+
112
+ describe("Multiple Configs", () => {
113
+ test("should register multiple configs", async () => {
114
+ await fastify.register(xAuthLocal, {
115
+ configs: [
116
+ { name: "api", prefix: "/api", secret: API_SECRET },
117
+ { name: "admin", prefix: "/admin", secret: ADMIN_SECRET },
118
+ ],
119
+ });
120
+ await fastify.ready();
121
+
122
+ assert.ok(fastify.xauthlocal.configs.api);
123
+ assert.ok(fastify.xauthlocal.configs.admin);
124
+ assert.strictEqual(fastify.xauthlocal.config.configCount, 2);
125
+ assert.deepStrictEqual(fastify.xauthlocal.config.configNames, ["api", "admin"]);
126
+ });
127
+
128
+ test("should use different secrets for each config", async () => {
129
+ await fastify.register(xAuthLocal, {
130
+ configs: [
131
+ { name: "api", prefix: "/api", secret: API_SECRET },
132
+ { name: "admin", prefix: "/admin", secret: ADMIN_SECRET },
133
+ ],
134
+ });
135
+ await fastify.ready();
136
+
137
+ const apiToken = fastify.xauthlocal.configs.api.jwt.sign({ id: 1 });
138
+ const adminToken = fastify.xauthlocal.configs.admin.jwt.sign({ id: 2 });
139
+
140
+ // Each config should verify its own tokens
141
+ const apiDecoded = fastify.xauthlocal.configs.api.jwt.verify(apiToken);
142
+ assert.strictEqual(apiDecoded.id, 1);
143
+
144
+ const adminDecoded = fastify.xauthlocal.configs.admin.jwt.verify(adminToken);
145
+ assert.strictEqual(adminDecoded.id, 2);
146
+
147
+ // Cross-verification should fail
148
+ assert.throws(() => {
149
+ fastify.xauthlocal.configs.api.jwt.verify(adminToken);
150
+ });
151
+ });
152
+
153
+ test("should get config by name", async () => {
154
+ await fastify.register(xAuthLocal, {
155
+ configs: [{ name: "api", prefix: "/api", secret: API_SECRET }],
156
+ });
157
+ await fastify.ready();
158
+
159
+ const apiConfig = fastify.xauthlocal.get("api");
160
+ assert.ok(apiConfig);
161
+ assert.strictEqual(apiConfig.name, "api");
162
+ assert.strictEqual(apiConfig.prefix, "/api");
163
+ });
164
+ });
165
+
166
+ describe("Route Protection", () => {
167
+ test("should protect routes matching config prefix", async () => {
168
+ await fastify.register(xAuthLocal, {
169
+ configs: [{ name: "api", prefix: "/api", secret: API_SECRET }],
170
+ });
171
+
172
+ fastify.get("/api/users", async (request) => ({ auth: request.auth }));
173
+ fastify.get("/public/info", async () => ({ info: "public" }));
174
+
175
+ await fastify.ready();
176
+
177
+ // API route should require auth
178
+ const apiResponse = await fastify.inject({
179
+ method: "GET",
180
+ url: "/api/users",
181
+ });
182
+ assert.strictEqual(apiResponse.statusCode, 401);
183
+
184
+ // Public route should not require auth
185
+ const publicResponse = await fastify.inject({
186
+ method: "GET",
187
+ url: "/public/info",
188
+ });
189
+ assert.strictEqual(publicResponse.statusCode, 200);
190
+ });
191
+
192
+ test("should accept valid token for protected route", async () => {
193
+ await fastify.register(xAuthLocal, {
194
+ configs: [{ name: "api", prefix: "/api", secret: API_SECRET }],
195
+ });
196
+
197
+ fastify.get("/api/users", async (request) => ({ auth: request.auth }));
198
+
199
+ await fastify.ready();
200
+
201
+ const token = fastify.xauthlocal.configs.api.jwt.sign({ id: 1, email: "test@example.com" });
202
+
203
+ const response = await fastify.inject({
204
+ method: "GET",
205
+ url: "/api/users",
206
+ headers: {
207
+ authorization: `Bearer ${token}`,
208
+ },
209
+ });
210
+
211
+ assert.strictEqual(response.statusCode, 200);
212
+ const body = JSON.parse(response.body);
213
+ assert.strictEqual(body.auth.id, 1);
214
+ });
215
+
216
+ test("should reject wrong token for protected route", async () => {
217
+ await fastify.register(xAuthLocal, {
218
+ configs: [
219
+ { name: "api", prefix: "/api", secret: API_SECRET },
220
+ { name: "admin", prefix: "/admin", secret: ADMIN_SECRET },
221
+ ],
222
+ });
223
+
224
+ fastify.get("/api/users", async (request) => ({ auth: request.auth }));
225
+
226
+ await fastify.ready();
227
+
228
+ // Use admin token for API route
229
+ const adminToken = fastify.xauthlocal.configs.admin.jwt.sign({ id: 1 });
230
+
231
+ const response = await fastify.inject({
232
+ method: "GET",
233
+ url: "/api/users",
234
+ headers: {
235
+ authorization: `Bearer ${adminToken}`,
236
+ },
237
+ });
238
+
239
+ assert.strictEqual(response.statusCode, 401);
240
+ });
241
+ });
242
+
243
+ describe("Role Middleware", () => {
244
+ test("should allow access with matching role", async () => {
245
+ await fastify.register(xAuthLocal, {
246
+ configs: [{ name: "api", prefix: "/api", secret: API_SECRET }],
247
+ });
248
+
249
+ fastify.get(
250
+ "/api/admin",
251
+ { preHandler: [fastify.xauthlocal.configs.api.requireRole("admin")] },
252
+ async () => ({ admin: true })
253
+ );
254
+
255
+ await fastify.ready();
256
+
257
+ const token = fastify.xauthlocal.configs.api.jwt.sign({ id: 1, scope: ["admin"] });
258
+
259
+ const response = await fastify.inject({
260
+ method: "GET",
261
+ url: "/api/admin",
262
+ headers: {
263
+ authorization: `Bearer ${token}`,
264
+ },
265
+ });
266
+
267
+ assert.strictEqual(response.statusCode, 200);
268
+ });
269
+
270
+ test("should deny access without matching role", async () => {
271
+ await fastify.register(xAuthLocal, {
272
+ configs: [{ name: "api", prefix: "/api", secret: API_SECRET }],
273
+ });
274
+
275
+ fastify.get(
276
+ "/api/admin",
277
+ { preHandler: [fastify.xauthlocal.configs.api.requireRole("admin")] },
278
+ async () => ({ admin: true })
279
+ );
280
+
281
+ await fastify.ready();
282
+
283
+ const token = fastify.xauthlocal.configs.api.jwt.sign({ id: 1, scope: ["user"] });
284
+
285
+ const response = await fastify.inject({
286
+ method: "GET",
287
+ url: "/api/admin",
288
+ headers: {
289
+ authorization: `Bearer ${token}`,
290
+ },
291
+ });
292
+
293
+ assert.strictEqual(response.statusCode, 403);
294
+ });
295
+ });
296
+
297
+ describe("Custom Request Property", () => {
298
+ test("should use custom requestProperty", async () => {
299
+ await fastify.register(xAuthLocal, {
300
+ configs: [
301
+ { name: "api", prefix: "/api", secret: API_SECRET, requestProperty: "user" },
302
+ ],
303
+ });
304
+
305
+ fastify.get("/api/users", async (request) => ({ user: request.user }));
306
+
307
+ await fastify.ready();
308
+
309
+ const token = fastify.xauthlocal.configs.api.jwt.sign({ id: 1 });
310
+
311
+ const response = await fastify.inject({
312
+ method: "GET",
313
+ url: "/api/users",
314
+ headers: {
315
+ authorization: `Bearer ${token}`,
316
+ },
317
+ });
318
+
319
+ assert.strictEqual(response.statusCode, 200);
320
+ const body = JSON.parse(response.body);
321
+ assert.strictEqual(body.user.id, 1);
322
+ });
323
+ });
324
+
325
+ describe("Excluded Paths", () => {
326
+ test("should exclude paths from auth", async () => {
327
+ await fastify.register(xAuthLocal, {
328
+ configs: [
329
+ {
330
+ name: "api",
331
+ prefix: "/api",
332
+ secret: API_SECRET,
333
+ excludedPaths: ["/api/public"],
334
+ },
335
+ ],
336
+ });
337
+
338
+ fastify.get("/api/public/health", async () => ({ status: "ok" }));
339
+ fastify.get("/api/protected", async () => ({ protected: true }));
340
+
341
+ await fastify.ready();
342
+
343
+ // Excluded path should not require auth
344
+ const publicResponse = await fastify.inject({
345
+ method: "GET",
346
+ url: "/api/public/health",
347
+ });
348
+ assert.strictEqual(publicResponse.statusCode, 200);
349
+
350
+ // Non-excluded path should require auth
351
+ const protectedResponse = await fastify.inject({
352
+ method: "GET",
353
+ url: "/api/protected",
354
+ });
355
+ assert.strictEqual(protectedResponse.statusCode, 401);
356
+ });
357
+ });
358
+ });
359
+
360
+ describe("Local Routes", () => {
361
+ let fastify;
362
+ const users = new Map();
363
+
364
+ beforeEach(() => {
365
+ fastify = Fastify();
366
+ users.clear();
367
+ });
368
+
369
+ afterEach(async () => {
370
+ await fastify.close();
371
+ });
372
+
373
+ test("should register local routes when enabled", async () => {
374
+ await fastify.register(xAuthLocal, {
375
+ configs: [
376
+ {
377
+ name: "api",
378
+ prefix: "/api",
379
+ secret: API_SECRET,
380
+ local: {
381
+ enabled: true,
382
+ userLookup: async (email) => users.get(email),
383
+ },
384
+ },
385
+ ],
386
+ });
387
+ await fastify.ready();
388
+
389
+ assert.strictEqual(fastify.xauthlocal.configs.api.hasLocalRoutes, true);
390
+ assert.strictEqual(fastify.xauthlocal.configs.api.localPrefix, "/api/local");
391
+ });
392
+
393
+ test("POST /api/local should authenticate valid user", async () => {
394
+ const hashedPassword = await hashPassword("password123");
395
+ users.set("test@example.com", {
396
+ id: 1,
397
+ email: "test@example.com",
398
+ password: hashedPassword,
399
+ first_name: "Test",
400
+ last_name: "User",
401
+ scope: ["user"],
402
+ });
403
+
404
+ await fastify.register(xAuthLocal, {
405
+ configs: [
406
+ {
407
+ name: "api",
408
+ prefix: "/api",
409
+ secret: API_SECRET,
410
+ local: {
411
+ enabled: true,
412
+ userLookup: async (email) => users.get(email),
413
+ },
414
+ },
415
+ ],
416
+ });
417
+ await fastify.ready();
418
+
419
+ const response = await fastify.inject({
420
+ method: "POST",
421
+ url: "/api/local",
422
+ payload: {
423
+ email: "test@example.com",
424
+ password: "password123",
425
+ },
426
+ });
427
+
428
+ assert.strictEqual(response.statusCode, 200);
429
+ const body = JSON.parse(response.body);
430
+ assert.ok(body.token);
431
+ assert.strictEqual(body.user.email, "test@example.com");
432
+ });
433
+
434
+ test("GET /api/local/me should return authenticated user (skipUserLookup)", async () => {
435
+ await fastify.register(xAuthLocal, {
436
+ configs: [
437
+ {
438
+ name: "api",
439
+ prefix: "/api",
440
+ secret: API_SECRET,
441
+ local: {
442
+ enabled: true,
443
+ skipUserLookup: true,
444
+ },
445
+ },
446
+ ],
447
+ });
448
+ await fastify.ready();
449
+
450
+ const token = fastify.xauthlocal.configs.api.jwt.sign({
451
+ id: 1,
452
+ email: "test@example.com",
453
+ first_name: "Test",
454
+ last_name: "User",
455
+ scope: ["user"],
456
+ });
457
+
458
+ const response = await fastify.inject({
459
+ method: "GET",
460
+ url: "/api/local/me",
461
+ headers: {
462
+ authorization: `Bearer ${token}`,
463
+ },
464
+ });
465
+
466
+ assert.strictEqual(response.statusCode, 200);
467
+ const body = JSON.parse(response.body);
468
+ assert.strictEqual(body.email, "test@example.com");
469
+ assert.deepStrictEqual(body.scope, ["user"]);
470
+ });
471
+
472
+ test("GET /api/local/me should fetch fresh user data when skipUserLookup is false", async () => {
473
+ users.set("test@example.com", {
474
+ id: 1,
475
+ email: "test@example.com",
476
+ first_name: "Updated",
477
+ last_name: "Name",
478
+ scope: ["admin"],
479
+ });
480
+
481
+ await fastify.register(xAuthLocal, {
482
+ configs: [
483
+ {
484
+ name: "api",
485
+ prefix: "/api",
486
+ secret: API_SECRET,
487
+ local: {
488
+ enabled: true,
489
+ skipUserLookup: false,
490
+ userLookup: async (email) => users.get(email),
491
+ },
492
+ },
493
+ ],
494
+ });
495
+ await fastify.ready();
496
+
497
+ // Token has old data
498
+ const token = fastify.xauthlocal.configs.api.jwt.sign({
499
+ id: 1,
500
+ email: "test@example.com",
501
+ first_name: "Original",
502
+ last_name: "User",
503
+ scope: ["user"],
504
+ });
505
+
506
+ const response = await fastify.inject({
507
+ method: "GET",
508
+ url: "/api/local/me",
509
+ headers: {
510
+ authorization: `Bearer ${token}`,
511
+ },
512
+ });
513
+
514
+ assert.strictEqual(response.statusCode, 200);
515
+ const body = JSON.parse(response.body);
516
+ // Should have fresh data from userLookup
517
+ assert.strictEqual(body.first_name, "Updated");
518
+ assert.deepStrictEqual(body.scope, ["admin"]);
519
+ });
520
+
521
+ test("should support multiple configs with separate local routes", async () => {
522
+ const apiUsers = new Map();
523
+ const adminUsers = new Map();
524
+
525
+ const apiHashedPassword = await hashPassword("apipass");
526
+ apiUsers.set("api@example.com", {
527
+ id: 1,
528
+ email: "api@example.com",
529
+ password: apiHashedPassword,
530
+ first_name: "API",
531
+ last_name: "User",
532
+ });
533
+
534
+ const adminHashedPassword = await hashPassword("adminpass");
535
+ adminUsers.set("admin@example.com", {
536
+ id: 2,
537
+ email: "admin@example.com",
538
+ password: adminHashedPassword,
539
+ first_name: "Admin",
540
+ last_name: "User",
541
+ });
542
+
543
+ await fastify.register(xAuthLocal, {
544
+ configs: [
545
+ {
546
+ name: "api",
547
+ prefix: "/api",
548
+ secret: API_SECRET,
549
+ local: {
550
+ enabled: true,
551
+ userLookup: async (email) => apiUsers.get(email),
552
+ },
553
+ },
554
+ {
555
+ name: "admin",
556
+ prefix: "/admin",
557
+ secret: ADMIN_SECRET,
558
+ local: {
559
+ enabled: true,
560
+ userLookup: async (email) => adminUsers.get(email),
561
+ },
562
+ },
563
+ ],
564
+ });
565
+ await fastify.ready();
566
+
567
+ // Login to API
568
+ const apiResponse = await fastify.inject({
569
+ method: "POST",
570
+ url: "/api/local",
571
+ payload: { email: "api@example.com", password: "apipass" },
572
+ });
573
+ assert.strictEqual(apiResponse.statusCode, 200);
574
+ const apiBody = JSON.parse(apiResponse.body);
575
+ assert.strictEqual(apiBody.user.first_name, "API");
576
+
577
+ // Login to Admin
578
+ const adminResponse = await fastify.inject({
579
+ method: "POST",
580
+ url: "/admin/local",
581
+ payload: { email: "admin@example.com", password: "adminpass" },
582
+ });
583
+ assert.strictEqual(adminResponse.statusCode, 200);
584
+ const adminBody = JSON.parse(adminResponse.body);
585
+ assert.strictEqual(adminBody.user.first_name, "Admin");
586
+ });
587
+
588
+ test("POST /register should create new user", async () => {
589
+ await fastify.register(xAuthLocal, {
590
+ configs: [
591
+ {
592
+ name: "api",
593
+ prefix: "/api",
594
+ secret: API_SECRET,
595
+ local: {
596
+ enabled: true,
597
+ userLookup: async (email) => users.get(email),
598
+ createUser: async (userData) => {
599
+ const user = { id: 1, ...userData };
600
+ users.set(userData.email, user);
601
+ return user;
602
+ },
603
+ },
604
+ },
605
+ ],
606
+ });
607
+ await fastify.ready();
608
+
609
+ const response = await fastify.inject({
610
+ method: "POST",
611
+ url: "/api/local/register",
612
+ payload: {
613
+ email: "new@example.com",
614
+ password: "password123",
615
+ first_name: "New",
616
+ last_name: "User",
617
+ },
618
+ });
619
+
620
+ assert.strictEqual(response.statusCode, 201);
621
+ const body = JSON.parse(response.body);
622
+ assert.ok(body.token);
623
+ assert.strictEqual(body.user.email, "new@example.com");
624
+ });
625
+
626
+ test("POST /password-reset should not reveal if email exists", async () => {
627
+ await fastify.register(xAuthLocal, {
628
+ configs: [
629
+ {
630
+ name: "api",
631
+ prefix: "/api",
632
+ secret: API_SECRET,
633
+ local: {
634
+ enabled: true,
635
+ userLookup: async () => null,
636
+ passwordReset: async () => {},
637
+ },
638
+ },
639
+ ],
640
+ });
641
+ await fastify.ready();
642
+
643
+ const response = await fastify.inject({
644
+ method: "POST",
645
+ url: "/api/local/password-reset",
646
+ payload: { email: "nonexistent@example.com" },
647
+ });
648
+
649
+ assert.strictEqual(response.statusCode, 200);
650
+ const body = JSON.parse(response.body);
651
+ assert.ok(body.message.includes("If an account"));
652
+ });
653
+ });
654
+
655
+ describe("JWT Service (Standalone)", () => {
656
+ test("should create service with secret", () => {
657
+ const jwt = createJwtService({ secret: API_SECRET });
658
+
659
+ assert.strictEqual(jwt.algorithm, "HS256");
660
+
661
+ const token = jwt.sign({ id: 1 });
662
+ const decoded = jwt.verify(token);
663
+
664
+ assert.strictEqual(decoded.id, 1);
665
+ });
666
+
667
+ test("should include audience and issuer", () => {
668
+ const jwt = createJwtService({
669
+ secret: API_SECRET,
670
+ audience: "my-app",
671
+ issuer: "auth-server",
672
+ });
673
+
674
+ const token = jwt.sign({ id: 1 });
675
+ const decoded = jwt.verify(token);
676
+
677
+ assert.strictEqual(decoded.aud, "my-app");
678
+ assert.strictEqual(decoded.iss, "auth-server");
679
+ });
680
+ });
681
+
682
+ describe("isExcludedRoute", () => {
683
+ test("should return false for empty excludedPaths", () => {
684
+ assert.strictEqual(isExcludedRoute("/api/test", "GET", []), false);
685
+ assert.strictEqual(isExcludedRoute("/api/test", "GET", null), false);
686
+ });
687
+
688
+ test("should match string prefix", () => {
689
+ assert.strictEqual(isExcludedRoute("/public/test", "GET", ["/public"]), true);
690
+ assert.strictEqual(isExcludedRoute("/api/test", "GET", ["/public"]), false);
691
+ });
692
+
693
+ test("should match regex", () => {
694
+ assert.strictEqual(isExcludedRoute("/api/v1/test", "GET", [/^\/api\/v\d+/]), true);
695
+ assert.strictEqual(isExcludedRoute("/other/test", "GET", [/^\/api\/v\d+/]), false);
696
+ });
697
+
698
+ test("should respect method restriction", () => {
699
+ const rules = [{ url: "/login", methods: ["POST"] }];
700
+
701
+ assert.strictEqual(isExcludedRoute("/login", "POST", rules), true);
702
+ assert.strictEqual(isExcludedRoute("/login", "GET", rules), false);
703
+ });
704
+ });
705
+
706
+ describe("Password Utilities", () => {
707
+ test("should hash password", async () => {
708
+ const hash = await hashPassword("mypassword");
709
+
710
+ assert.ok(hash);
711
+ assert.ok(hash.startsWith("$2"));
712
+ assert.notStrictEqual(hash, "mypassword");
713
+ });
714
+
715
+ test("should compare password correctly", async () => {
716
+ const hash = await hashPassword("mypassword");
717
+
718
+ const valid = await comparePassword("mypassword", hash);
719
+ const invalid = await comparePassword("wrongpassword", hash);
720
+
721
+ assert.strictEqual(valid, true);
722
+ assert.strictEqual(invalid, false);
723
+ });
724
+ });
725
+
726
+ describe("Role Middleware (Standalone)", () => {
727
+ test("should create middleware with single role", () => {
728
+ const middleware = createRoleMiddleware("admin");
729
+ assert.ok(typeof middleware === "function");
730
+ });
731
+
732
+ test("should create middleware with multiple roles", () => {
733
+ const middleware = createRoleMiddleware(["admin", "manager"]);
734
+ assert.ok(typeof middleware === "function");
735
+ });
736
+ });
737
+
738
+ describe("Auth Middleware (Standalone)", () => {
739
+ test("should create middleware", () => {
740
+ const jwt = createJwtService({ secret: API_SECRET });
741
+ const middleware = createAuthMiddleware(jwt, {});
742
+ assert.ok(typeof middleware === "function");
743
+ });
744
+ });