@checkstack/auth-backend 0.2.2 → 0.3.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.
- package/CHANGELOG.md +12 -0
- package/package.json +1 -1
- package/src/index.ts +43 -86
- package/src/router.test.ts +415 -30
- package/src/router.ts +299 -88
package/src/router.test.ts
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
import { describe, it, expect, mock } from "bun:test";
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
createAuthRouter,
|
|
4
|
+
ADMIN_ROLE_ID,
|
|
5
|
+
USERS_ROLE_ID,
|
|
6
|
+
ANONYMOUS_ROLE_ID,
|
|
7
|
+
APPLICATIONS_ROLE_ID,
|
|
8
|
+
} from "./router";
|
|
3
9
|
import { createMockRpcContext } from "@checkstack/backend-api";
|
|
4
10
|
import { call } from "@orpc/server";
|
|
5
11
|
import { z } from "zod";
|
|
@@ -82,7 +88,7 @@ describe("Auth Router", () => {
|
|
|
82
88
|
mockRegistry,
|
|
83
89
|
async () => {},
|
|
84
90
|
mockConfigService,
|
|
85
|
-
mockAccessRuleRegistry
|
|
91
|
+
mockAccessRuleRegistry,
|
|
86
92
|
);
|
|
87
93
|
|
|
88
94
|
it("getAccessRules returns current user access rules", async () => {
|
|
@@ -96,7 +102,7 @@ describe("Auth Router", () => {
|
|
|
96
102
|
|
|
97
103
|
mockDb.select.mockImplementationOnce(() => ({
|
|
98
104
|
from: mock(() =>
|
|
99
|
-
createChain([{ id: "1", email: "user1@test.com", name: "User 1" }])
|
|
105
|
+
createChain([{ id: "1", email: "user1@test.com", name: "User 1" }]),
|
|
100
106
|
),
|
|
101
107
|
}));
|
|
102
108
|
mockDb.select.mockImplementationOnce(() => ({
|
|
@@ -108,21 +114,32 @@ describe("Auth Router", () => {
|
|
|
108
114
|
expect(result[0].roles).toContain("admin");
|
|
109
115
|
});
|
|
110
116
|
|
|
111
|
-
it("deleteUser prevents deleting
|
|
117
|
+
it("deleteUser prevents deleting users with admin role", async () => {
|
|
112
118
|
const context = createMockRpcContext({ user: mockUser });
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
119
|
+
|
|
120
|
+
// Mock user has admin role
|
|
121
|
+
mockDb.select.mockImplementationOnce(() => ({
|
|
122
|
+
from: mock(() => createChain([{ roleId: ADMIN_ROLE_ID }])),
|
|
123
|
+
}));
|
|
124
|
+
|
|
125
|
+
await expect(
|
|
126
|
+
call(router.deleteUser, "admin-user-id", { context }),
|
|
127
|
+
).rejects.toThrow("admin role");
|
|
116
128
|
});
|
|
117
129
|
|
|
118
130
|
it("deleteUser cascades to delete related records", async () => {
|
|
119
131
|
const context = createMockRpcContext({ user: mockUser });
|
|
120
132
|
const userId = "user-to-delete";
|
|
121
133
|
|
|
134
|
+
// Mock user has no admin role
|
|
135
|
+
mockDb.select.mockImplementationOnce(() => ({
|
|
136
|
+
from: mock(() => createChain([{ roleId: USERS_ROLE_ID }])),
|
|
137
|
+
}));
|
|
138
|
+
|
|
122
139
|
// Track which tables had delete called on them
|
|
123
|
-
const deletedTables:
|
|
124
|
-
const mockTx:
|
|
125
|
-
delete: mock((table:
|
|
140
|
+
const deletedTables: unknown[] = [];
|
|
141
|
+
const mockTx: unknown = {
|
|
142
|
+
delete: mock((table: unknown) => {
|
|
126
143
|
deletedTables.push(table); // Track table
|
|
127
144
|
return {
|
|
128
145
|
where: mock(() => Promise.resolve()),
|
|
@@ -130,7 +147,9 @@ describe("Auth Router", () => {
|
|
|
130
147
|
}),
|
|
131
148
|
};
|
|
132
149
|
|
|
133
|
-
mockDb.transaction.mockImplementationOnce((cb:
|
|
150
|
+
mockDb.transaction.mockImplementationOnce((cb: (tx: unknown) => unknown) =>
|
|
151
|
+
cb(mockTx),
|
|
152
|
+
);
|
|
134
153
|
|
|
135
154
|
await call(router.deleteUser, userId, { context });
|
|
136
155
|
|
|
@@ -152,7 +171,7 @@ describe("Auth Router", () => {
|
|
|
152
171
|
}));
|
|
153
172
|
mockDb.select.mockImplementationOnce(() => ({
|
|
154
173
|
from: mock(() =>
|
|
155
|
-
createChain([{ roleId: "admin", accessRuleId: "users.manage" }])
|
|
174
|
+
createChain([{ roleId: "admin", accessRuleId: "users.manage" }]),
|
|
156
175
|
),
|
|
157
176
|
}));
|
|
158
177
|
|
|
@@ -168,7 +187,7 @@ describe("Auth Router", () => {
|
|
|
168
187
|
const result = await call(
|
|
169
188
|
router.updateUserRoles,
|
|
170
189
|
{ userId: "other-user", roles: ["admin"] },
|
|
171
|
-
{ context }
|
|
190
|
+
{ context },
|
|
172
191
|
);
|
|
173
192
|
// updateUserRoles returns void, so just check it completed
|
|
174
193
|
expect(result).toBeUndefined();
|
|
@@ -182,8 +201,8 @@ describe("Auth Router", () => {
|
|
|
182
201
|
call(
|
|
183
202
|
router.updateUserRoles,
|
|
184
203
|
{ userId: "test-user", roles: ["admin"] },
|
|
185
|
-
{ context }
|
|
186
|
-
)
|
|
204
|
+
{ context },
|
|
205
|
+
),
|
|
187
206
|
).rejects.toThrow("Cannot update your own roles");
|
|
188
207
|
});
|
|
189
208
|
|
|
@@ -200,7 +219,7 @@ describe("Auth Router", () => {
|
|
|
200
219
|
const result = await call(
|
|
201
220
|
router.updateStrategy,
|
|
202
221
|
{ id: "credential", enabled: false },
|
|
203
|
-
{ context }
|
|
222
|
+
{ context },
|
|
204
223
|
);
|
|
205
224
|
expect(result.success).toBe(true);
|
|
206
225
|
expect(mockConfigService.set).toHaveBeenCalled();
|
|
@@ -219,14 +238,14 @@ describe("Auth Router", () => {
|
|
|
219
238
|
const result = await call(
|
|
220
239
|
router.setRegistrationStatus,
|
|
221
240
|
{ allowRegistration: false },
|
|
222
|
-
{ context }
|
|
241
|
+
{ context },
|
|
223
242
|
);
|
|
224
243
|
expect(result.success).toBe(true);
|
|
225
244
|
expect(mockConfigService.set).toHaveBeenCalledWith(
|
|
226
245
|
"platform.registration",
|
|
227
246
|
expect.anything(),
|
|
228
247
|
1,
|
|
229
|
-
{ allowRegistration: false }
|
|
248
|
+
{ allowRegistration: false },
|
|
230
249
|
);
|
|
231
250
|
});
|
|
232
251
|
|
|
@@ -249,7 +268,7 @@ describe("Auth Router", () => {
|
|
|
249
268
|
const result = await call(
|
|
250
269
|
router.findUserByEmail,
|
|
251
270
|
{ email: "test@example.com" },
|
|
252
|
-
{ context }
|
|
271
|
+
{ context },
|
|
253
272
|
);
|
|
254
273
|
expect(result).toEqual({ id: "user-123" });
|
|
255
274
|
});
|
|
@@ -264,7 +283,7 @@ describe("Auth Router", () => {
|
|
|
264
283
|
const result = await call(
|
|
265
284
|
router.findUserByEmail,
|
|
266
285
|
{ email: "nonexistent@example.com" },
|
|
267
|
-
{ context }
|
|
286
|
+
{ context },
|
|
268
287
|
);
|
|
269
288
|
expect(result).toBeUndefined();
|
|
270
289
|
});
|
|
@@ -289,7 +308,7 @@ describe("Auth Router", () => {
|
|
|
289
308
|
accountId: "ldapuser",
|
|
290
309
|
password: "hashed-password",
|
|
291
310
|
},
|
|
292
|
-
{ context }
|
|
311
|
+
{ context },
|
|
293
312
|
);
|
|
294
313
|
|
|
295
314
|
expect(result.created).toBe(true);
|
|
@@ -322,7 +341,7 @@ describe("Auth Router", () => {
|
|
|
322
341
|
password: "hashed-password",
|
|
323
342
|
autoUpdateUser: true,
|
|
324
343
|
},
|
|
325
|
-
{ context }
|
|
344
|
+
{ context },
|
|
326
345
|
);
|
|
327
346
|
|
|
328
347
|
expect(result.created).toBe(false);
|
|
@@ -342,7 +361,7 @@ describe("Auth Router", () => {
|
|
|
342
361
|
token: "session-token",
|
|
343
362
|
expiresAt,
|
|
344
363
|
},
|
|
345
|
-
{ context }
|
|
364
|
+
{ context },
|
|
346
365
|
);
|
|
347
366
|
|
|
348
367
|
expect(result.sessionId).toBeDefined();
|
|
@@ -371,7 +390,7 @@ describe("Auth Router", () => {
|
|
|
371
390
|
name: "New User",
|
|
372
391
|
password: "ValidPass123",
|
|
373
392
|
},
|
|
374
|
-
{ context }
|
|
393
|
+
{ context },
|
|
375
394
|
);
|
|
376
395
|
|
|
377
396
|
expect(result.userId).toBeDefined();
|
|
@@ -390,8 +409,8 @@ describe("Auth Router", () => {
|
|
|
390
409
|
name: "Test User",
|
|
391
410
|
password: "weakpass1",
|
|
392
411
|
},
|
|
393
|
-
{ context }
|
|
394
|
-
)
|
|
412
|
+
{ context },
|
|
413
|
+
),
|
|
395
414
|
).rejects.toThrow("uppercase");
|
|
396
415
|
});
|
|
397
416
|
|
|
@@ -414,8 +433,8 @@ describe("Auth Router", () => {
|
|
|
414
433
|
name: "Existing User",
|
|
415
434
|
password: "ValidPass123",
|
|
416
435
|
},
|
|
417
|
-
{ context }
|
|
418
|
-
)
|
|
436
|
+
{ context },
|
|
437
|
+
),
|
|
419
438
|
).rejects.toThrow("already exists");
|
|
420
439
|
});
|
|
421
440
|
|
|
@@ -433,8 +452,374 @@ describe("Auth Router", () => {
|
|
|
433
452
|
name: "Test User",
|
|
434
453
|
password: "ValidPass123",
|
|
435
454
|
},
|
|
436
|
-
{ context }
|
|
437
|
-
)
|
|
455
|
+
{ context },
|
|
456
|
+
),
|
|
438
457
|
).rejects.toThrow("not enabled");
|
|
439
458
|
});
|
|
459
|
+
|
|
460
|
+
// ==========================================================================
|
|
461
|
+
// ONBOARDING ENDPOINT TESTS
|
|
462
|
+
// ==========================================================================
|
|
463
|
+
|
|
464
|
+
it("getOnboardingStatus returns needsOnboarding true when no users exist", async () => {
|
|
465
|
+
const context = createMockRpcContext({ user: undefined });
|
|
466
|
+
|
|
467
|
+
mockDb.select.mockImplementationOnce(() => ({
|
|
468
|
+
from: mock(() => createChain([])),
|
|
469
|
+
}));
|
|
470
|
+
|
|
471
|
+
const result = await call(router.getOnboardingStatus, undefined, {
|
|
472
|
+
context,
|
|
473
|
+
});
|
|
474
|
+
expect(result.needsOnboarding).toBe(true);
|
|
475
|
+
});
|
|
476
|
+
|
|
477
|
+
it("getOnboardingStatus returns needsOnboarding false when users exist", async () => {
|
|
478
|
+
const context = createMockRpcContext({ user: undefined });
|
|
479
|
+
|
|
480
|
+
mockDb.select.mockImplementationOnce(() => ({
|
|
481
|
+
from: mock(() => createChain([{ id: "existing-user" }])),
|
|
482
|
+
}));
|
|
483
|
+
|
|
484
|
+
const result = await call(router.getOnboardingStatus, undefined, {
|
|
485
|
+
context,
|
|
486
|
+
});
|
|
487
|
+
expect(result.needsOnboarding).toBe(false);
|
|
488
|
+
});
|
|
489
|
+
|
|
490
|
+
it("completeOnboarding creates first admin user", async () => {
|
|
491
|
+
const context = createMockRpcContext({ user: undefined });
|
|
492
|
+
|
|
493
|
+
// No existing users
|
|
494
|
+
mockDb.select.mockImplementationOnce(() => ({
|
|
495
|
+
from: mock(() => createChain([])),
|
|
496
|
+
}));
|
|
497
|
+
|
|
498
|
+
const result = await call(
|
|
499
|
+
router.completeOnboarding,
|
|
500
|
+
{
|
|
501
|
+
name: "Admin User",
|
|
502
|
+
email: "admin@example.com",
|
|
503
|
+
password: "ValidPass123",
|
|
504
|
+
},
|
|
505
|
+
{ context },
|
|
506
|
+
);
|
|
507
|
+
|
|
508
|
+
expect(result.success).toBe(true);
|
|
509
|
+
expect(mockDb.transaction).toHaveBeenCalled();
|
|
510
|
+
});
|
|
511
|
+
|
|
512
|
+
it("completeOnboarding rejects when users already exist", async () => {
|
|
513
|
+
const context = createMockRpcContext({ user: undefined });
|
|
514
|
+
|
|
515
|
+
// Existing user
|
|
516
|
+
mockDb.select.mockImplementationOnce(() => ({
|
|
517
|
+
from: mock(() => createChain([{ id: "existing-user" }])),
|
|
518
|
+
}));
|
|
519
|
+
|
|
520
|
+
expect(
|
|
521
|
+
call(
|
|
522
|
+
router.completeOnboarding,
|
|
523
|
+
{
|
|
524
|
+
name: "Admin User",
|
|
525
|
+
email: "admin@example.com",
|
|
526
|
+
password: "ValidPass123",
|
|
527
|
+
},
|
|
528
|
+
{ context },
|
|
529
|
+
),
|
|
530
|
+
).rejects.toThrow("already been completed");
|
|
531
|
+
});
|
|
532
|
+
|
|
533
|
+
it("completeOnboarding rejects weak password", async () => {
|
|
534
|
+
const context = createMockRpcContext({ user: undefined });
|
|
535
|
+
|
|
536
|
+
// No existing users
|
|
537
|
+
mockDb.select.mockImplementationOnce(() => ({
|
|
538
|
+
from: mock(() => createChain([])),
|
|
539
|
+
}));
|
|
540
|
+
|
|
541
|
+
expect(
|
|
542
|
+
call(
|
|
543
|
+
router.completeOnboarding,
|
|
544
|
+
{
|
|
545
|
+
name: "Admin User",
|
|
546
|
+
email: "admin@example.com",
|
|
547
|
+
password: "weak",
|
|
548
|
+
},
|
|
549
|
+
{ context },
|
|
550
|
+
),
|
|
551
|
+
).rejects.toThrow();
|
|
552
|
+
});
|
|
553
|
+
|
|
554
|
+
// ==========================================================================
|
|
555
|
+
// USER PROFILE ENDPOINT TESTS
|
|
556
|
+
// ==========================================================================
|
|
557
|
+
|
|
558
|
+
it("getCurrentUserProfile returns profile with credential account flag", async () => {
|
|
559
|
+
const context = createMockRpcContext({ user: mockUser });
|
|
560
|
+
|
|
561
|
+
// Mock user data
|
|
562
|
+
mockDb.select.mockImplementationOnce(() => ({
|
|
563
|
+
from: mock(() =>
|
|
564
|
+
createChain([
|
|
565
|
+
{ id: "test-user", name: "Test", email: "test@test.com" },
|
|
566
|
+
]),
|
|
567
|
+
),
|
|
568
|
+
}));
|
|
569
|
+
// Mock credential account check
|
|
570
|
+
mockDb.select.mockImplementationOnce(() => ({
|
|
571
|
+
from: mock(() => createChain([{ providerId: "credential" }])),
|
|
572
|
+
}));
|
|
573
|
+
|
|
574
|
+
const result = await call(router.getCurrentUserProfile, undefined, {
|
|
575
|
+
context,
|
|
576
|
+
});
|
|
577
|
+
expect(result.id).toBe("test-user");
|
|
578
|
+
expect(result.hasCredentialAccount).toBe(true);
|
|
579
|
+
});
|
|
580
|
+
|
|
581
|
+
it("getCurrentUserProfile returns false for OAuth-only users", async () => {
|
|
582
|
+
const context = createMockRpcContext({ user: mockUser });
|
|
583
|
+
|
|
584
|
+
mockDb.select.mockImplementationOnce(() => ({
|
|
585
|
+
from: mock(() =>
|
|
586
|
+
createChain([
|
|
587
|
+
{ id: "test-user", name: "Test", email: "test@test.com" },
|
|
588
|
+
]),
|
|
589
|
+
),
|
|
590
|
+
}));
|
|
591
|
+
mockDb.select.mockImplementationOnce(() => ({
|
|
592
|
+
from: mock(() => createChain([])), // No credential account
|
|
593
|
+
}));
|
|
594
|
+
|
|
595
|
+
const result = await call(router.getCurrentUserProfile, undefined, {
|
|
596
|
+
context,
|
|
597
|
+
});
|
|
598
|
+
expect(result.hasCredentialAccount).toBe(false);
|
|
599
|
+
});
|
|
600
|
+
|
|
601
|
+
it("updateCurrentUser updates name for any user", async () => {
|
|
602
|
+
const context = createMockRpcContext({ user: mockUser });
|
|
603
|
+
|
|
604
|
+
mockDb.update = mock(() => ({
|
|
605
|
+
set: mock(() => ({
|
|
606
|
+
where: mock(() => Promise.resolve()),
|
|
607
|
+
})),
|
|
608
|
+
}));
|
|
609
|
+
|
|
610
|
+
await call(router.updateCurrentUser, { name: "New Name" }, { context });
|
|
611
|
+
|
|
612
|
+
expect(mockDb.update).toHaveBeenCalled();
|
|
613
|
+
});
|
|
614
|
+
|
|
615
|
+
it("updateCurrentUser allows email update for credential users", async () => {
|
|
616
|
+
const context = createMockRpcContext({ user: mockUser });
|
|
617
|
+
|
|
618
|
+
// Has credential account
|
|
619
|
+
mockDb.select.mockImplementationOnce(() => ({
|
|
620
|
+
from: mock(() => createChain([{ providerId: "credential" }])),
|
|
621
|
+
}));
|
|
622
|
+
// No duplicate email
|
|
623
|
+
mockDb.select.mockImplementationOnce(() => ({
|
|
624
|
+
from: mock(() => createChain([])),
|
|
625
|
+
}));
|
|
626
|
+
|
|
627
|
+
mockDb.update = mock(() => ({
|
|
628
|
+
set: mock(() => ({
|
|
629
|
+
where: mock(() => Promise.resolve()),
|
|
630
|
+
})),
|
|
631
|
+
}));
|
|
632
|
+
|
|
633
|
+
await call(
|
|
634
|
+
router.updateCurrentUser,
|
|
635
|
+
{ email: "new@example.com" },
|
|
636
|
+
{ context },
|
|
637
|
+
);
|
|
638
|
+
|
|
639
|
+
expect(mockDb.update).toHaveBeenCalled();
|
|
640
|
+
});
|
|
641
|
+
|
|
642
|
+
it("updateCurrentUser rejects email update for OAuth users", async () => {
|
|
643
|
+
const context = createMockRpcContext({ user: mockUser });
|
|
644
|
+
|
|
645
|
+
// No credential account
|
|
646
|
+
mockDb.select.mockImplementationOnce(() => ({
|
|
647
|
+
from: mock(() => createChain([])),
|
|
648
|
+
}));
|
|
649
|
+
|
|
650
|
+
expect(
|
|
651
|
+
call(router.updateCurrentUser, { email: "new@example.com" }, { context }),
|
|
652
|
+
).rejects.toThrow("credential-based accounts");
|
|
653
|
+
});
|
|
654
|
+
|
|
655
|
+
// ==========================================================================
|
|
656
|
+
// ROLE CRUD ENDPOINT TESTS
|
|
657
|
+
// ==========================================================================
|
|
658
|
+
|
|
659
|
+
it("createRole creates role with access rules", async () => {
|
|
660
|
+
const context = createMockRpcContext({ user: mockUser });
|
|
661
|
+
|
|
662
|
+
const result = await call(
|
|
663
|
+
router.createRole,
|
|
664
|
+
{
|
|
665
|
+
name: "Custom Role",
|
|
666
|
+
description: "A custom role",
|
|
667
|
+
accessRules: ["auth-backend.users.read"],
|
|
668
|
+
},
|
|
669
|
+
{ context },
|
|
670
|
+
);
|
|
671
|
+
|
|
672
|
+
expect(mockDb.transaction).toHaveBeenCalled();
|
|
673
|
+
});
|
|
674
|
+
|
|
675
|
+
it("deleteRole prevents deleting system roles", async () => {
|
|
676
|
+
// Use a user without admin role to properly test system role protection
|
|
677
|
+
const nonAdminUser = {
|
|
678
|
+
type: "user" as const,
|
|
679
|
+
id: "non-admin-user",
|
|
680
|
+
accessRules: ["*"],
|
|
681
|
+
roles: ["users"],
|
|
682
|
+
} as ReturnType<typeof createMockRpcContext>["user"];
|
|
683
|
+
const context = createMockRpcContext({ user: nonAdminUser });
|
|
684
|
+
|
|
685
|
+
mockDb.select.mockImplementationOnce(() => ({
|
|
686
|
+
from: mock(() => createChain([{ id: ADMIN_ROLE_ID, isSystem: true }])),
|
|
687
|
+
}));
|
|
688
|
+
|
|
689
|
+
expect(call(router.deleteRole, ADMIN_ROLE_ID, { context })).rejects.toThrow(
|
|
690
|
+
"system role",
|
|
691
|
+
);
|
|
692
|
+
});
|
|
693
|
+
|
|
694
|
+
it("deleteRole prevents deleting own roles", async () => {
|
|
695
|
+
const userWithRole = {
|
|
696
|
+
...mockUser,
|
|
697
|
+
roles: ["custom-role"],
|
|
698
|
+
};
|
|
699
|
+
const context = createMockRpcContext({ user: userWithRole });
|
|
700
|
+
|
|
701
|
+
expect(call(router.deleteRole, "custom-role", { context })).rejects.toThrow(
|
|
702
|
+
"currently have",
|
|
703
|
+
);
|
|
704
|
+
});
|
|
705
|
+
|
|
706
|
+
it("getAccessRules returns registry access rules", async () => {
|
|
707
|
+
const context = createMockRpcContext({ user: mockUser });
|
|
708
|
+
|
|
709
|
+
const result = await call(router.getAccessRules, undefined, { context });
|
|
710
|
+
expect(
|
|
711
|
+
result.some((r: { id: string }) => r.id === "auth-backend.users.read"),
|
|
712
|
+
).toBe(true);
|
|
713
|
+
});
|
|
714
|
+
|
|
715
|
+
it("updateUserRoles prevents assigning anonymous role", async () => {
|
|
716
|
+
const context = createMockRpcContext({ user: mockUser });
|
|
717
|
+
|
|
718
|
+
expect(
|
|
719
|
+
call(
|
|
720
|
+
router.updateUserRoles,
|
|
721
|
+
{ userId: "other-user", roles: [ANONYMOUS_ROLE_ID] },
|
|
722
|
+
{ context },
|
|
723
|
+
),
|
|
724
|
+
).rejects.toThrow("anonymous");
|
|
725
|
+
});
|
|
726
|
+
|
|
727
|
+
// ==========================================================================
|
|
728
|
+
// APPLICATION MANAGEMENT ENDPOINT TESTS
|
|
729
|
+
// ==========================================================================
|
|
730
|
+
|
|
731
|
+
it("getApplications returns applications with roles", async () => {
|
|
732
|
+
const context = createMockRpcContext({ user: mockUser });
|
|
733
|
+
|
|
734
|
+
mockDb.select.mockImplementationOnce(() => ({
|
|
735
|
+
from: mock(() =>
|
|
736
|
+
createChain([
|
|
737
|
+
{
|
|
738
|
+
id: "app-1",
|
|
739
|
+
name: "Test App",
|
|
740
|
+
description: "Test description",
|
|
741
|
+
createdById: "user-1",
|
|
742
|
+
createdAt: new Date(),
|
|
743
|
+
lastUsedAt: null,
|
|
744
|
+
},
|
|
745
|
+
]),
|
|
746
|
+
),
|
|
747
|
+
}));
|
|
748
|
+
mockDb.select.mockImplementationOnce(() => ({
|
|
749
|
+
from: mock(() =>
|
|
750
|
+
createChain([{ applicationId: "app-1", roleId: APPLICATIONS_ROLE_ID }]),
|
|
751
|
+
),
|
|
752
|
+
}));
|
|
753
|
+
|
|
754
|
+
const result = await call(router.getApplications, undefined, { context });
|
|
755
|
+
expect(result).toHaveLength(1);
|
|
756
|
+
expect(result[0].roles).toContain(APPLICATIONS_ROLE_ID);
|
|
757
|
+
});
|
|
758
|
+
|
|
759
|
+
it("createApplication creates application with secret", async () => {
|
|
760
|
+
const context = createMockRpcContext({ user: mockUser });
|
|
761
|
+
|
|
762
|
+
const result = await call(
|
|
763
|
+
router.createApplication,
|
|
764
|
+
{ name: "New App", description: "Test application" },
|
|
765
|
+
{ context },
|
|
766
|
+
);
|
|
767
|
+
|
|
768
|
+
expect(result.application.name).toBe("New App");
|
|
769
|
+
expect(result.secret).toMatch(/^ck_/); // Secret has proper prefix
|
|
770
|
+
expect(mockDb.transaction).toHaveBeenCalled();
|
|
771
|
+
});
|
|
772
|
+
|
|
773
|
+
it("deleteApplication handles not found", async () => {
|
|
774
|
+
const context = createMockRpcContext({ user: mockUser });
|
|
775
|
+
|
|
776
|
+
mockDb.select.mockImplementationOnce(() => ({
|
|
777
|
+
from: mock(() => createChain([])), // No application found
|
|
778
|
+
}));
|
|
779
|
+
|
|
780
|
+
await expect(
|
|
781
|
+
call(router.deleteApplication, "non-existent-id", { context }),
|
|
782
|
+
).rejects.toThrow("not found");
|
|
783
|
+
});
|
|
784
|
+
|
|
785
|
+
it("regenerateApplicationSecret returns new secret", async () => {
|
|
786
|
+
const context = createMockRpcContext({ user: mockUser });
|
|
787
|
+
|
|
788
|
+
mockDb.select.mockImplementationOnce(() => ({
|
|
789
|
+
from: mock(() => createChain([{ id: "app-1" }])),
|
|
790
|
+
}));
|
|
791
|
+
|
|
792
|
+
mockDb.update = mock(() => ({
|
|
793
|
+
set: mock(() => ({
|
|
794
|
+
where: mock(() => Promise.resolve()),
|
|
795
|
+
})),
|
|
796
|
+
}));
|
|
797
|
+
|
|
798
|
+
const result = await call(router.regenerateApplicationSecret, "app-1", {
|
|
799
|
+
context,
|
|
800
|
+
});
|
|
801
|
+
|
|
802
|
+
expect(result.secret).toMatch(/^ck_app-1_/);
|
|
803
|
+
expect(mockDb.update).toHaveBeenCalled();
|
|
804
|
+
});
|
|
805
|
+
|
|
806
|
+
// ==========================================================================
|
|
807
|
+
// PUBLIC ENDPOINT TESTS
|
|
808
|
+
// ==========================================================================
|
|
809
|
+
|
|
810
|
+
it("getEnabledStrategies returns only enabled strategies", async () => {
|
|
811
|
+
const context = createMockRpcContext({ user: undefined });
|
|
812
|
+
|
|
813
|
+
// Mock credential enabled (default)
|
|
814
|
+
mockConfigService.get.mockResolvedValueOnce(undefined); // Uses default
|
|
815
|
+
|
|
816
|
+
const result = await call(router.getEnabledStrategies, undefined, {
|
|
817
|
+
context,
|
|
818
|
+
});
|
|
819
|
+
|
|
820
|
+
// Should contain the credential strategy
|
|
821
|
+
expect(result.some((s: { id: string }) => s.id === "credential")).toBe(
|
|
822
|
+
true,
|
|
823
|
+
);
|
|
824
|
+
});
|
|
440
825
|
});
|