@classic-homes/auth 0.1.23 → 0.1.25
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/README.md +486 -81
- package/dist/config-C-iBNu07.d.ts +86 -0
- package/dist/core/index.d.ts +72 -94
- package/dist/core/index.js +822 -430
- package/dist/index.d.ts +3 -2
- package/dist/index.js +867 -718
- package/dist/svelte/index.d.ts +12 -1
- package/dist/svelte/index.js +840 -193
- package/dist/testing/index.d.ts +1082 -0
- package/dist/testing/index.js +2033 -0
- package/dist/{types-exFUQyBX.d.ts → types-DGN45Uih.d.ts} +9 -1
- package/package.json +5 -1
|
@@ -0,0 +1,2033 @@
|
|
|
1
|
+
// src/testing/fixtures/users.ts
|
|
2
|
+
var mockUser = {
|
|
3
|
+
id: "user-123",
|
|
4
|
+
username: "testuser",
|
|
5
|
+
email: "test@example.com",
|
|
6
|
+
firstName: "Test",
|
|
7
|
+
lastName: "User",
|
|
8
|
+
phone: "+1234567890",
|
|
9
|
+
role: "user",
|
|
10
|
+
roles: ["user"],
|
|
11
|
+
permissions: ["read:profile", "write:profile"],
|
|
12
|
+
isActive: true,
|
|
13
|
+
emailVerified: true,
|
|
14
|
+
authMethod: "password",
|
|
15
|
+
createdAt: "2024-01-01T00:00:00.000Z",
|
|
16
|
+
lastLoginAt: "2024-06-01T12:00:00.000Z"
|
|
17
|
+
};
|
|
18
|
+
var mockAdminUser = {
|
|
19
|
+
id: "admin-123",
|
|
20
|
+
username: "adminuser",
|
|
21
|
+
email: "admin@example.com",
|
|
22
|
+
firstName: "Admin",
|
|
23
|
+
lastName: "User",
|
|
24
|
+
phone: "+1234567891",
|
|
25
|
+
role: "admin",
|
|
26
|
+
roles: ["admin", "user"],
|
|
27
|
+
permissions: [
|
|
28
|
+
"read:profile",
|
|
29
|
+
"write:profile",
|
|
30
|
+
"read:users",
|
|
31
|
+
"write:users",
|
|
32
|
+
"delete:users",
|
|
33
|
+
"read:admin",
|
|
34
|
+
"write:admin",
|
|
35
|
+
"manage:system"
|
|
36
|
+
],
|
|
37
|
+
isActive: true,
|
|
38
|
+
emailVerified: true,
|
|
39
|
+
authMethod: "password",
|
|
40
|
+
createdAt: "2024-01-01T00:00:00.000Z",
|
|
41
|
+
lastLoginAt: "2024-06-01T12:00:00.000Z"
|
|
42
|
+
};
|
|
43
|
+
var mockSSOUser = {
|
|
44
|
+
id: "sso-123",
|
|
45
|
+
username: "ssouser",
|
|
46
|
+
email: "sso@example.com",
|
|
47
|
+
firstName: "SSO",
|
|
48
|
+
lastName: "User",
|
|
49
|
+
role: "user",
|
|
50
|
+
roles: ["user"],
|
|
51
|
+
permissions: ["read:profile", "write:profile"],
|
|
52
|
+
isActive: true,
|
|
53
|
+
emailVerified: true,
|
|
54
|
+
authMethod: "oauth",
|
|
55
|
+
ssoProfileUrl: "https://sso.example.com/profile/sso-123",
|
|
56
|
+
createdAt: "2024-01-01T00:00:00.000Z",
|
|
57
|
+
lastLoginAt: "2024-06-01T12:00:00.000Z"
|
|
58
|
+
};
|
|
59
|
+
var mockMFAUser = {
|
|
60
|
+
id: "mfa-123",
|
|
61
|
+
username: "mfauser",
|
|
62
|
+
email: "mfa@example.com",
|
|
63
|
+
firstName: "MFA",
|
|
64
|
+
lastName: "User",
|
|
65
|
+
role: "user",
|
|
66
|
+
roles: ["user"],
|
|
67
|
+
permissions: ["read:profile", "write:profile"],
|
|
68
|
+
isActive: true,
|
|
69
|
+
emailVerified: true,
|
|
70
|
+
authMethod: "password",
|
|
71
|
+
createdAt: "2024-01-01T00:00:00.000Z",
|
|
72
|
+
lastLoginAt: "2024-06-01T12:00:00.000Z"
|
|
73
|
+
};
|
|
74
|
+
var mockUnverifiedUser = {
|
|
75
|
+
id: "unverified-123",
|
|
76
|
+
username: "unverifieduser",
|
|
77
|
+
email: "unverified@example.com",
|
|
78
|
+
firstName: "Unverified",
|
|
79
|
+
lastName: "User",
|
|
80
|
+
role: "user",
|
|
81
|
+
roles: ["user"],
|
|
82
|
+
permissions: [],
|
|
83
|
+
isActive: true,
|
|
84
|
+
emailVerified: false,
|
|
85
|
+
authMethod: "password",
|
|
86
|
+
createdAt: "2024-01-01T00:00:00.000Z"
|
|
87
|
+
};
|
|
88
|
+
var mockInactiveUser = {
|
|
89
|
+
id: "inactive-123",
|
|
90
|
+
username: "inactiveuser",
|
|
91
|
+
email: "inactive@example.com",
|
|
92
|
+
firstName: "Inactive",
|
|
93
|
+
lastName: "User",
|
|
94
|
+
role: "user",
|
|
95
|
+
roles: ["user"],
|
|
96
|
+
permissions: [],
|
|
97
|
+
isActive: false,
|
|
98
|
+
emailVerified: true,
|
|
99
|
+
authMethod: "password",
|
|
100
|
+
createdAt: "2024-01-01T00:00:00.000Z"
|
|
101
|
+
};
|
|
102
|
+
function createMockUser(overrides = {}) {
|
|
103
|
+
return {
|
|
104
|
+
...mockUser,
|
|
105
|
+
id: overrides.id ?? `user-${Date.now()}`,
|
|
106
|
+
...overrides
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
function createMockUserWithRoles(roles, permissions, overrides = {}) {
|
|
110
|
+
return createMockUser({
|
|
111
|
+
role: roles[0] ?? "user",
|
|
112
|
+
roles,
|
|
113
|
+
permissions,
|
|
114
|
+
...overrides
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
function createMockUserWithAuthMethod(authMethod, overrides = {}) {
|
|
118
|
+
const baseOverrides = { authMethod };
|
|
119
|
+
if (authMethod === "oauth" || authMethod === "both") {
|
|
120
|
+
baseOverrides.ssoProfileUrl = `https://sso.example.com/profile/${overrides.id ?? "user-123"}`;
|
|
121
|
+
}
|
|
122
|
+
return createMockUser({
|
|
123
|
+
...baseOverrides,
|
|
124
|
+
...overrides
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// src/testing/fixtures/tokens.ts
|
|
129
|
+
var BASE64_URL_ENCODE = (str) => {
|
|
130
|
+
return btoa(str).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
131
|
+
};
|
|
132
|
+
var createFakeJWT = (payload, expiresIn = 3600) => {
|
|
133
|
+
const header = { alg: "HS256", typ: "JWT" };
|
|
134
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
135
|
+
const fullPayload = {
|
|
136
|
+
iat: now,
|
|
137
|
+
exp: now + expiresIn,
|
|
138
|
+
...payload
|
|
139
|
+
};
|
|
140
|
+
const headerB64 = BASE64_URL_ENCODE(JSON.stringify(header));
|
|
141
|
+
const payloadB64 = BASE64_URL_ENCODE(JSON.stringify(fullPayload));
|
|
142
|
+
const signature = BASE64_URL_ENCODE("fake-signature");
|
|
143
|
+
return `${headerB64}.${payloadB64}.${signature}`;
|
|
144
|
+
};
|
|
145
|
+
var mockAccessToken = createFakeJWT(
|
|
146
|
+
{
|
|
147
|
+
sub: "user-123",
|
|
148
|
+
username: "testuser",
|
|
149
|
+
email: "test@example.com",
|
|
150
|
+
roles: ["user"],
|
|
151
|
+
permissions: ["read:profile", "write:profile"],
|
|
152
|
+
type: "access"
|
|
153
|
+
},
|
|
154
|
+
3600
|
|
155
|
+
);
|
|
156
|
+
var mockRefreshToken = createFakeJWT(
|
|
157
|
+
{
|
|
158
|
+
sub: "user-123",
|
|
159
|
+
type: "refresh"
|
|
160
|
+
},
|
|
161
|
+
604800
|
|
162
|
+
);
|
|
163
|
+
var mockExpiredToken = createFakeJWT(
|
|
164
|
+
{
|
|
165
|
+
sub: "user-123",
|
|
166
|
+
username: "testuser",
|
|
167
|
+
email: "test@example.com",
|
|
168
|
+
roles: ["user"],
|
|
169
|
+
permissions: ["read:profile", "write:profile"],
|
|
170
|
+
type: "access"
|
|
171
|
+
},
|
|
172
|
+
-3600
|
|
173
|
+
// Already expired
|
|
174
|
+
);
|
|
175
|
+
var mockAdminToken = createFakeJWT(
|
|
176
|
+
{
|
|
177
|
+
sub: "admin-123",
|
|
178
|
+
username: "adminuser",
|
|
179
|
+
email: "admin@example.com",
|
|
180
|
+
roles: ["admin", "user"],
|
|
181
|
+
permissions: [
|
|
182
|
+
"read:profile",
|
|
183
|
+
"write:profile",
|
|
184
|
+
"read:users",
|
|
185
|
+
"write:users",
|
|
186
|
+
"delete:users",
|
|
187
|
+
"read:admin",
|
|
188
|
+
"write:admin",
|
|
189
|
+
"manage:system"
|
|
190
|
+
],
|
|
191
|
+
type: "access"
|
|
192
|
+
},
|
|
193
|
+
3600
|
|
194
|
+
);
|
|
195
|
+
var mockMFAToken = createFakeJWT(
|
|
196
|
+
{
|
|
197
|
+
sub: "mfa-123",
|
|
198
|
+
type: "mfa_challenge",
|
|
199
|
+
username: "mfauser"
|
|
200
|
+
},
|
|
201
|
+
300
|
|
202
|
+
// 5 minutes
|
|
203
|
+
);
|
|
204
|
+
var mockSessionToken = createFakeJWT(
|
|
205
|
+
{
|
|
206
|
+
sub: "user-123",
|
|
207
|
+
type: "session",
|
|
208
|
+
deviceId: "device-123",
|
|
209
|
+
trusted: true
|
|
210
|
+
},
|
|
211
|
+
86400
|
|
212
|
+
// 24 hours
|
|
213
|
+
);
|
|
214
|
+
function createMockJWT(payload = {}, expiresIn = 3600) {
|
|
215
|
+
const defaultPayload = {
|
|
216
|
+
sub: "user-123",
|
|
217
|
+
type: "access",
|
|
218
|
+
...payload
|
|
219
|
+
};
|
|
220
|
+
return createFakeJWT(defaultPayload, expiresIn);
|
|
221
|
+
}
|
|
222
|
+
function createExpiredMockJWT(payload = {}, expiredSecondsAgo = 3600) {
|
|
223
|
+
return createMockJWT(payload, -expiredSecondsAgo);
|
|
224
|
+
}
|
|
225
|
+
function createMockTokenPair(userId = "user-123", options = {}) {
|
|
226
|
+
const accessToken = createMockJWT(
|
|
227
|
+
{
|
|
228
|
+
sub: userId,
|
|
229
|
+
type: "access",
|
|
230
|
+
roles: options.roles ?? ["user"],
|
|
231
|
+
permissions: options.permissions ?? ["read:profile", "write:profile"]
|
|
232
|
+
},
|
|
233
|
+
options.accessExpiresIn ?? 3600
|
|
234
|
+
);
|
|
235
|
+
const refreshToken = createMockJWT(
|
|
236
|
+
{
|
|
237
|
+
sub: userId,
|
|
238
|
+
type: "refresh"
|
|
239
|
+
},
|
|
240
|
+
options.refreshExpiresIn ?? 604800
|
|
241
|
+
);
|
|
242
|
+
return { accessToken, refreshToken };
|
|
243
|
+
}
|
|
244
|
+
function createMockMFAToken(userId = "mfa-123", expiresIn = 300) {
|
|
245
|
+
return createMockJWT(
|
|
246
|
+
{
|
|
247
|
+
sub: userId,
|
|
248
|
+
type: "mfa_challenge"
|
|
249
|
+
},
|
|
250
|
+
expiresIn
|
|
251
|
+
);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// src/testing/fixtures/responses.ts
|
|
255
|
+
var mockLoginSuccess = {
|
|
256
|
+
accessToken: mockAccessToken,
|
|
257
|
+
refreshToken: mockRefreshToken,
|
|
258
|
+
sessionToken: mockSessionToken,
|
|
259
|
+
user: mockUser,
|
|
260
|
+
expiresIn: 3600
|
|
261
|
+
};
|
|
262
|
+
var mockMFARequired = {
|
|
263
|
+
accessToken: "",
|
|
264
|
+
refreshToken: "",
|
|
265
|
+
user: mockMFAUser,
|
|
266
|
+
expiresIn: 0,
|
|
267
|
+
requiresMFA: true,
|
|
268
|
+
mfaToken: mockMFAToken,
|
|
269
|
+
availableMethods: ["totp", "backup"]
|
|
270
|
+
};
|
|
271
|
+
var mockMFARequiredLegacy = {
|
|
272
|
+
accessToken: "",
|
|
273
|
+
refreshToken: "",
|
|
274
|
+
user: mockMFAUser,
|
|
275
|
+
expiresIn: 0,
|
|
276
|
+
mfaRequired: true,
|
|
277
|
+
mfaChallengeToken: mockMFAToken
|
|
278
|
+
};
|
|
279
|
+
var mockAdminLoginSuccess = {
|
|
280
|
+
...mockLoginSuccess,
|
|
281
|
+
user: mockAdminUser,
|
|
282
|
+
...createMockTokenPair("admin-123", {
|
|
283
|
+
roles: ["admin", "user"],
|
|
284
|
+
permissions: mockAdminUser.permissions
|
|
285
|
+
})
|
|
286
|
+
};
|
|
287
|
+
var mockSSOLoginSuccess = {
|
|
288
|
+
...mockLoginSuccess,
|
|
289
|
+
user: mockSSOUser
|
|
290
|
+
};
|
|
291
|
+
var mockLogoutSuccess = {
|
|
292
|
+
success: true,
|
|
293
|
+
message: "Logged out successfully"
|
|
294
|
+
};
|
|
295
|
+
var mockSSOLogoutResponse = {
|
|
296
|
+
success: true,
|
|
297
|
+
message: "Logged out successfully",
|
|
298
|
+
ssoLogout: true,
|
|
299
|
+
logoutUrl: "https://sso.example.com/logout?redirect=https://app.example.com"
|
|
300
|
+
};
|
|
301
|
+
var mockRegisterSuccess = {
|
|
302
|
+
message: "Registration successful",
|
|
303
|
+
user: mockUser,
|
|
304
|
+
requiresEmailVerification: false
|
|
305
|
+
};
|
|
306
|
+
var mockRegisterRequiresVerification = {
|
|
307
|
+
message: "Registration successful. Please verify your email.",
|
|
308
|
+
user: mockUnverifiedUser,
|
|
309
|
+
requiresEmailVerification: true
|
|
310
|
+
};
|
|
311
|
+
var mockMFASetup = {
|
|
312
|
+
qrCodeUrl: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==",
|
|
313
|
+
manualEntryKey: "JBSWY3DPEHPK3PXP",
|
|
314
|
+
backupCodes: [
|
|
315
|
+
"AAAA-BBBB-CCCC",
|
|
316
|
+
"DDDD-EEEE-FFFF",
|
|
317
|
+
"GGGG-HHHH-IIII",
|
|
318
|
+
"JJJJ-KKKK-LLLL",
|
|
319
|
+
"MMMM-NNNN-OOOO",
|
|
320
|
+
"PPPP-QQQQ-RRRR",
|
|
321
|
+
"SSSS-TTTT-UUUU",
|
|
322
|
+
"VVVV-WWWW-XXXX"
|
|
323
|
+
],
|
|
324
|
+
instructions: {
|
|
325
|
+
step1: "Download an authenticator app (e.g., Google Authenticator, Authy)",
|
|
326
|
+
step2: "Scan the QR code or manually enter the key",
|
|
327
|
+
step3: "Enter the 6-digit code from your authenticator app",
|
|
328
|
+
step4: "Store your backup codes in a safe place"
|
|
329
|
+
}
|
|
330
|
+
};
|
|
331
|
+
var mockMFAStatusEnabled = {
|
|
332
|
+
enabled: true,
|
|
333
|
+
methods: ["totp"]
|
|
334
|
+
};
|
|
335
|
+
var mockMFAStatusDisabled = {
|
|
336
|
+
enabled: false,
|
|
337
|
+
methods: []
|
|
338
|
+
};
|
|
339
|
+
var mockCurrentSession = {
|
|
340
|
+
id: "session-current",
|
|
341
|
+
deviceName: "Chrome on macOS",
|
|
342
|
+
browser: "Chrome 120",
|
|
343
|
+
location: "San Francisco, CA",
|
|
344
|
+
ipAddress: "192.168.1.1",
|
|
345
|
+
lastActivity: (/* @__PURE__ */ new Date()).toISOString(),
|
|
346
|
+
isCurrent: true,
|
|
347
|
+
isTrusted: true,
|
|
348
|
+
deviceFingerprint: "fp-abc123",
|
|
349
|
+
trustedDeviceId: "device-123",
|
|
350
|
+
createdAt: new Date(Date.now() - 864e5).toISOString(),
|
|
351
|
+
expiresAt: new Date(Date.now() + 6048e5).toISOString()
|
|
352
|
+
};
|
|
353
|
+
var mockOtherSession = {
|
|
354
|
+
id: "session-other",
|
|
355
|
+
deviceName: "Firefox on Windows",
|
|
356
|
+
browser: "Firefox 121",
|
|
357
|
+
location: "New York, NY",
|
|
358
|
+
ipAddress: "10.0.0.1",
|
|
359
|
+
lastActivity: new Date(Date.now() - 36e5).toISOString(),
|
|
360
|
+
isCurrent: false,
|
|
361
|
+
isTrusted: false,
|
|
362
|
+
deviceFingerprint: "fp-def456",
|
|
363
|
+
createdAt: new Date(Date.now() - 1728e5).toISOString(),
|
|
364
|
+
expiresAt: new Date(Date.now() + 5184e5).toISOString()
|
|
365
|
+
};
|
|
366
|
+
var mockSessions = [mockCurrentSession, mockOtherSession];
|
|
367
|
+
var mockTrustedDevice = {
|
|
368
|
+
id: "device-trusted",
|
|
369
|
+
deviceFingerprint: "fp-trusted-123",
|
|
370
|
+
deviceName: "MacBook Pro",
|
|
371
|
+
userAgent: "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)",
|
|
372
|
+
ipAddress: "192.168.1.1",
|
|
373
|
+
trusted: true,
|
|
374
|
+
status: "active",
|
|
375
|
+
lastSeen: (/* @__PURE__ */ new Date()).toISOString(),
|
|
376
|
+
createdAt: new Date(Date.now() - 2592e6).toISOString()
|
|
377
|
+
};
|
|
378
|
+
var mockUntrustedDevice = {
|
|
379
|
+
id: "device-untrusted",
|
|
380
|
+
deviceFingerprint: "fp-untrusted-456",
|
|
381
|
+
deviceName: "Unknown Device",
|
|
382
|
+
userAgent: "Mozilla/5.0 (Windows NT 10.0; Win64; x64)",
|
|
383
|
+
ipAddress: "10.0.0.100",
|
|
384
|
+
trusted: false,
|
|
385
|
+
status: "active",
|
|
386
|
+
lastSeen: new Date(Date.now() - 864e5).toISOString(),
|
|
387
|
+
createdAt: new Date(Date.now() - 6048e5).toISOString()
|
|
388
|
+
};
|
|
389
|
+
var mockDevices = [mockTrustedDevice, mockUntrustedDevice];
|
|
390
|
+
var mockApiKey = {
|
|
391
|
+
id: "key-123",
|
|
392
|
+
name: "Production API Key",
|
|
393
|
+
description: "Main API key for production environment",
|
|
394
|
+
keyPreview: "sk_live_xxxx...xxxx",
|
|
395
|
+
permissions: ["read:data", "write:data"],
|
|
396
|
+
role: "api",
|
|
397
|
+
isActive: true,
|
|
398
|
+
expiresAt: new Date(Date.now() + 31536e6).toISOString(),
|
|
399
|
+
lastUsedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
400
|
+
createdAt: new Date(Date.now() - 2592e6).toISOString(),
|
|
401
|
+
usageCount: 1234
|
|
402
|
+
};
|
|
403
|
+
var mockApiKeys = [
|
|
404
|
+
mockApiKey,
|
|
405
|
+
{
|
|
406
|
+
...mockApiKey,
|
|
407
|
+
id: "key-456",
|
|
408
|
+
name: "Development API Key",
|
|
409
|
+
description: "API key for development",
|
|
410
|
+
keyPreview: "sk_test_xxxx...xxxx",
|
|
411
|
+
usageCount: 56
|
|
412
|
+
}
|
|
413
|
+
];
|
|
414
|
+
var mockLinkedAccount = {
|
|
415
|
+
id: "link-123",
|
|
416
|
+
provider: "authentik",
|
|
417
|
+
providerAccountId: "auth-user-123",
|
|
418
|
+
providerEmail: "user@sso.example.com",
|
|
419
|
+
providerUsername: "ssouser",
|
|
420
|
+
providerAvatarUrl: "https://sso.example.com/avatar/123.png",
|
|
421
|
+
isPrimary: true,
|
|
422
|
+
isVerified: true,
|
|
423
|
+
createdAt: new Date(Date.now() - 2592e6).toISOString(),
|
|
424
|
+
lastUsedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
425
|
+
};
|
|
426
|
+
var mockSecurityEventLogin = {
|
|
427
|
+
id: "event-login",
|
|
428
|
+
eventType: "login",
|
|
429
|
+
description: "Successful login from new device",
|
|
430
|
+
severity: "low",
|
|
431
|
+
ipAddress: "192.168.1.1",
|
|
432
|
+
userAgent: "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)",
|
|
433
|
+
location: "San Francisco, CA",
|
|
434
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
435
|
+
};
|
|
436
|
+
var mockSecurityEventPasswordChange = {
|
|
437
|
+
id: "event-password",
|
|
438
|
+
eventType: "password_change",
|
|
439
|
+
description: "Password changed successfully",
|
|
440
|
+
severity: "medium",
|
|
441
|
+
ipAddress: "192.168.1.1",
|
|
442
|
+
createdAt: new Date(Date.now() - 864e5).toISOString()
|
|
443
|
+
};
|
|
444
|
+
var mockSecurityEventSuspicious = {
|
|
445
|
+
id: "event-suspicious",
|
|
446
|
+
eventType: "suspicious_login",
|
|
447
|
+
description: "Login attempt from unusual location",
|
|
448
|
+
severity: "high",
|
|
449
|
+
ipAddress: "203.0.113.50",
|
|
450
|
+
location: "Unknown",
|
|
451
|
+
metadata: {
|
|
452
|
+
blocked: true,
|
|
453
|
+
reason: "Location mismatch"
|
|
454
|
+
},
|
|
455
|
+
createdAt: new Date(Date.now() - 36e5).toISOString()
|
|
456
|
+
};
|
|
457
|
+
var mockSecurityEvents = [
|
|
458
|
+
mockSecurityEventLogin,
|
|
459
|
+
mockSecurityEventPasswordChange,
|
|
460
|
+
mockSecurityEventSuspicious
|
|
461
|
+
];
|
|
462
|
+
var mockUserPreferences = {
|
|
463
|
+
emailNotifications: true,
|
|
464
|
+
securityAlerts: true,
|
|
465
|
+
loginNotifications: true,
|
|
466
|
+
suspiciousActivityAlerts: true,
|
|
467
|
+
passwordChangeNotifications: true,
|
|
468
|
+
mfaChangeNotifications: true,
|
|
469
|
+
accountUpdates: true,
|
|
470
|
+
systemMaintenance: false,
|
|
471
|
+
featureAnnouncements: true,
|
|
472
|
+
newsAndUpdates: false,
|
|
473
|
+
apiKeyExpiration: true,
|
|
474
|
+
apiUsageAlerts: false,
|
|
475
|
+
rateLimit: true,
|
|
476
|
+
dataExportReady: true,
|
|
477
|
+
reportGeneration: true,
|
|
478
|
+
theme: "system",
|
|
479
|
+
language: "en",
|
|
480
|
+
timezone: "America/Los_Angeles",
|
|
481
|
+
dateFormat: "MM/DD/YYYY",
|
|
482
|
+
timeFormat: "12h"
|
|
483
|
+
};
|
|
484
|
+
function createMockLoginSuccess(options = {}) {
|
|
485
|
+
const tokenPair = createMockTokenPair(options.userId ?? mockUser.id);
|
|
486
|
+
return {
|
|
487
|
+
...tokenPair,
|
|
488
|
+
user: options.user ? { ...mockUser, ...options.user } : mockUser,
|
|
489
|
+
expiresIn: options.expiresIn ?? 3600
|
|
490
|
+
};
|
|
491
|
+
}
|
|
492
|
+
function createMockMFARequired(options = {}) {
|
|
493
|
+
return {
|
|
494
|
+
accessToken: "",
|
|
495
|
+
refreshToken: "",
|
|
496
|
+
user: mockMFAUser,
|
|
497
|
+
expiresIn: 0,
|
|
498
|
+
requiresMFA: true,
|
|
499
|
+
mfaToken: options.mfaToken ?? mockMFAToken,
|
|
500
|
+
availableMethods: options.availableMethods ?? ["totp"]
|
|
501
|
+
};
|
|
502
|
+
}
|
|
503
|
+
function createMockSession(overrides = {}) {
|
|
504
|
+
return {
|
|
505
|
+
...mockCurrentSession,
|
|
506
|
+
id: `session-${Date.now()}`,
|
|
507
|
+
...overrides
|
|
508
|
+
};
|
|
509
|
+
}
|
|
510
|
+
function createMockDevice(overrides = {}) {
|
|
511
|
+
return {
|
|
512
|
+
...mockTrustedDevice,
|
|
513
|
+
id: `device-${Date.now()}`,
|
|
514
|
+
...overrides
|
|
515
|
+
};
|
|
516
|
+
}
|
|
517
|
+
function createMockSecurityEvent(overrides = {}) {
|
|
518
|
+
return {
|
|
519
|
+
...mockSecurityEventLogin,
|
|
520
|
+
id: `event-${Date.now()}`,
|
|
521
|
+
...overrides
|
|
522
|
+
};
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
// src/testing/mocks/storage.ts
|
|
526
|
+
var MockStorageAdapter = class {
|
|
527
|
+
constructor(options = {}) {
|
|
528
|
+
this.data = /* @__PURE__ */ new Map();
|
|
529
|
+
this.callHistory = [];
|
|
530
|
+
if (options.initialData) {
|
|
531
|
+
Object.entries(options.initialData).forEach(([key, value]) => {
|
|
532
|
+
this.data.set(key, value);
|
|
533
|
+
});
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
// ============================================================================
|
|
537
|
+
// StorageAdapter Interface
|
|
538
|
+
// ============================================================================
|
|
539
|
+
/**
|
|
540
|
+
* Get an item from storage.
|
|
541
|
+
*/
|
|
542
|
+
getItem(key) {
|
|
543
|
+
this.callHistory.push({
|
|
544
|
+
method: "getItem",
|
|
545
|
+
key,
|
|
546
|
+
timestamp: Date.now()
|
|
547
|
+
});
|
|
548
|
+
return this.data.get(key) ?? null;
|
|
549
|
+
}
|
|
550
|
+
/**
|
|
551
|
+
* Set an item in storage.
|
|
552
|
+
*/
|
|
553
|
+
setItem(key, value) {
|
|
554
|
+
this.callHistory.push({
|
|
555
|
+
method: "setItem",
|
|
556
|
+
key,
|
|
557
|
+
value,
|
|
558
|
+
timestamp: Date.now()
|
|
559
|
+
});
|
|
560
|
+
this.data.set(key, value);
|
|
561
|
+
}
|
|
562
|
+
/**
|
|
563
|
+
* Remove an item from storage.
|
|
564
|
+
*/
|
|
565
|
+
removeItem(key) {
|
|
566
|
+
this.callHistory.push({
|
|
567
|
+
method: "removeItem",
|
|
568
|
+
key,
|
|
569
|
+
timestamp: Date.now()
|
|
570
|
+
});
|
|
571
|
+
this.data.delete(key);
|
|
572
|
+
}
|
|
573
|
+
// ============================================================================
|
|
574
|
+
// Test Utilities
|
|
575
|
+
// ============================================================================
|
|
576
|
+
/**
|
|
577
|
+
* Clear all data from storage.
|
|
578
|
+
*/
|
|
579
|
+
clear() {
|
|
580
|
+
this.data.clear();
|
|
581
|
+
}
|
|
582
|
+
/**
|
|
583
|
+
* Reset the call history.
|
|
584
|
+
*/
|
|
585
|
+
resetHistory() {
|
|
586
|
+
this.callHistory = [];
|
|
587
|
+
}
|
|
588
|
+
/**
|
|
589
|
+
* Reset both data and history.
|
|
590
|
+
*/
|
|
591
|
+
reset() {
|
|
592
|
+
this.clear();
|
|
593
|
+
this.resetHistory();
|
|
594
|
+
}
|
|
595
|
+
/**
|
|
596
|
+
* Get all storage keys.
|
|
597
|
+
*/
|
|
598
|
+
keys() {
|
|
599
|
+
return Array.from(this.data.keys());
|
|
600
|
+
}
|
|
601
|
+
/**
|
|
602
|
+
* Check if a key exists in storage.
|
|
603
|
+
*/
|
|
604
|
+
has(key) {
|
|
605
|
+
return this.data.has(key);
|
|
606
|
+
}
|
|
607
|
+
/**
|
|
608
|
+
* Get the number of items in storage.
|
|
609
|
+
*/
|
|
610
|
+
get size() {
|
|
611
|
+
return this.data.size;
|
|
612
|
+
}
|
|
613
|
+
/**
|
|
614
|
+
* Get all data as a plain object.
|
|
615
|
+
*/
|
|
616
|
+
getData() {
|
|
617
|
+
return Object.fromEntries(this.data);
|
|
618
|
+
}
|
|
619
|
+
/**
|
|
620
|
+
* Set multiple items at once.
|
|
621
|
+
*/
|
|
622
|
+
setData(data) {
|
|
623
|
+
Object.entries(data).forEach(([key, value]) => {
|
|
624
|
+
this.data.set(key, value);
|
|
625
|
+
});
|
|
626
|
+
}
|
|
627
|
+
// ============================================================================
|
|
628
|
+
// Call History Utilities
|
|
629
|
+
// ============================================================================
|
|
630
|
+
/**
|
|
631
|
+
* Get the full call history.
|
|
632
|
+
*/
|
|
633
|
+
getHistory() {
|
|
634
|
+
return [...this.callHistory];
|
|
635
|
+
}
|
|
636
|
+
/**
|
|
637
|
+
* Get calls for a specific method.
|
|
638
|
+
*/
|
|
639
|
+
getCallsFor(method) {
|
|
640
|
+
return this.callHistory.filter((call) => call.method === method);
|
|
641
|
+
}
|
|
642
|
+
/**
|
|
643
|
+
* Get calls for a specific key.
|
|
644
|
+
*/
|
|
645
|
+
getCallsForKey(key) {
|
|
646
|
+
return this.callHistory.filter((call) => call.key === key);
|
|
647
|
+
}
|
|
648
|
+
/**
|
|
649
|
+
* Check if a method was called.
|
|
650
|
+
*/
|
|
651
|
+
wasCalled(method) {
|
|
652
|
+
return this.callHistory.some((call) => call.method === method);
|
|
653
|
+
}
|
|
654
|
+
/**
|
|
655
|
+
* Check if a method was called with a specific key.
|
|
656
|
+
*/
|
|
657
|
+
wasCalledWith(method, key) {
|
|
658
|
+
return this.callHistory.some((call) => call.method === method && call.key === key);
|
|
659
|
+
}
|
|
660
|
+
/**
|
|
661
|
+
* Get the last call made.
|
|
662
|
+
*/
|
|
663
|
+
getLastCall() {
|
|
664
|
+
return this.callHistory[this.callHistory.length - 1];
|
|
665
|
+
}
|
|
666
|
+
/**
|
|
667
|
+
* Get the total number of calls made.
|
|
668
|
+
*/
|
|
669
|
+
get callCount() {
|
|
670
|
+
return this.callHistory.length;
|
|
671
|
+
}
|
|
672
|
+
};
|
|
673
|
+
function createMockStorage(options = {}) {
|
|
674
|
+
return new MockStorageAdapter(options);
|
|
675
|
+
}
|
|
676
|
+
function createMockStorageWithAuth(accessToken, refreshToken, user, storageKey = "classic_auth") {
|
|
677
|
+
const storage = new MockStorageAdapter();
|
|
678
|
+
storage.setItem(
|
|
679
|
+
storageKey,
|
|
680
|
+
JSON.stringify({
|
|
681
|
+
accessToken,
|
|
682
|
+
refreshToken,
|
|
683
|
+
user
|
|
684
|
+
})
|
|
685
|
+
);
|
|
686
|
+
return storage;
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
// src/testing/mocks/fetch.ts
|
|
690
|
+
var MockFetchInstance = class {
|
|
691
|
+
constructor(options = {}) {
|
|
692
|
+
this.routes = [];
|
|
693
|
+
this.callHistory = [];
|
|
694
|
+
// ============================================================================
|
|
695
|
+
// Fetch Implementation
|
|
696
|
+
// ============================================================================
|
|
697
|
+
/**
|
|
698
|
+
* The mock fetch function.
|
|
699
|
+
*/
|
|
700
|
+
this.fetch = async (input, init) => {
|
|
701
|
+
const url = typeof input === "string" ? input : input instanceof URL ? input.href : input.url;
|
|
702
|
+
const method = (init?.method ?? "GET").toUpperCase();
|
|
703
|
+
const path = this.extractPath(url);
|
|
704
|
+
this.callHistory.push({
|
|
705
|
+
url,
|
|
706
|
+
options: init ?? {},
|
|
707
|
+
timestamp: Date.now()
|
|
708
|
+
});
|
|
709
|
+
let body = null;
|
|
710
|
+
if (init?.body) {
|
|
711
|
+
try {
|
|
712
|
+
body = JSON.parse(init.body);
|
|
713
|
+
} catch {
|
|
714
|
+
body = init.body;
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
const request = {
|
|
718
|
+
method,
|
|
719
|
+
url,
|
|
720
|
+
path,
|
|
721
|
+
body,
|
|
722
|
+
headers: new Headers(init?.headers)
|
|
723
|
+
};
|
|
724
|
+
const route = this.routes.find((r) => r.method === method && this.pathMatches(r.path, path));
|
|
725
|
+
if (!route) {
|
|
726
|
+
return this.createResponse(this.defaultResponse);
|
|
727
|
+
}
|
|
728
|
+
if (route.delay) {
|
|
729
|
+
await new Promise((resolve) => setTimeout(resolve, route.delay));
|
|
730
|
+
}
|
|
731
|
+
let responseData;
|
|
732
|
+
if (route.handler) {
|
|
733
|
+
responseData = await route.handler(request);
|
|
734
|
+
} else {
|
|
735
|
+
responseData = {
|
|
736
|
+
status: route.status ?? 200,
|
|
737
|
+
response: route.response,
|
|
738
|
+
headers: route.headers
|
|
739
|
+
};
|
|
740
|
+
}
|
|
741
|
+
return this.createResponse(responseData);
|
|
742
|
+
};
|
|
743
|
+
this.baseUrl = options.baseUrl ?? "http://localhost:3000";
|
|
744
|
+
this.defaultResponse = options.defaultResponse ?? {
|
|
745
|
+
status: 404,
|
|
746
|
+
response: { error: { message: "Not Found" } }
|
|
747
|
+
};
|
|
748
|
+
this.setupDefaultRoutes();
|
|
749
|
+
}
|
|
750
|
+
// ============================================================================
|
|
751
|
+
// Route Management
|
|
752
|
+
// ============================================================================
|
|
753
|
+
/**
|
|
754
|
+
* Add a route to the mock.
|
|
755
|
+
*/
|
|
756
|
+
addRoute(route) {
|
|
757
|
+
this.routes.unshift(route);
|
|
758
|
+
return this;
|
|
759
|
+
}
|
|
760
|
+
/**
|
|
761
|
+
* Add multiple routes at once.
|
|
762
|
+
*/
|
|
763
|
+
addRoutes(routes) {
|
|
764
|
+
routes.forEach((route) => this.addRoute(route));
|
|
765
|
+
return this;
|
|
766
|
+
}
|
|
767
|
+
/**
|
|
768
|
+
* Remove a route by path and method.
|
|
769
|
+
*/
|
|
770
|
+
removeRoute(method, path) {
|
|
771
|
+
this.routes = this.routes.filter(
|
|
772
|
+
(route) => !(route.method === method && this.pathMatches(route.path, path.toString()))
|
|
773
|
+
);
|
|
774
|
+
return this;
|
|
775
|
+
}
|
|
776
|
+
/**
|
|
777
|
+
* Clear all routes.
|
|
778
|
+
*/
|
|
779
|
+
clearRoutes() {
|
|
780
|
+
this.routes = [];
|
|
781
|
+
return this;
|
|
782
|
+
}
|
|
783
|
+
/**
|
|
784
|
+
* Reset to default routes.
|
|
785
|
+
*/
|
|
786
|
+
reset() {
|
|
787
|
+
this.routes = [];
|
|
788
|
+
this.callHistory = [];
|
|
789
|
+
this.setupDefaultRoutes();
|
|
790
|
+
return this;
|
|
791
|
+
}
|
|
792
|
+
// ============================================================================
|
|
793
|
+
// Response Helpers
|
|
794
|
+
// ============================================================================
|
|
795
|
+
createResponse(data) {
|
|
796
|
+
const status = data.status ?? 200;
|
|
797
|
+
const body = data.response !== void 0 ? JSON.stringify(data.response) : null;
|
|
798
|
+
const headers = new Headers({
|
|
799
|
+
"Content-Type": "application/json",
|
|
800
|
+
...data.headers
|
|
801
|
+
});
|
|
802
|
+
return new Response(body, { status, headers });
|
|
803
|
+
}
|
|
804
|
+
extractPath(url) {
|
|
805
|
+
try {
|
|
806
|
+
const urlObj = new URL(url, this.baseUrl);
|
|
807
|
+
return urlObj.pathname + urlObj.search;
|
|
808
|
+
} catch {
|
|
809
|
+
return url;
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
pathMatches(pattern, path) {
|
|
813
|
+
if (pattern instanceof RegExp) {
|
|
814
|
+
return pattern.test(path);
|
|
815
|
+
}
|
|
816
|
+
if (pattern.includes("*")) {
|
|
817
|
+
const regexPattern = pattern.replace(/\*/g, "[^/]+");
|
|
818
|
+
return new RegExp(`^${regexPattern}$`).test(path);
|
|
819
|
+
}
|
|
820
|
+
if (path.includes("?") && !pattern.includes("?")) {
|
|
821
|
+
return path.startsWith(pattern);
|
|
822
|
+
}
|
|
823
|
+
return pattern === path;
|
|
824
|
+
}
|
|
825
|
+
// ============================================================================
|
|
826
|
+
// Call History Utilities
|
|
827
|
+
// ============================================================================
|
|
828
|
+
/**
|
|
829
|
+
* Get all recorded calls.
|
|
830
|
+
*/
|
|
831
|
+
getCalls() {
|
|
832
|
+
return [...this.callHistory];
|
|
833
|
+
}
|
|
834
|
+
/**
|
|
835
|
+
* Get calls to a specific path.
|
|
836
|
+
*/
|
|
837
|
+
getCallsTo(path) {
|
|
838
|
+
return this.callHistory.filter((call) => {
|
|
839
|
+
const callPath = this.extractPath(call.url);
|
|
840
|
+
return callPath.startsWith(path);
|
|
841
|
+
});
|
|
842
|
+
}
|
|
843
|
+
/**
|
|
844
|
+
* Get calls with a specific method.
|
|
845
|
+
*/
|
|
846
|
+
getCallsWithMethod(method) {
|
|
847
|
+
return this.callHistory.filter(
|
|
848
|
+
(call) => (call.options.method ?? "GET").toUpperCase() === method.toUpperCase()
|
|
849
|
+
);
|
|
850
|
+
}
|
|
851
|
+
/**
|
|
852
|
+
* Check if a path was called.
|
|
853
|
+
*/
|
|
854
|
+
wasCalled(path) {
|
|
855
|
+
return this.getCallsTo(path).length > 0;
|
|
856
|
+
}
|
|
857
|
+
/**
|
|
858
|
+
* Check if a path was called with a specific method.
|
|
859
|
+
*/
|
|
860
|
+
wasCalledWith(method, path) {
|
|
861
|
+
return this.callHistory.some((call) => {
|
|
862
|
+
const callPath = this.extractPath(call.url);
|
|
863
|
+
const callMethod = (call.options.method ?? "GET").toUpperCase();
|
|
864
|
+
return callMethod === method.toUpperCase() && callPath.startsWith(path);
|
|
865
|
+
});
|
|
866
|
+
}
|
|
867
|
+
/**
|
|
868
|
+
* Assert that a path was called.
|
|
869
|
+
*/
|
|
870
|
+
assertCalled(path, message) {
|
|
871
|
+
if (!this.wasCalled(path)) {
|
|
872
|
+
throw new Error(message ?? `Expected ${path} to be called`);
|
|
873
|
+
}
|
|
874
|
+
}
|
|
875
|
+
/**
|
|
876
|
+
* Assert that a path was not called.
|
|
877
|
+
*/
|
|
878
|
+
assertNotCalled(path, message) {
|
|
879
|
+
if (this.wasCalled(path)) {
|
|
880
|
+
throw new Error(message ?? `Expected ${path} not to be called`);
|
|
881
|
+
}
|
|
882
|
+
}
|
|
883
|
+
/**
|
|
884
|
+
* Get the last call made.
|
|
885
|
+
*/
|
|
886
|
+
getLastCall() {
|
|
887
|
+
return this.callHistory[this.callHistory.length - 1];
|
|
888
|
+
}
|
|
889
|
+
/**
|
|
890
|
+
* Clear call history.
|
|
891
|
+
*/
|
|
892
|
+
clearHistory() {
|
|
893
|
+
this.callHistory = [];
|
|
894
|
+
return this;
|
|
895
|
+
}
|
|
896
|
+
/**
|
|
897
|
+
* Get total call count.
|
|
898
|
+
*/
|
|
899
|
+
get callCount() {
|
|
900
|
+
return this.callHistory.length;
|
|
901
|
+
}
|
|
902
|
+
// ============================================================================
|
|
903
|
+
// Scenario Configuration Helpers
|
|
904
|
+
// ============================================================================
|
|
905
|
+
/**
|
|
906
|
+
* Configure login to require MFA.
|
|
907
|
+
*/
|
|
908
|
+
requireMFA() {
|
|
909
|
+
this.removeRoute("POST", "/auth/login");
|
|
910
|
+
this.addRoute({
|
|
911
|
+
method: "POST",
|
|
912
|
+
path: "/auth/login",
|
|
913
|
+
response: { data: mockMFARequired }
|
|
914
|
+
});
|
|
915
|
+
return this;
|
|
916
|
+
}
|
|
917
|
+
/**
|
|
918
|
+
* Configure login to fail.
|
|
919
|
+
*/
|
|
920
|
+
failLogin(error = "Invalid credentials") {
|
|
921
|
+
this.removeRoute("POST", "/auth/login");
|
|
922
|
+
this.addRoute({
|
|
923
|
+
method: "POST",
|
|
924
|
+
path: "/auth/login",
|
|
925
|
+
status: 401,
|
|
926
|
+
response: { error: { message: error } }
|
|
927
|
+
});
|
|
928
|
+
return this;
|
|
929
|
+
}
|
|
930
|
+
/**
|
|
931
|
+
* Configure an endpoint to fail.
|
|
932
|
+
*/
|
|
933
|
+
failEndpoint(method, path, status = 500, error = "Internal Server Error") {
|
|
934
|
+
this.removeRoute(method, path);
|
|
935
|
+
this.addRoute({
|
|
936
|
+
method,
|
|
937
|
+
path,
|
|
938
|
+
status,
|
|
939
|
+
response: { error: { message: error } }
|
|
940
|
+
});
|
|
941
|
+
return this;
|
|
942
|
+
}
|
|
943
|
+
/**
|
|
944
|
+
* Configure logout to return SSO redirect URL.
|
|
945
|
+
*/
|
|
946
|
+
enableSSOLogout(logoutUrl = "https://sso.example.com/logout") {
|
|
947
|
+
this.removeRoute("POST", "/auth/logout");
|
|
948
|
+
this.addRoute({
|
|
949
|
+
method: "POST",
|
|
950
|
+
path: "/auth/logout",
|
|
951
|
+
response: { data: { ...mockSSOLogoutResponse, logoutUrl } }
|
|
952
|
+
});
|
|
953
|
+
return this;
|
|
954
|
+
}
|
|
955
|
+
/**
|
|
956
|
+
* Add response delay to all routes.
|
|
957
|
+
*/
|
|
958
|
+
setDelay(delay) {
|
|
959
|
+
this.routes.forEach((route) => {
|
|
960
|
+
route.delay = delay;
|
|
961
|
+
});
|
|
962
|
+
return this;
|
|
963
|
+
}
|
|
964
|
+
// ============================================================================
|
|
965
|
+
// Default Routes Setup
|
|
966
|
+
// ============================================================================
|
|
967
|
+
setupDefaultRoutes() {
|
|
968
|
+
this.addRoute({
|
|
969
|
+
method: "POST",
|
|
970
|
+
path: "/auth/login",
|
|
971
|
+
response: { data: mockLoginSuccess }
|
|
972
|
+
});
|
|
973
|
+
this.addRoute({
|
|
974
|
+
method: "POST",
|
|
975
|
+
path: "/auth/logout",
|
|
976
|
+
response: { data: mockLogoutSuccess }
|
|
977
|
+
});
|
|
978
|
+
this.addRoute({
|
|
979
|
+
method: "POST",
|
|
980
|
+
path: "/auth/register",
|
|
981
|
+
response: { data: mockRegisterSuccess }
|
|
982
|
+
});
|
|
983
|
+
this.addRoute({
|
|
984
|
+
method: "POST",
|
|
985
|
+
path: "/auth/refresh",
|
|
986
|
+
response: { data: { accessToken: mockAccessToken, refreshToken: mockRefreshToken } }
|
|
987
|
+
});
|
|
988
|
+
this.addRoute({
|
|
989
|
+
method: "POST",
|
|
990
|
+
path: "/auth/forgot-password",
|
|
991
|
+
response: { message: "Password reset email sent" }
|
|
992
|
+
});
|
|
993
|
+
this.addRoute({
|
|
994
|
+
method: "POST",
|
|
995
|
+
path: "/auth/reset-password",
|
|
996
|
+
response: { message: "Password reset successful" }
|
|
997
|
+
});
|
|
998
|
+
this.addRoute({
|
|
999
|
+
method: "GET",
|
|
1000
|
+
path: "/auth/profile",
|
|
1001
|
+
response: mockUser
|
|
1002
|
+
});
|
|
1003
|
+
this.addRoute({
|
|
1004
|
+
method: "PUT",
|
|
1005
|
+
path: "/auth/profile",
|
|
1006
|
+
response: { data: mockUser }
|
|
1007
|
+
});
|
|
1008
|
+
this.addRoute({
|
|
1009
|
+
method: "PUT",
|
|
1010
|
+
path: "/auth/change-password",
|
|
1011
|
+
response: { message: "Password changed successfully" }
|
|
1012
|
+
});
|
|
1013
|
+
this.addRoute({
|
|
1014
|
+
method: "POST",
|
|
1015
|
+
path: "/auth/resend-verification",
|
|
1016
|
+
response: { message: "Verification email sent" }
|
|
1017
|
+
});
|
|
1018
|
+
this.addRoute({
|
|
1019
|
+
method: "POST",
|
|
1020
|
+
path: "/auth/verify-email",
|
|
1021
|
+
response: { message: "Email verified successfully", user: mockUser }
|
|
1022
|
+
});
|
|
1023
|
+
this.addRoute({
|
|
1024
|
+
method: "GET",
|
|
1025
|
+
path: "/auth/sessions",
|
|
1026
|
+
response: { sessions: mockSessions, total: mockSessions.length }
|
|
1027
|
+
});
|
|
1028
|
+
this.addRoute({
|
|
1029
|
+
method: "DELETE",
|
|
1030
|
+
path: /^\/auth\/sessions(\/.*)?$/,
|
|
1031
|
+
response: { message: "Session revoked" }
|
|
1032
|
+
});
|
|
1033
|
+
this.addRoute({
|
|
1034
|
+
method: "GET",
|
|
1035
|
+
path: "/auth/api-keys",
|
|
1036
|
+
response: { apiKeys: mockApiKeys }
|
|
1037
|
+
});
|
|
1038
|
+
this.addRoute({
|
|
1039
|
+
method: "POST",
|
|
1040
|
+
path: "/auth/api-keys",
|
|
1041
|
+
response: { data: { ...mockApiKeys[0], key: "sk_live_full_key_shown_once" } }
|
|
1042
|
+
});
|
|
1043
|
+
this.addRoute({
|
|
1044
|
+
method: "DELETE",
|
|
1045
|
+
path: /^\/auth\/api-keys\/.+$/,
|
|
1046
|
+
response: { message: "API key revoked" }
|
|
1047
|
+
});
|
|
1048
|
+
this.addRoute({
|
|
1049
|
+
method: "PUT",
|
|
1050
|
+
path: /^\/auth\/api-keys\/.+$/,
|
|
1051
|
+
response: { message: "API key updated" }
|
|
1052
|
+
});
|
|
1053
|
+
this.addRoute({
|
|
1054
|
+
method: "GET",
|
|
1055
|
+
path: "/auth/mfa/status",
|
|
1056
|
+
response: mockMFAStatusEnabled
|
|
1057
|
+
});
|
|
1058
|
+
this.addRoute({
|
|
1059
|
+
method: "POST",
|
|
1060
|
+
path: "/auth/mfa-setup",
|
|
1061
|
+
response: { data: mockMFASetup }
|
|
1062
|
+
});
|
|
1063
|
+
this.addRoute({
|
|
1064
|
+
method: "POST",
|
|
1065
|
+
path: "/auth/mfa/verify",
|
|
1066
|
+
response: { message: "MFA setup complete" }
|
|
1067
|
+
});
|
|
1068
|
+
this.addRoute({
|
|
1069
|
+
method: "POST",
|
|
1070
|
+
path: "/auth/mfa/challenge",
|
|
1071
|
+
response: { data: mockLoginSuccess }
|
|
1072
|
+
});
|
|
1073
|
+
this.addRoute({
|
|
1074
|
+
method: "DELETE",
|
|
1075
|
+
path: "/auth/mfa",
|
|
1076
|
+
response: { message: "MFA disabled" }
|
|
1077
|
+
});
|
|
1078
|
+
this.addRoute({
|
|
1079
|
+
method: "POST",
|
|
1080
|
+
path: "/auth/mfa/regenerate-backup-codes",
|
|
1081
|
+
response: { data: { backupCodes: mockMFASetup.backupCodes } }
|
|
1082
|
+
});
|
|
1083
|
+
this.addRoute({
|
|
1084
|
+
method: "GET",
|
|
1085
|
+
path: "/auth/devices",
|
|
1086
|
+
response: { devices: mockDevices, total: mockDevices.length }
|
|
1087
|
+
});
|
|
1088
|
+
this.addRoute({
|
|
1089
|
+
method: "PUT",
|
|
1090
|
+
path: /^\/auth\/devices\/.+\/trust$/,
|
|
1091
|
+
response: { message: "Device trusted" }
|
|
1092
|
+
});
|
|
1093
|
+
this.addRoute({
|
|
1094
|
+
method: "DELETE",
|
|
1095
|
+
path: /^\/auth\/devices\/.+\/(revoke)?$/,
|
|
1096
|
+
response: { message: "Device removed" }
|
|
1097
|
+
});
|
|
1098
|
+
this.addRoute({
|
|
1099
|
+
method: "POST",
|
|
1100
|
+
path: "/auth/device/approve",
|
|
1101
|
+
response: { data: { message: "Device approved", device: mockDevices[0] } }
|
|
1102
|
+
});
|
|
1103
|
+
this.addRoute({
|
|
1104
|
+
method: "POST",
|
|
1105
|
+
path: "/auth/device/block",
|
|
1106
|
+
response: { data: { message: "Device blocked", device: mockDevices[0] } }
|
|
1107
|
+
});
|
|
1108
|
+
this.addRoute({
|
|
1109
|
+
method: "GET",
|
|
1110
|
+
path: "/auth/preferences",
|
|
1111
|
+
response: { preferences: mockUserPreferences }
|
|
1112
|
+
});
|
|
1113
|
+
this.addRoute({
|
|
1114
|
+
method: "PUT",
|
|
1115
|
+
path: "/auth/preferences",
|
|
1116
|
+
response: { message: "Preferences updated" }
|
|
1117
|
+
});
|
|
1118
|
+
this.addRoute({
|
|
1119
|
+
method: "GET",
|
|
1120
|
+
path: "/auth/sso/accounts",
|
|
1121
|
+
response: { accounts: [mockLinkedAccount], count: 1 }
|
|
1122
|
+
});
|
|
1123
|
+
this.addRoute({
|
|
1124
|
+
method: "DELETE",
|
|
1125
|
+
path: "/auth/sso/unlink",
|
|
1126
|
+
response: { message: "Account unlinked" }
|
|
1127
|
+
});
|
|
1128
|
+
this.addRoute({
|
|
1129
|
+
method: "GET",
|
|
1130
|
+
path: "/auth/security/events",
|
|
1131
|
+
response: {
|
|
1132
|
+
data: {
|
|
1133
|
+
events: mockSecurityEvents,
|
|
1134
|
+
pagination: { page: 1, limit: 10, total: mockSecurityEvents.length, totalPages: 1 }
|
|
1135
|
+
}
|
|
1136
|
+
}
|
|
1137
|
+
});
|
|
1138
|
+
}
|
|
1139
|
+
};
|
|
1140
|
+
function createMockFetch(options = {}) {
|
|
1141
|
+
return new MockFetchInstance(options);
|
|
1142
|
+
}
|
|
1143
|
+
function createMockFetchWithRoutes(routes) {
|
|
1144
|
+
const mock = new MockFetchInstance();
|
|
1145
|
+
mock.clearRoutes();
|
|
1146
|
+
mock.addRoutes(routes);
|
|
1147
|
+
return mock;
|
|
1148
|
+
}
|
|
1149
|
+
|
|
1150
|
+
// src/testing/mocks/auth-store.ts
|
|
1151
|
+
var MockAuthStore = class {
|
|
1152
|
+
constructor(options = {}) {
|
|
1153
|
+
this.subscribers = /* @__PURE__ */ new Set();
|
|
1154
|
+
this.callHistory = [];
|
|
1155
|
+
this.state = {
|
|
1156
|
+
accessToken: options.initialState?.accessToken ?? null,
|
|
1157
|
+
refreshToken: options.initialState?.refreshToken ?? null,
|
|
1158
|
+
user: options.initialState?.user ?? null,
|
|
1159
|
+
isAuthenticated: options.initialState?.isAuthenticated ?? false
|
|
1160
|
+
};
|
|
1161
|
+
}
|
|
1162
|
+
// ============================================================================
|
|
1163
|
+
// Store Contract
|
|
1164
|
+
// ============================================================================
|
|
1165
|
+
/**
|
|
1166
|
+
* Subscribe to state changes (Svelte store contract).
|
|
1167
|
+
*/
|
|
1168
|
+
subscribe(subscriber) {
|
|
1169
|
+
this.subscribers.add(subscriber);
|
|
1170
|
+
subscriber(this.state);
|
|
1171
|
+
return () => this.subscribers.delete(subscriber);
|
|
1172
|
+
}
|
|
1173
|
+
notify() {
|
|
1174
|
+
this.subscribers.forEach((sub) => sub(this.state));
|
|
1175
|
+
}
|
|
1176
|
+
recordCall(method, args) {
|
|
1177
|
+
this.callHistory.push({
|
|
1178
|
+
method,
|
|
1179
|
+
args,
|
|
1180
|
+
timestamp: Date.now()
|
|
1181
|
+
});
|
|
1182
|
+
}
|
|
1183
|
+
// ============================================================================
|
|
1184
|
+
// Getters
|
|
1185
|
+
// ============================================================================
|
|
1186
|
+
get accessToken() {
|
|
1187
|
+
return this.state.accessToken;
|
|
1188
|
+
}
|
|
1189
|
+
get refreshToken() {
|
|
1190
|
+
return this.state.refreshToken;
|
|
1191
|
+
}
|
|
1192
|
+
get user() {
|
|
1193
|
+
return this.state.user;
|
|
1194
|
+
}
|
|
1195
|
+
get isAuthenticated() {
|
|
1196
|
+
return this.state.isAuthenticated;
|
|
1197
|
+
}
|
|
1198
|
+
/**
|
|
1199
|
+
* Get the current auth state snapshot.
|
|
1200
|
+
*/
|
|
1201
|
+
getState() {
|
|
1202
|
+
return { ...this.state };
|
|
1203
|
+
}
|
|
1204
|
+
// ============================================================================
|
|
1205
|
+
// Auth Actions
|
|
1206
|
+
// ============================================================================
|
|
1207
|
+
/**
|
|
1208
|
+
* Set auth data after successful login.
|
|
1209
|
+
*/
|
|
1210
|
+
setAuth(accessToken, refreshToken, user, sessionToken) {
|
|
1211
|
+
this.recordCall("setAuth", [accessToken, refreshToken, user, sessionToken]);
|
|
1212
|
+
this.state = {
|
|
1213
|
+
accessToken,
|
|
1214
|
+
refreshToken,
|
|
1215
|
+
user,
|
|
1216
|
+
isAuthenticated: true
|
|
1217
|
+
};
|
|
1218
|
+
this.notify();
|
|
1219
|
+
}
|
|
1220
|
+
/**
|
|
1221
|
+
* Update tokens (e.g., after refresh).
|
|
1222
|
+
*/
|
|
1223
|
+
updateTokens(accessToken, refreshToken) {
|
|
1224
|
+
this.recordCall("updateTokens", [accessToken, refreshToken]);
|
|
1225
|
+
this.state = {
|
|
1226
|
+
...this.state,
|
|
1227
|
+
accessToken,
|
|
1228
|
+
refreshToken
|
|
1229
|
+
};
|
|
1230
|
+
this.notify();
|
|
1231
|
+
}
|
|
1232
|
+
/**
|
|
1233
|
+
* Update user data.
|
|
1234
|
+
*/
|
|
1235
|
+
updateUser(user) {
|
|
1236
|
+
this.recordCall("updateUser", [user]);
|
|
1237
|
+
this.state = {
|
|
1238
|
+
...this.state,
|
|
1239
|
+
user
|
|
1240
|
+
};
|
|
1241
|
+
this.notify();
|
|
1242
|
+
}
|
|
1243
|
+
/**
|
|
1244
|
+
* Clear auth state (logout).
|
|
1245
|
+
*/
|
|
1246
|
+
logout() {
|
|
1247
|
+
this.recordCall("logout", []);
|
|
1248
|
+
this.state = {
|
|
1249
|
+
accessToken: null,
|
|
1250
|
+
refreshToken: null,
|
|
1251
|
+
user: null,
|
|
1252
|
+
isAuthenticated: false
|
|
1253
|
+
};
|
|
1254
|
+
this.notify();
|
|
1255
|
+
}
|
|
1256
|
+
/**
|
|
1257
|
+
* Logout with SSO support.
|
|
1258
|
+
*/
|
|
1259
|
+
async logoutWithSSO() {
|
|
1260
|
+
this.recordCall("logoutWithSSO", []);
|
|
1261
|
+
this.logout();
|
|
1262
|
+
return {};
|
|
1263
|
+
}
|
|
1264
|
+
/**
|
|
1265
|
+
* Re-hydrate state from storage.
|
|
1266
|
+
*/
|
|
1267
|
+
rehydrate() {
|
|
1268
|
+
this.recordCall("rehydrate", []);
|
|
1269
|
+
}
|
|
1270
|
+
// ============================================================================
|
|
1271
|
+
// Permission Checks
|
|
1272
|
+
// ============================================================================
|
|
1273
|
+
/**
|
|
1274
|
+
* Check if user has a specific permission.
|
|
1275
|
+
*/
|
|
1276
|
+
hasPermission(permission) {
|
|
1277
|
+
return this.state.user?.permissions?.includes(permission) ?? false;
|
|
1278
|
+
}
|
|
1279
|
+
/**
|
|
1280
|
+
* Check if user has a specific role.
|
|
1281
|
+
*/
|
|
1282
|
+
hasRole(role) {
|
|
1283
|
+
return this.state.user?.roles?.includes(role) ?? false;
|
|
1284
|
+
}
|
|
1285
|
+
/**
|
|
1286
|
+
* Check if user has any of the specified roles.
|
|
1287
|
+
*/
|
|
1288
|
+
hasAnyRole(roles) {
|
|
1289
|
+
return roles.some((role) => this.state.user?.roles?.includes(role)) ?? false;
|
|
1290
|
+
}
|
|
1291
|
+
/**
|
|
1292
|
+
* Check if user has all of the specified roles.
|
|
1293
|
+
*/
|
|
1294
|
+
hasAllRoles(roles) {
|
|
1295
|
+
return roles.every((role) => this.state.user?.roles?.includes(role)) ?? false;
|
|
1296
|
+
}
|
|
1297
|
+
/**
|
|
1298
|
+
* Check if user has any of the specified permissions.
|
|
1299
|
+
*/
|
|
1300
|
+
hasAnyPermission(permissions) {
|
|
1301
|
+
return permissions.some((perm) => this.state.user?.permissions?.includes(perm)) ?? false;
|
|
1302
|
+
}
|
|
1303
|
+
/**
|
|
1304
|
+
* Check if user has all of the specified permissions.
|
|
1305
|
+
*/
|
|
1306
|
+
hasAllPermissions(permissions) {
|
|
1307
|
+
return permissions.every((perm) => this.state.user?.permissions?.includes(perm)) ?? false;
|
|
1308
|
+
}
|
|
1309
|
+
// ============================================================================
|
|
1310
|
+
// Test Utilities
|
|
1311
|
+
// ============================================================================
|
|
1312
|
+
/**
|
|
1313
|
+
* Reset the store to initial state.
|
|
1314
|
+
*/
|
|
1315
|
+
reset() {
|
|
1316
|
+
this.state = {
|
|
1317
|
+
accessToken: null,
|
|
1318
|
+
refreshToken: null,
|
|
1319
|
+
user: null,
|
|
1320
|
+
isAuthenticated: false
|
|
1321
|
+
};
|
|
1322
|
+
this.callHistory = [];
|
|
1323
|
+
this.notify();
|
|
1324
|
+
}
|
|
1325
|
+
/**
|
|
1326
|
+
* Simulate an authenticated state.
|
|
1327
|
+
*/
|
|
1328
|
+
simulateAuthenticated(user = mockUser) {
|
|
1329
|
+
this.state = {
|
|
1330
|
+
accessToken: mockAccessToken,
|
|
1331
|
+
refreshToken: mockRefreshToken,
|
|
1332
|
+
user,
|
|
1333
|
+
isAuthenticated: true
|
|
1334
|
+
};
|
|
1335
|
+
this.notify();
|
|
1336
|
+
}
|
|
1337
|
+
/**
|
|
1338
|
+
* Simulate an unauthenticated state.
|
|
1339
|
+
*/
|
|
1340
|
+
simulateUnauthenticated() {
|
|
1341
|
+
this.state = {
|
|
1342
|
+
accessToken: null,
|
|
1343
|
+
refreshToken: null,
|
|
1344
|
+
user: null,
|
|
1345
|
+
isAuthenticated: false
|
|
1346
|
+
};
|
|
1347
|
+
this.notify();
|
|
1348
|
+
}
|
|
1349
|
+
/**
|
|
1350
|
+
* Set the state directly for testing.
|
|
1351
|
+
*/
|
|
1352
|
+
setState(state) {
|
|
1353
|
+
this.state = {
|
|
1354
|
+
...this.state,
|
|
1355
|
+
...state
|
|
1356
|
+
};
|
|
1357
|
+
this.notify();
|
|
1358
|
+
}
|
|
1359
|
+
// ============================================================================
|
|
1360
|
+
// Call History
|
|
1361
|
+
// ============================================================================
|
|
1362
|
+
/**
|
|
1363
|
+
* Get all recorded method calls.
|
|
1364
|
+
*/
|
|
1365
|
+
getCalls() {
|
|
1366
|
+
return [...this.callHistory];
|
|
1367
|
+
}
|
|
1368
|
+
/**
|
|
1369
|
+
* Get calls for a specific method.
|
|
1370
|
+
*/
|
|
1371
|
+
getCallsFor(method) {
|
|
1372
|
+
return this.callHistory.filter((call) => call.method === method);
|
|
1373
|
+
}
|
|
1374
|
+
/**
|
|
1375
|
+
* Check if a method was called.
|
|
1376
|
+
*/
|
|
1377
|
+
wasCalled(method) {
|
|
1378
|
+
return this.callHistory.some((call) => call.method === method);
|
|
1379
|
+
}
|
|
1380
|
+
/**
|
|
1381
|
+
* Assert that a method was called.
|
|
1382
|
+
*/
|
|
1383
|
+
assertMethodCalled(method, message) {
|
|
1384
|
+
if (!this.wasCalled(method)) {
|
|
1385
|
+
throw new Error(message ?? `Expected ${method} to be called`);
|
|
1386
|
+
}
|
|
1387
|
+
}
|
|
1388
|
+
/**
|
|
1389
|
+
* Assert that a method was not called.
|
|
1390
|
+
*/
|
|
1391
|
+
assertMethodNotCalled(method, message) {
|
|
1392
|
+
if (this.wasCalled(method)) {
|
|
1393
|
+
throw new Error(message ?? `Expected ${method} not to be called`);
|
|
1394
|
+
}
|
|
1395
|
+
}
|
|
1396
|
+
/**
|
|
1397
|
+
* Get the last call made.
|
|
1398
|
+
*/
|
|
1399
|
+
getLastCall() {
|
|
1400
|
+
return this.callHistory[this.callHistory.length - 1];
|
|
1401
|
+
}
|
|
1402
|
+
/**
|
|
1403
|
+
* Get the last call to a specific method.
|
|
1404
|
+
*/
|
|
1405
|
+
getLastCallTo(method) {
|
|
1406
|
+
return [...this.callHistory].reverse().find((call) => call.method === method);
|
|
1407
|
+
}
|
|
1408
|
+
/**
|
|
1409
|
+
* Get total call count.
|
|
1410
|
+
*/
|
|
1411
|
+
get callCount() {
|
|
1412
|
+
return this.callHistory.length;
|
|
1413
|
+
}
|
|
1414
|
+
/**
|
|
1415
|
+
* Clear call history.
|
|
1416
|
+
*/
|
|
1417
|
+
clearHistory() {
|
|
1418
|
+
this.callHistory = [];
|
|
1419
|
+
}
|
|
1420
|
+
};
|
|
1421
|
+
function createMockAuthStore(options = {}) {
|
|
1422
|
+
return new MockAuthStore(options);
|
|
1423
|
+
}
|
|
1424
|
+
function createAuthenticatedMockStore(user = mockUser) {
|
|
1425
|
+
const store = new MockAuthStore();
|
|
1426
|
+
store.simulateAuthenticated(user);
|
|
1427
|
+
return store;
|
|
1428
|
+
}
|
|
1429
|
+
function createMockAuthActions(store) {
|
|
1430
|
+
return {
|
|
1431
|
+
setAuth: (accessToken, refreshToken, user, sessionToken) => store.setAuth(accessToken, refreshToken, user, sessionToken),
|
|
1432
|
+
updateTokens: (accessToken, refreshToken) => store.updateTokens(accessToken, refreshToken),
|
|
1433
|
+
updateUser: (user) => store.updateUser(user),
|
|
1434
|
+
logout: () => store.logout(),
|
|
1435
|
+
logoutWithSSO: () => store.logoutWithSSO(),
|
|
1436
|
+
hasPermission: (permission) => store.hasPermission(permission),
|
|
1437
|
+
hasRole: (role) => store.hasRole(role),
|
|
1438
|
+
hasAnyRole: (roles) => store.hasAnyRole(roles),
|
|
1439
|
+
hasAllRoles: (roles) => store.hasAllRoles(roles),
|
|
1440
|
+
hasAnyPermission: (permissions) => store.hasAnyPermission(permissions),
|
|
1441
|
+
hasAllPermissions: (permissions) => store.hasAllPermissions(permissions),
|
|
1442
|
+
rehydrate: () => store.rehydrate()
|
|
1443
|
+
};
|
|
1444
|
+
}
|
|
1445
|
+
function createMockIsAuthenticated(store) {
|
|
1446
|
+
return {
|
|
1447
|
+
subscribe: (subscriber) => {
|
|
1448
|
+
return store.subscribe((state) => subscriber(state.isAuthenticated));
|
|
1449
|
+
}
|
|
1450
|
+
};
|
|
1451
|
+
}
|
|
1452
|
+
function createMockCurrentUser(store) {
|
|
1453
|
+
return {
|
|
1454
|
+
subscribe: (subscriber) => {
|
|
1455
|
+
return store.subscribe((state) => subscriber(state.user));
|
|
1456
|
+
}
|
|
1457
|
+
};
|
|
1458
|
+
}
|
|
1459
|
+
function initAuth(options) {
|
|
1460
|
+
({
|
|
1461
|
+
...options,
|
|
1462
|
+
storageKey: options.storageKey ?? "classic_auth"
|
|
1463
|
+
});
|
|
1464
|
+
}
|
|
1465
|
+
|
|
1466
|
+
// src/testing/helpers/setup.ts
|
|
1467
|
+
function setupTestAuth(options = {}) {
|
|
1468
|
+
const {
|
|
1469
|
+
baseUrl = "http://localhost:3000",
|
|
1470
|
+
storageKey = "classic_auth",
|
|
1471
|
+
initialStorage,
|
|
1472
|
+
initialAuthState,
|
|
1473
|
+
fetchOptions,
|
|
1474
|
+
initConfig = true
|
|
1475
|
+
} = options;
|
|
1476
|
+
const mockStorage = createMockStorage({ initialData: initialStorage });
|
|
1477
|
+
const mockFetch = createMockFetch({ baseUrl, ...fetchOptions });
|
|
1478
|
+
const mockStore = createMockAuthStore({ initialState: initialAuthState });
|
|
1479
|
+
if (initConfig) {
|
|
1480
|
+
const config2 = {
|
|
1481
|
+
baseUrl,
|
|
1482
|
+
storageKey,
|
|
1483
|
+
storage: mockStorage,
|
|
1484
|
+
fetch: mockFetch.fetch
|
|
1485
|
+
};
|
|
1486
|
+
initAuth(config2);
|
|
1487
|
+
}
|
|
1488
|
+
const cleanup = () => {
|
|
1489
|
+
mockStorage.reset();
|
|
1490
|
+
mockFetch.reset();
|
|
1491
|
+
mockStore.reset();
|
|
1492
|
+
};
|
|
1493
|
+
return {
|
|
1494
|
+
mockFetch,
|
|
1495
|
+
mockStorage,
|
|
1496
|
+
mockStore,
|
|
1497
|
+
cleanup
|
|
1498
|
+
};
|
|
1499
|
+
}
|
|
1500
|
+
function createTestAuthHelpers(options = {}) {
|
|
1501
|
+
let context = null;
|
|
1502
|
+
return {
|
|
1503
|
+
/**
|
|
1504
|
+
* Setup function to call in beforeEach.
|
|
1505
|
+
*/
|
|
1506
|
+
setup: () => {
|
|
1507
|
+
context = setupTestAuth(options);
|
|
1508
|
+
return context;
|
|
1509
|
+
},
|
|
1510
|
+
/**
|
|
1511
|
+
* Cleanup function to call in afterEach.
|
|
1512
|
+
*/
|
|
1513
|
+
cleanup: () => {
|
|
1514
|
+
context?.cleanup();
|
|
1515
|
+
context = null;
|
|
1516
|
+
},
|
|
1517
|
+
/**
|
|
1518
|
+
* Get the current test context.
|
|
1519
|
+
*/
|
|
1520
|
+
getContext: () => {
|
|
1521
|
+
if (!context) {
|
|
1522
|
+
throw new Error("Test context not initialized. Call setup() first.");
|
|
1523
|
+
}
|
|
1524
|
+
return context;
|
|
1525
|
+
},
|
|
1526
|
+
/**
|
|
1527
|
+
* Get mocks directly (convenience accessors).
|
|
1528
|
+
*/
|
|
1529
|
+
getMockFetch: () => context?.mockFetch ?? null,
|
|
1530
|
+
getMockStorage: () => context?.mockStorage ?? null,
|
|
1531
|
+
getMockStore: () => context?.mockStore ?? null
|
|
1532
|
+
};
|
|
1533
|
+
}
|
|
1534
|
+
function quickSetupAuth(options = {}) {
|
|
1535
|
+
const { cleanup } = setupTestAuth(options);
|
|
1536
|
+
return cleanup;
|
|
1537
|
+
}
|
|
1538
|
+
function initAuthWithMocks(mockFetch, mockStorage, options = {}) {
|
|
1539
|
+
initAuth({
|
|
1540
|
+
baseUrl: options.baseUrl ?? "http://localhost:3000",
|
|
1541
|
+
storageKey: options.storageKey ?? "classic_auth",
|
|
1542
|
+
storage: mockStorage,
|
|
1543
|
+
fetch: mockFetch.fetch,
|
|
1544
|
+
...options
|
|
1545
|
+
});
|
|
1546
|
+
}
|
|
1547
|
+
function resetTestAuth(mockFetch, mockStorage, mockStore) {
|
|
1548
|
+
mockFetch?.reset();
|
|
1549
|
+
mockStorage?.reset();
|
|
1550
|
+
mockStore?.reset();
|
|
1551
|
+
}
|
|
1552
|
+
async function withTestAuth(fn, options = {}) {
|
|
1553
|
+
const context = setupTestAuth(options);
|
|
1554
|
+
try {
|
|
1555
|
+
return await fn(context);
|
|
1556
|
+
} finally {
|
|
1557
|
+
context.cleanup();
|
|
1558
|
+
}
|
|
1559
|
+
}
|
|
1560
|
+
async function withTestAuthEach(tests, options = {}) {
|
|
1561
|
+
const results = [];
|
|
1562
|
+
for (const test of tests) {
|
|
1563
|
+
const result = await withTestAuth(test, options);
|
|
1564
|
+
results.push(result);
|
|
1565
|
+
}
|
|
1566
|
+
return results;
|
|
1567
|
+
}
|
|
1568
|
+
|
|
1569
|
+
// src/testing/helpers/states.ts
|
|
1570
|
+
var authScenarios = {
|
|
1571
|
+
unauthenticated: {
|
|
1572
|
+
state: {
|
|
1573
|
+
accessToken: null,
|
|
1574
|
+
refreshToken: null,
|
|
1575
|
+
user: null,
|
|
1576
|
+
isAuthenticated: false
|
|
1577
|
+
},
|
|
1578
|
+
user: null,
|
|
1579
|
+
description: "User is not logged in"
|
|
1580
|
+
},
|
|
1581
|
+
authenticated: {
|
|
1582
|
+
state: {
|
|
1583
|
+
accessToken: mockAccessToken,
|
|
1584
|
+
refreshToken: mockRefreshToken,
|
|
1585
|
+
user: mockUser,
|
|
1586
|
+
isAuthenticated: true
|
|
1587
|
+
},
|
|
1588
|
+
user: mockUser,
|
|
1589
|
+
description: "Standard authenticated user"
|
|
1590
|
+
},
|
|
1591
|
+
admin: {
|
|
1592
|
+
state: {
|
|
1593
|
+
accessToken: mockAdminToken,
|
|
1594
|
+
refreshToken: mockRefreshToken,
|
|
1595
|
+
user: mockAdminUser,
|
|
1596
|
+
isAuthenticated: true
|
|
1597
|
+
},
|
|
1598
|
+
user: mockAdminUser,
|
|
1599
|
+
description: "Admin user with elevated permissions"
|
|
1600
|
+
},
|
|
1601
|
+
ssoUser: {
|
|
1602
|
+
state: {
|
|
1603
|
+
accessToken: mockAccessToken,
|
|
1604
|
+
refreshToken: mockRefreshToken,
|
|
1605
|
+
user: mockSSOUser,
|
|
1606
|
+
isAuthenticated: true
|
|
1607
|
+
},
|
|
1608
|
+
user: mockSSOUser,
|
|
1609
|
+
description: "SSO-authenticated user"
|
|
1610
|
+
},
|
|
1611
|
+
mfaEnabled: {
|
|
1612
|
+
state: {
|
|
1613
|
+
accessToken: mockAccessToken,
|
|
1614
|
+
refreshToken: mockRefreshToken,
|
|
1615
|
+
user: mockMFAUser,
|
|
1616
|
+
isAuthenticated: true
|
|
1617
|
+
},
|
|
1618
|
+
user: mockMFAUser,
|
|
1619
|
+
description: "User with MFA enabled"
|
|
1620
|
+
},
|
|
1621
|
+
unverifiedEmail: {
|
|
1622
|
+
state: {
|
|
1623
|
+
accessToken: mockAccessToken,
|
|
1624
|
+
refreshToken: mockRefreshToken,
|
|
1625
|
+
user: mockUnverifiedUser,
|
|
1626
|
+
isAuthenticated: true
|
|
1627
|
+
},
|
|
1628
|
+
user: mockUnverifiedUser,
|
|
1629
|
+
description: "User with unverified email"
|
|
1630
|
+
},
|
|
1631
|
+
inactive: {
|
|
1632
|
+
state: {
|
|
1633
|
+
accessToken: null,
|
|
1634
|
+
refreshToken: null,
|
|
1635
|
+
user: mockInactiveUser,
|
|
1636
|
+
isAuthenticated: false
|
|
1637
|
+
},
|
|
1638
|
+
user: mockInactiveUser,
|
|
1639
|
+
description: "Inactive/deactivated user"
|
|
1640
|
+
},
|
|
1641
|
+
expiredToken: {
|
|
1642
|
+
state: {
|
|
1643
|
+
accessToken: "expired.token.here",
|
|
1644
|
+
refreshToken: mockRefreshToken,
|
|
1645
|
+
user: mockUser,
|
|
1646
|
+
isAuthenticated: true
|
|
1647
|
+
// Still "authenticated" until refresh fails
|
|
1648
|
+
},
|
|
1649
|
+
user: mockUser,
|
|
1650
|
+
description: "User with expired access token"
|
|
1651
|
+
}
|
|
1652
|
+
};
|
|
1653
|
+
function applyScenario(store, scenario) {
|
|
1654
|
+
const scenarioData = authScenarios[scenario];
|
|
1655
|
+
store.setState(scenarioData.state);
|
|
1656
|
+
}
|
|
1657
|
+
function applyScenarios(store, scenarios) {
|
|
1658
|
+
scenarios.forEach((scenario) => applyScenario(store, scenario));
|
|
1659
|
+
}
|
|
1660
|
+
function getScenarioState(scenario) {
|
|
1661
|
+
return authScenarios[scenario];
|
|
1662
|
+
}
|
|
1663
|
+
function configureMFAFlow(mockFetch) {
|
|
1664
|
+
mockFetch.removeRoute("POST", "/auth/login");
|
|
1665
|
+
mockFetch.addRoute({
|
|
1666
|
+
method: "POST",
|
|
1667
|
+
path: "/auth/login",
|
|
1668
|
+
response: { data: mockMFARequired }
|
|
1669
|
+
});
|
|
1670
|
+
mockFetch.removeRoute("POST", "/auth/mfa/challenge");
|
|
1671
|
+
mockFetch.addRoute({
|
|
1672
|
+
method: "POST",
|
|
1673
|
+
path: "/auth/mfa/challenge",
|
|
1674
|
+
response: { data: mockLoginSuccess }
|
|
1675
|
+
});
|
|
1676
|
+
}
|
|
1677
|
+
function configureTokenRefresh(mockFetch, options = {}) {
|
|
1678
|
+
const { success = true, newTokens, errorMessage = "Refresh token expired" } = options;
|
|
1679
|
+
mockFetch.removeRoute("POST", "/auth/refresh");
|
|
1680
|
+
if (success) {
|
|
1681
|
+
const tokens = newTokens ?? createMockTokenPair();
|
|
1682
|
+
mockFetch.addRoute({
|
|
1683
|
+
method: "POST",
|
|
1684
|
+
path: "/auth/refresh",
|
|
1685
|
+
response: { data: tokens }
|
|
1686
|
+
});
|
|
1687
|
+
} else {
|
|
1688
|
+
mockFetch.addRoute({
|
|
1689
|
+
method: "POST",
|
|
1690
|
+
path: "/auth/refresh",
|
|
1691
|
+
status: 401,
|
|
1692
|
+
response: { error: { message: errorMessage } }
|
|
1693
|
+
});
|
|
1694
|
+
}
|
|
1695
|
+
}
|
|
1696
|
+
function configureSSOLogout(mockFetch, logoutUrl = "https://sso.example.com/logout") {
|
|
1697
|
+
mockFetch.removeRoute("POST", "/auth/logout");
|
|
1698
|
+
mockFetch.addRoute({
|
|
1699
|
+
method: "POST",
|
|
1700
|
+
path: "/auth/logout",
|
|
1701
|
+
response: { data: { ...mockSSOLogoutResponse, logoutUrl } }
|
|
1702
|
+
});
|
|
1703
|
+
}
|
|
1704
|
+
function configureAuthFailure(mockFetch, options = {}) {
|
|
1705
|
+
const { endpoint, status = 401, message = "Unauthorized" } = options;
|
|
1706
|
+
if (endpoint) {
|
|
1707
|
+
mockFetch.failEndpoint("GET", endpoint, status, message);
|
|
1708
|
+
mockFetch.failEndpoint("POST", endpoint, status, message);
|
|
1709
|
+
} else {
|
|
1710
|
+
mockFetch.failLogin(message);
|
|
1711
|
+
mockFetch.failEndpoint("GET", "/auth/profile", status, message);
|
|
1712
|
+
mockFetch.failEndpoint("GET", "/auth/sessions", status, message);
|
|
1713
|
+
}
|
|
1714
|
+
}
|
|
1715
|
+
function configureRateLimiting(mockFetch, endpoint) {
|
|
1716
|
+
mockFetch.failEndpoint("GET", endpoint, 429, "Too Many Requests");
|
|
1717
|
+
mockFetch.failEndpoint("POST", endpoint, 429, "Too Many Requests");
|
|
1718
|
+
}
|
|
1719
|
+
function createTestUserWithPermissions(permissions, roles = ["user"], overrides = {}) {
|
|
1720
|
+
return createMockUserWithRoles(roles, permissions, overrides);
|
|
1721
|
+
}
|
|
1722
|
+
function createTestAdminUser(overrides = {}) {
|
|
1723
|
+
return {
|
|
1724
|
+
...mockAdminUser,
|
|
1725
|
+
...overrides
|
|
1726
|
+
};
|
|
1727
|
+
}
|
|
1728
|
+
function createTestSSOUser(provider = "authentik", overrides = {}) {
|
|
1729
|
+
return {
|
|
1730
|
+
...mockSSOUser,
|
|
1731
|
+
authMethod: "oauth",
|
|
1732
|
+
ssoProfileUrl: `https://${provider}.example.com/profile/${mockSSOUser.id}`,
|
|
1733
|
+
...overrides
|
|
1734
|
+
};
|
|
1735
|
+
}
|
|
1736
|
+
function simulateLogin(store, user = mockUser, tokens) {
|
|
1737
|
+
const { accessToken, refreshToken } = tokens ?? createMockTokenPair(user.id);
|
|
1738
|
+
store.setAuth(accessToken, refreshToken, user);
|
|
1739
|
+
}
|
|
1740
|
+
function simulateLogout(store) {
|
|
1741
|
+
store.logout();
|
|
1742
|
+
}
|
|
1743
|
+
function simulateTokenRefresh(store, newTokens) {
|
|
1744
|
+
const tokens = newTokens ?? createMockTokenPair();
|
|
1745
|
+
store.updateTokens(tokens.accessToken, tokens.refreshToken);
|
|
1746
|
+
}
|
|
1747
|
+
function simulateProfileUpdate(store, updates) {
|
|
1748
|
+
const currentUser = store.user;
|
|
1749
|
+
if (currentUser) {
|
|
1750
|
+
store.updateUser({ ...currentUser, ...updates });
|
|
1751
|
+
}
|
|
1752
|
+
}
|
|
1753
|
+
function configureMFAEnrollmentFlow(mockFetch) {
|
|
1754
|
+
mockFetch.addRoute({
|
|
1755
|
+
method: "GET",
|
|
1756
|
+
path: "/auth/mfa/status",
|
|
1757
|
+
response: { enabled: false, methods: [] }
|
|
1758
|
+
});
|
|
1759
|
+
}
|
|
1760
|
+
function configurePasswordResetFlow(mockFetch) {
|
|
1761
|
+
mockFetch.addRoute({
|
|
1762
|
+
method: "POST",
|
|
1763
|
+
path: "/auth/forgot-password",
|
|
1764
|
+
response: { message: "Password reset email sent" }
|
|
1765
|
+
});
|
|
1766
|
+
mockFetch.addRoute({
|
|
1767
|
+
method: "POST",
|
|
1768
|
+
path: "/auth/reset-password",
|
|
1769
|
+
response: { message: "Password reset successful" }
|
|
1770
|
+
});
|
|
1771
|
+
}
|
|
1772
|
+
function configureSessionManagementFlow(mockFetch, sessionCount = 3) {
|
|
1773
|
+
const sessions = Array.from({ length: sessionCount }, (_, i) => ({
|
|
1774
|
+
id: `session-${i}`,
|
|
1775
|
+
deviceName: `Device ${i}`,
|
|
1776
|
+
browser: "Chrome",
|
|
1777
|
+
location: "Test Location",
|
|
1778
|
+
ipAddress: `192.168.1.${i}`,
|
|
1779
|
+
lastActivity: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1780
|
+
isCurrent: i === 0,
|
|
1781
|
+
isTrusted: i === 0,
|
|
1782
|
+
deviceFingerprint: `fp-${i}`,
|
|
1783
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1784
|
+
expiresAt: new Date(Date.now() + 6048e5).toISOString()
|
|
1785
|
+
}));
|
|
1786
|
+
mockFetch.removeRoute("GET", "/auth/sessions");
|
|
1787
|
+
mockFetch.addRoute({
|
|
1788
|
+
method: "GET",
|
|
1789
|
+
path: "/auth/sessions",
|
|
1790
|
+
response: { sessions, total: sessionCount }
|
|
1791
|
+
});
|
|
1792
|
+
}
|
|
1793
|
+
|
|
1794
|
+
// src/core/jwt.ts
|
|
1795
|
+
function decodeJWT(token) {
|
|
1796
|
+
try {
|
|
1797
|
+
const parts = token.split(".");
|
|
1798
|
+
if (parts.length !== 3) {
|
|
1799
|
+
return null;
|
|
1800
|
+
}
|
|
1801
|
+
const payload = parts[1];
|
|
1802
|
+
const decoded = atob(payload.replace(/-/g, "+").replace(/_/g, "/"));
|
|
1803
|
+
return JSON.parse(decoded);
|
|
1804
|
+
} catch {
|
|
1805
|
+
return null;
|
|
1806
|
+
}
|
|
1807
|
+
}
|
|
1808
|
+
function isTokenExpired(token) {
|
|
1809
|
+
const payload = decodeJWT(token);
|
|
1810
|
+
if (!payload || !payload.exp) {
|
|
1811
|
+
return true;
|
|
1812
|
+
}
|
|
1813
|
+
return payload.exp * 1e3 < Date.now();
|
|
1814
|
+
}
|
|
1815
|
+
|
|
1816
|
+
// src/testing/helpers/assertions.ts
|
|
1817
|
+
function assertAuthenticated(state, message = "Expected state to be authenticated") {
|
|
1818
|
+
if (!state.isAuthenticated) {
|
|
1819
|
+
throw new Error(message);
|
|
1820
|
+
}
|
|
1821
|
+
if (!state.accessToken) {
|
|
1822
|
+
throw new Error(`${message}: missing access token`);
|
|
1823
|
+
}
|
|
1824
|
+
if (!state.user) {
|
|
1825
|
+
throw new Error(`${message}: missing user`);
|
|
1826
|
+
}
|
|
1827
|
+
}
|
|
1828
|
+
function assertUnauthenticated(state, message = "Expected state to be unauthenticated") {
|
|
1829
|
+
if (state.isAuthenticated) {
|
|
1830
|
+
throw new Error(message);
|
|
1831
|
+
}
|
|
1832
|
+
if (state.accessToken) {
|
|
1833
|
+
throw new Error(`${message}: should not have access token`);
|
|
1834
|
+
}
|
|
1835
|
+
if (state.user) {
|
|
1836
|
+
throw new Error(`${message}: should not have user`);
|
|
1837
|
+
}
|
|
1838
|
+
}
|
|
1839
|
+
function assertHasUser(state, userId, message) {
|
|
1840
|
+
if (!state.user) {
|
|
1841
|
+
throw new Error(message ?? `Expected user with id ${userId} but no user found`);
|
|
1842
|
+
}
|
|
1843
|
+
if (state.user.id !== userId) {
|
|
1844
|
+
throw new Error(message ?? `Expected user id ${userId} but got ${state.user.id}`);
|
|
1845
|
+
}
|
|
1846
|
+
}
|
|
1847
|
+
function assertHasPermissions(user, permissions, message) {
|
|
1848
|
+
const missing = permissions.filter((p) => !user.permissions?.includes(p));
|
|
1849
|
+
if (missing.length > 0) {
|
|
1850
|
+
throw new Error(message ?? `User missing permissions: ${missing.join(", ")}`);
|
|
1851
|
+
}
|
|
1852
|
+
}
|
|
1853
|
+
function assertLacksPermissions(user, permissions, message) {
|
|
1854
|
+
const present = permissions.filter((p) => user.permissions?.includes(p));
|
|
1855
|
+
if (present.length > 0) {
|
|
1856
|
+
throw new Error(message ?? `User should not have permissions: ${present.join(", ")}`);
|
|
1857
|
+
}
|
|
1858
|
+
}
|
|
1859
|
+
function assertHasRoles(user, roles, message) {
|
|
1860
|
+
const missing = roles.filter((r) => !user.roles?.includes(r));
|
|
1861
|
+
if (missing.length > 0) {
|
|
1862
|
+
throw new Error(message ?? `User missing roles: ${missing.join(", ")}`);
|
|
1863
|
+
}
|
|
1864
|
+
}
|
|
1865
|
+
function assertLacksRoles(user, roles, message) {
|
|
1866
|
+
const present = roles.filter((r) => user.roles?.includes(r));
|
|
1867
|
+
if (present.length > 0) {
|
|
1868
|
+
throw new Error(message ?? `User should not have roles: ${present.join(", ")}`);
|
|
1869
|
+
}
|
|
1870
|
+
}
|
|
1871
|
+
function assertTokenValid(token, message = "Expected token to be valid") {
|
|
1872
|
+
if (isTokenExpired(token)) {
|
|
1873
|
+
throw new Error(message);
|
|
1874
|
+
}
|
|
1875
|
+
}
|
|
1876
|
+
function assertTokenExpired(token, message = "Expected token to be expired") {
|
|
1877
|
+
if (!isTokenExpired(token)) {
|
|
1878
|
+
throw new Error(message);
|
|
1879
|
+
}
|
|
1880
|
+
}
|
|
1881
|
+
function assertTokenHasClaims(token, claims, message) {
|
|
1882
|
+
const payload = decodeJWT(token);
|
|
1883
|
+
if (!payload) {
|
|
1884
|
+
throw new Error(message ?? "Could not decode token");
|
|
1885
|
+
}
|
|
1886
|
+
for (const [key, value] of Object.entries(claims)) {
|
|
1887
|
+
if (payload[key] !== value) {
|
|
1888
|
+
throw new Error(
|
|
1889
|
+
message ?? `Expected token claim ${key} to be ${value} but got ${payload[key]}`
|
|
1890
|
+
);
|
|
1891
|
+
}
|
|
1892
|
+
}
|
|
1893
|
+
}
|
|
1894
|
+
function assertTokenSubject(token, subject, message) {
|
|
1895
|
+
const payload = decodeJWT(token);
|
|
1896
|
+
if (!payload || payload.sub !== subject) {
|
|
1897
|
+
throw new Error(message ?? `Expected token subject ${subject} but got ${payload?.sub}`);
|
|
1898
|
+
}
|
|
1899
|
+
}
|
|
1900
|
+
function assertApiCalled(mockFetch, method, path, options = {}) {
|
|
1901
|
+
const calls = mockFetch.getCallsTo(path).filter((call) => (call.options.method ?? "GET").toUpperCase() === method.toUpperCase());
|
|
1902
|
+
if (calls.length === 0) {
|
|
1903
|
+
throw new Error(`Expected ${method} ${path} to be called`);
|
|
1904
|
+
}
|
|
1905
|
+
if (options.times !== void 0 && calls.length !== options.times) {
|
|
1906
|
+
throw new Error(
|
|
1907
|
+
`Expected ${method} ${path} to be called ${options.times} times but was called ${calls.length} times`
|
|
1908
|
+
);
|
|
1909
|
+
}
|
|
1910
|
+
if (options.body !== void 0) {
|
|
1911
|
+
const lastCall = calls[calls.length - 1];
|
|
1912
|
+
let body;
|
|
1913
|
+
try {
|
|
1914
|
+
body = JSON.parse(lastCall.options.body);
|
|
1915
|
+
} catch {
|
|
1916
|
+
body = lastCall.options.body;
|
|
1917
|
+
}
|
|
1918
|
+
const bodyStr = JSON.stringify(body);
|
|
1919
|
+
const expectedStr = JSON.stringify(options.body);
|
|
1920
|
+
if (bodyStr !== expectedStr) {
|
|
1921
|
+
throw new Error(
|
|
1922
|
+
`Expected ${method} ${path} to be called with body ${expectedStr} but got ${bodyStr}`
|
|
1923
|
+
);
|
|
1924
|
+
}
|
|
1925
|
+
}
|
|
1926
|
+
if (options.headers) {
|
|
1927
|
+
const lastCall = calls[calls.length - 1];
|
|
1928
|
+
const callHeaders = lastCall.options.headers;
|
|
1929
|
+
for (const [key, value] of Object.entries(options.headers)) {
|
|
1930
|
+
if (callHeaders?.[key] !== value) {
|
|
1931
|
+
throw new Error(`Expected ${method} ${path} to have header ${key}: ${value}`);
|
|
1932
|
+
}
|
|
1933
|
+
}
|
|
1934
|
+
}
|
|
1935
|
+
}
|
|
1936
|
+
function assertApiNotCalled(mockFetch, method, path, message) {
|
|
1937
|
+
const calls = mockFetch.getCallsTo(path).filter((call) => (call.options.method ?? "GET").toUpperCase() === method.toUpperCase());
|
|
1938
|
+
if (calls.length > 0) {
|
|
1939
|
+
throw new Error(
|
|
1940
|
+
message ?? `Expected ${method} ${path} not to be called but was called ${calls.length} times`
|
|
1941
|
+
);
|
|
1942
|
+
}
|
|
1943
|
+
}
|
|
1944
|
+
function assertApiCallOrder(mockFetch, expectedOrder) {
|
|
1945
|
+
const calls = mockFetch.getCalls();
|
|
1946
|
+
let lastIndex = -1;
|
|
1947
|
+
for (const expected of expectedOrder) {
|
|
1948
|
+
const index = calls.findIndex((call, i) => {
|
|
1949
|
+
if (i <= lastIndex) return false;
|
|
1950
|
+
const method = (call.options.method ?? "GET").toUpperCase();
|
|
1951
|
+
const path = new URL(call.url, "http://localhost").pathname;
|
|
1952
|
+
return method === expected.method.toUpperCase() && path.startsWith(expected.path);
|
|
1953
|
+
});
|
|
1954
|
+
if (index === -1) {
|
|
1955
|
+
throw new Error(
|
|
1956
|
+
`Expected ${expected.method} ${expected.path} to be called after previous calls`
|
|
1957
|
+
);
|
|
1958
|
+
}
|
|
1959
|
+
lastIndex = index;
|
|
1960
|
+
}
|
|
1961
|
+
}
|
|
1962
|
+
function assertStoreMethodCalled(store, method, options = {}) {
|
|
1963
|
+
const calls = store.getCallsFor(method);
|
|
1964
|
+
if (calls.length === 0) {
|
|
1965
|
+
throw new Error(`Expected store.${method}() to be called`);
|
|
1966
|
+
}
|
|
1967
|
+
if (options.times !== void 0 && calls.length !== options.times) {
|
|
1968
|
+
throw new Error(
|
|
1969
|
+
`Expected store.${method}() to be called ${options.times} times but was called ${calls.length} times`
|
|
1970
|
+
);
|
|
1971
|
+
}
|
|
1972
|
+
if (options.args !== void 0) {
|
|
1973
|
+
const lastCall = calls[calls.length - 1];
|
|
1974
|
+
const argsStr = JSON.stringify(lastCall.args);
|
|
1975
|
+
const expectedStr = JSON.stringify(options.args);
|
|
1976
|
+
if (argsStr !== expectedStr) {
|
|
1977
|
+
throw new Error(
|
|
1978
|
+
`Expected store.${method}() to be called with ${expectedStr} but got ${argsStr}`
|
|
1979
|
+
);
|
|
1980
|
+
}
|
|
1981
|
+
}
|
|
1982
|
+
}
|
|
1983
|
+
function assertStoreMethodNotCalled(store, method, message) {
|
|
1984
|
+
const calls = store.getCallsFor(method);
|
|
1985
|
+
if (calls.length > 0) {
|
|
1986
|
+
throw new Error(
|
|
1987
|
+
message ?? `Expected store.${method}() not to be called but was called ${calls.length} times`
|
|
1988
|
+
);
|
|
1989
|
+
}
|
|
1990
|
+
}
|
|
1991
|
+
function assertRequiresMFA(response, message = "Expected response to require MFA") {
|
|
1992
|
+
if (!response.requiresMFA && !response.mfaRequired) {
|
|
1993
|
+
throw new Error(message);
|
|
1994
|
+
}
|
|
1995
|
+
if (!response.mfaToken) {
|
|
1996
|
+
throw new Error(`${message}: missing MFA token`);
|
|
1997
|
+
}
|
|
1998
|
+
}
|
|
1999
|
+
function assertNoMFARequired(response, message = "Expected response not to require MFA") {
|
|
2000
|
+
if (response.requiresMFA || response.mfaRequired) {
|
|
2001
|
+
throw new Error(message);
|
|
2002
|
+
}
|
|
2003
|
+
if (!response.accessToken) {
|
|
2004
|
+
throw new Error(`${message}: missing access token`);
|
|
2005
|
+
}
|
|
2006
|
+
}
|
|
2007
|
+
function assertEmailVerified(user, message = "Expected user email to be verified") {
|
|
2008
|
+
if (!user.emailVerified) {
|
|
2009
|
+
throw new Error(message);
|
|
2010
|
+
}
|
|
2011
|
+
}
|
|
2012
|
+
function assertEmailNotVerified(user, message = "Expected user email not to be verified") {
|
|
2013
|
+
if (user.emailVerified) {
|
|
2014
|
+
throw new Error(message);
|
|
2015
|
+
}
|
|
2016
|
+
}
|
|
2017
|
+
function assertUserActive(user, message = "Expected user to be active") {
|
|
2018
|
+
if (!user.isActive) {
|
|
2019
|
+
throw new Error(message);
|
|
2020
|
+
}
|
|
2021
|
+
}
|
|
2022
|
+
function assertUserInactive(user, message = "Expected user to be inactive") {
|
|
2023
|
+
if (user.isActive) {
|
|
2024
|
+
throw new Error(message);
|
|
2025
|
+
}
|
|
2026
|
+
}
|
|
2027
|
+
function assertAuthMethod(user, method, message) {
|
|
2028
|
+
if (user.authMethod !== method) {
|
|
2029
|
+
throw new Error(message ?? `Expected user auth method ${method} but got ${user.authMethod}`);
|
|
2030
|
+
}
|
|
2031
|
+
}
|
|
2032
|
+
|
|
2033
|
+
export { MockAuthStore, MockFetchInstance, MockStorageAdapter, applyScenario, applyScenarios, assertApiCallOrder, assertApiCalled, assertApiNotCalled, assertAuthMethod, assertAuthenticated, assertEmailNotVerified, assertEmailVerified, assertHasPermissions, assertHasRoles, assertHasUser, assertLacksPermissions, assertLacksRoles, assertNoMFARequired, assertRequiresMFA, assertStoreMethodCalled, assertStoreMethodNotCalled, assertTokenExpired, assertTokenHasClaims, assertTokenSubject, assertTokenValid, assertUnauthenticated, assertUserActive, assertUserInactive, authScenarios, configureAuthFailure, configureMFAEnrollmentFlow, configureMFAFlow, configurePasswordResetFlow, configureRateLimiting, configureSSOLogout, configureSessionManagementFlow, configureTokenRefresh, createAuthenticatedMockStore, createExpiredMockJWT, createMockAuthActions, createMockAuthStore, createMockCurrentUser, createMockDevice, createMockFetch, createMockFetchWithRoutes, createMockIsAuthenticated, createMockJWT, createMockLoginSuccess, createMockMFARequired, createMockMFAToken, createMockSecurityEvent, createMockSession, createMockStorage, createMockStorageWithAuth, createMockTokenPair, createMockUser, createMockUserWithAuthMethod, createMockUserWithRoles, createTestAdminUser, createTestAuthHelpers, createTestSSOUser, createTestUserWithPermissions, getScenarioState, initAuthWithMocks, mockAccessToken, mockAdminLoginSuccess, mockAdminToken, mockAdminUser, mockApiKey, mockApiKeys, mockCurrentSession, mockDevices, mockExpiredToken, mockInactiveUser, mockLinkedAccount, mockLoginSuccess, mockLogoutSuccess, mockMFARequired, mockMFARequiredLegacy, mockMFASetup, mockMFAStatusDisabled, mockMFAStatusEnabled, mockMFAToken, mockMFAUser, mockOtherSession, mockRefreshToken, mockRegisterRequiresVerification, mockRegisterSuccess, mockSSOLoginSuccess, mockSSOLogoutResponse, mockSSOUser, mockSecurityEventLogin, mockSecurityEventPasswordChange, mockSecurityEventSuspicious, mockSecurityEvents, mockSessionToken, mockSessions, mockTrustedDevice, mockUntrustedDevice, mockUnverifiedUser, mockUser, mockUserPreferences, quickSetupAuth, resetTestAuth, setupTestAuth, simulateLogin, simulateLogout, simulateProfileUpdate, simulateTokenRefresh, withTestAuth, withTestAuthEach };
|