@classic-homes/auth 0.1.22 → 0.1.24
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
package/README.md
CHANGED
|
@@ -5,8 +5,9 @@ Framework-agnostic authentication core with Svelte bindings for the Classic Them
|
|
|
5
5
|
## Features
|
|
6
6
|
|
|
7
7
|
- JWT-based authentication with automatic token refresh
|
|
8
|
-
- SSO (Single Sign-On) support with configurable providers
|
|
9
|
-
- Multi-factor authentication (MFA/TOTP) support
|
|
8
|
+
- SSO (Single Sign-On) support with configurable providers and logout
|
|
9
|
+
- Multi-factor authentication (MFA/TOTP) support with type guards
|
|
10
|
+
- Auto-set auth state on successful login
|
|
10
11
|
- Pluggable storage adapter (localStorage, sessionStorage, or custom)
|
|
11
12
|
- Svelte reactive stores for authentication state
|
|
12
13
|
- Route guards for protected pages
|
|
@@ -26,6 +27,8 @@ In your app's entry point (e.g., `hooks.client.ts` for SvelteKit):
|
|
|
26
27
|
|
|
27
28
|
```typescript
|
|
28
29
|
import { initAuth } from '@classic-homes/auth';
|
|
30
|
+
import { goto } from '$app/navigation';
|
|
31
|
+
import { base } from '$app/paths';
|
|
29
32
|
|
|
30
33
|
initAuth({
|
|
31
34
|
baseUrl: 'https://api.example.com',
|
|
@@ -34,47 +37,163 @@ initAuth({
|
|
|
34
37
|
setItem: (key, value) => localStorage.setItem(key, value),
|
|
35
38
|
removeItem: (key) => localStorage.removeItem(key),
|
|
36
39
|
},
|
|
40
|
+
// SSO configuration (optional)
|
|
41
|
+
sso: {
|
|
42
|
+
enabled: true,
|
|
43
|
+
provider: 'authentik',
|
|
44
|
+
},
|
|
45
|
+
// Callback when auth errors occur
|
|
37
46
|
onAuthError: (error) => {
|
|
38
47
|
console.error('Auth error:', error);
|
|
39
48
|
},
|
|
49
|
+
// Callback when tokens are refreshed
|
|
50
|
+
onTokenRefresh: (tokens) => {
|
|
51
|
+
console.log('Tokens refreshed');
|
|
52
|
+
},
|
|
53
|
+
// Callback when user is logged out
|
|
54
|
+
onLogout: () => {
|
|
55
|
+
goto(`${base}/auth/login`);
|
|
56
|
+
},
|
|
40
57
|
});
|
|
41
58
|
```
|
|
42
59
|
|
|
43
|
-
### 2.
|
|
60
|
+
### 2. Login with Auto-Set Auth
|
|
61
|
+
|
|
62
|
+
The `authService.login()` method automatically sets the auth state on successful login:
|
|
63
|
+
|
|
64
|
+
```typescript
|
|
65
|
+
import {
|
|
66
|
+
authService,
|
|
67
|
+
isMfaChallengeResponse,
|
|
68
|
+
getMfaToken,
|
|
69
|
+
getAvailableMethods,
|
|
70
|
+
} from '@classic-homes/auth/core';
|
|
71
|
+
import { goto } from '$app/navigation';
|
|
72
|
+
|
|
73
|
+
async function handleLogin(email: string, password: string) {
|
|
74
|
+
const response = await authService.login({
|
|
75
|
+
username: email,
|
|
76
|
+
password: password,
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
// Check if MFA is required
|
|
80
|
+
if (isMfaChallengeResponse(response)) {
|
|
81
|
+
const mfaToken = getMfaToken(response);
|
|
82
|
+
const methods = getAvailableMethods(response);
|
|
83
|
+
// Redirect to MFA challenge page
|
|
84
|
+
goto(`/auth/mfa-challenge?token=${mfaToken}&methods=${methods.join(',')}`);
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Auth state is automatically set - redirect to dashboard
|
|
89
|
+
goto('/dashboard');
|
|
90
|
+
}
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
To disable auto-set auth (for manual control):
|
|
94
|
+
|
|
95
|
+
```typescript
|
|
96
|
+
const response = await authService.login(credentials, { autoSetAuth: false });
|
|
97
|
+
// Manually set auth state
|
|
98
|
+
authActions.setAuth(response.accessToken, response.refreshToken, response.user);
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
### 3. Use the Auth Store (Svelte)
|
|
44
102
|
|
|
45
103
|
```svelte
|
|
46
104
|
<script lang="ts">
|
|
47
|
-
import { authStore, authActions } from '@classic-homes/auth/svelte';
|
|
105
|
+
import { authStore, authActions, isAuthenticated, currentUser } from '@classic-homes/auth/svelte';
|
|
48
106
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
password: 'password123',
|
|
53
|
-
});
|
|
54
|
-
}
|
|
107
|
+
// Using derived stores
|
|
108
|
+
// $isAuthenticated - boolean
|
|
109
|
+
// $currentUser - User | null
|
|
55
110
|
|
|
56
111
|
async function handleLogout() {
|
|
57
|
-
|
|
112
|
+
// SSO-aware logout
|
|
113
|
+
const result = await authActions.logoutWithSSO();
|
|
114
|
+
if (result.ssoLogoutUrl) {
|
|
115
|
+
// Redirect to SSO provider logout
|
|
116
|
+
window.location.href = result.ssoLogoutUrl;
|
|
117
|
+
} else {
|
|
118
|
+
goto('/auth/login');
|
|
119
|
+
}
|
|
58
120
|
}
|
|
59
121
|
</script>
|
|
60
122
|
|
|
61
|
-
{#if $
|
|
62
|
-
<p>Welcome, {$
|
|
123
|
+
{#if $isAuthenticated}
|
|
124
|
+
<p>Welcome, {$currentUser?.firstName}</p>
|
|
63
125
|
<button onclick={handleLogout}>Logout</button>
|
|
64
126
|
{:else}
|
|
65
|
-
<
|
|
127
|
+
<a href="/auth/login">Login</a>
|
|
66
128
|
{/if}
|
|
67
129
|
```
|
|
68
130
|
|
|
69
|
-
###
|
|
131
|
+
### 4. SSO Login with Redirect URLs
|
|
132
|
+
|
|
133
|
+
```typescript
|
|
134
|
+
import { authService } from '@classic-homes/auth/core';
|
|
135
|
+
|
|
136
|
+
function handleSSOLogin(redirectUrl: string) {
|
|
137
|
+
// Specify where to redirect after SSO callback
|
|
138
|
+
authService.initiateSSOLogin({
|
|
139
|
+
callbackUrl: `${window.location.origin}/auth/sso-callback`,
|
|
140
|
+
redirectUrl: redirectUrl, // Final destination after auth
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
### 5. MFA Challenge Verification
|
|
146
|
+
|
|
147
|
+
```typescript
|
|
148
|
+
import { authService } from '@classic-homes/auth/core';
|
|
149
|
+
|
|
150
|
+
async function handleMFAVerify(mfaToken: string, code: string, trustDevice: boolean) {
|
|
151
|
+
// Auto-sets auth state on success
|
|
152
|
+
const response = await authService.verifyMFAChallenge({
|
|
153
|
+
mfaToken,
|
|
154
|
+
code,
|
|
155
|
+
method: 'totp',
|
|
156
|
+
trustDevice,
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
// Auth state is automatically set - redirect to dashboard
|
|
160
|
+
goto('/dashboard');
|
|
161
|
+
}
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
### 6. Protect Routes
|
|
70
165
|
|
|
71
166
|
```typescript
|
|
72
167
|
// src/routes/dashboard/+page.ts
|
|
73
|
-
import {
|
|
168
|
+
import { checkAuth, requireRole } from '@classic-homes/auth/svelte';
|
|
169
|
+
import { redirect } from '@sveltejs/kit';
|
|
170
|
+
import { browser } from '$app/environment';
|
|
171
|
+
|
|
172
|
+
export function load({ url }) {
|
|
173
|
+
if (browser) {
|
|
174
|
+
const result = checkAuth();
|
|
175
|
+
if (!result.allowed) {
|
|
176
|
+
throw redirect(302, `/auth/login?redirect=${encodeURIComponent(url.pathname)}`);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
return {};
|
|
180
|
+
}
|
|
74
181
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
182
|
+
// For role-based access:
|
|
183
|
+
export function load({ url }) {
|
|
184
|
+
if (browser) {
|
|
185
|
+
const result = checkAuth({ roles: ['admin', 'manager'] });
|
|
186
|
+
if (!result.allowed) {
|
|
187
|
+
if (result.reason === 'not_authenticated') {
|
|
188
|
+
throw redirect(302, `/auth/login?redirect=${encodeURIComponent(url.pathname)}`);
|
|
189
|
+
}
|
|
190
|
+
if (result.reason === 'missing_role') {
|
|
191
|
+
throw redirect(302, '/unauthorized');
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
return {};
|
|
196
|
+
}
|
|
78
197
|
```
|
|
79
198
|
|
|
80
199
|
## API Reference
|
|
@@ -85,7 +204,8 @@ export const load = authGuard({
|
|
|
85
204
|
import {
|
|
86
205
|
// Initialization
|
|
87
206
|
initAuth,
|
|
88
|
-
|
|
207
|
+
getConfig,
|
|
208
|
+
isInitialized,
|
|
89
209
|
|
|
90
210
|
// Service
|
|
91
211
|
authService,
|
|
@@ -94,13 +214,28 @@ import {
|
|
|
94
214
|
// API
|
|
95
215
|
authApi,
|
|
96
216
|
|
|
217
|
+
// MFA Guards
|
|
218
|
+
isMfaChallengeResponse,
|
|
219
|
+
isLoginSuccessResponse,
|
|
220
|
+
getMfaToken,
|
|
221
|
+
getAvailableMethods,
|
|
222
|
+
|
|
223
|
+
// JWT Utilities
|
|
224
|
+
decodeJWT,
|
|
225
|
+
isTokenExpired,
|
|
226
|
+
getTokenRemainingTime,
|
|
227
|
+
|
|
97
228
|
// Types
|
|
98
229
|
type User,
|
|
99
|
-
type
|
|
230
|
+
type AuthState,
|
|
100
231
|
type LoginCredentials,
|
|
232
|
+
type LoginResponse,
|
|
233
|
+
type LogoutResponse,
|
|
101
234
|
type RegisterData,
|
|
102
235
|
type AuthConfig,
|
|
103
|
-
|
|
236
|
+
type LoginOptions,
|
|
237
|
+
type MFAVerifyOptions,
|
|
238
|
+
} from '@classic-homes/auth/core';
|
|
104
239
|
```
|
|
105
240
|
|
|
106
241
|
### Svelte Exports
|
|
@@ -116,8 +251,12 @@ import {
|
|
|
116
251
|
authActions,
|
|
117
252
|
|
|
118
253
|
// Guards
|
|
119
|
-
|
|
120
|
-
|
|
254
|
+
checkAuth,
|
|
255
|
+
createAuthGuard,
|
|
256
|
+
requireAuth,
|
|
257
|
+
requireRole,
|
|
258
|
+
requirePermission,
|
|
259
|
+
protectedLoad,
|
|
121
260
|
} from '@classic-homes/auth/svelte';
|
|
122
261
|
```
|
|
123
262
|
|
|
@@ -128,26 +267,30 @@ interface AuthConfig {
|
|
|
128
267
|
/** Base URL for the auth API */
|
|
129
268
|
baseUrl: string;
|
|
130
269
|
|
|
131
|
-
/**
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
270
|
+
/** Custom fetch implementation (useful for SSR or testing) */
|
|
271
|
+
fetch?: typeof fetch;
|
|
272
|
+
|
|
273
|
+
/** Storage adapter for token persistence */
|
|
274
|
+
storage?: StorageAdapter;
|
|
275
|
+
|
|
276
|
+
/** Storage key prefix for auth data */
|
|
277
|
+
storageKey?: string;
|
|
137
278
|
|
|
138
279
|
/** SSO configuration */
|
|
139
280
|
sso?: {
|
|
140
281
|
enabled: boolean;
|
|
141
282
|
provider: string;
|
|
142
283
|
authorizeUrl?: string;
|
|
143
|
-
tokenUrl?: string;
|
|
144
284
|
};
|
|
145
285
|
|
|
146
286
|
/** Callback when auth errors occur */
|
|
147
287
|
onAuthError?: (error: Error) => void;
|
|
148
288
|
|
|
149
|
-
/**
|
|
150
|
-
|
|
289
|
+
/** Callback when tokens are refreshed */
|
|
290
|
+
onTokenRefresh?: (tokens: { accessToken: string; refreshToken: string }) => void;
|
|
291
|
+
|
|
292
|
+
/** Callback when user is logged out */
|
|
293
|
+
onLogout?: () => void;
|
|
151
294
|
}
|
|
152
295
|
```
|
|
153
296
|
|
|
@@ -156,69 +299,47 @@ interface AuthConfig {
|
|
|
156
299
|
The `authActions` object provides methods for authentication operations:
|
|
157
300
|
|
|
158
301
|
```typescript
|
|
159
|
-
//
|
|
160
|
-
|
|
302
|
+
// Set auth data after login
|
|
303
|
+
authActions.setAuth(accessToken, refreshToken, user, sessionToken);
|
|
161
304
|
|
|
162
|
-
//
|
|
163
|
-
|
|
305
|
+
// Update tokens after refresh
|
|
306
|
+
authActions.updateTokens(accessToken, refreshToken);
|
|
164
307
|
|
|
165
|
-
//
|
|
166
|
-
|
|
308
|
+
// Update user profile
|
|
309
|
+
authActions.updateUser(user);
|
|
167
310
|
|
|
168
|
-
//
|
|
169
|
-
|
|
311
|
+
// Clear auth state (local logout)
|
|
312
|
+
authActions.logout();
|
|
170
313
|
|
|
171
|
-
//
|
|
172
|
-
await authActions.
|
|
314
|
+
// SSO-aware logout (calls API, returns SSO logout URL if applicable)
|
|
315
|
+
const result = await authActions.logoutWithSSO();
|
|
316
|
+
if (result.ssoLogoutUrl) {
|
|
317
|
+
window.location.href = result.ssoLogoutUrl;
|
|
318
|
+
}
|
|
173
319
|
|
|
174
|
-
//
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
320
|
+
// Permission and role checks
|
|
321
|
+
authActions.hasPermission('users:read');
|
|
322
|
+
authActions.hasRole('admin');
|
|
323
|
+
authActions.hasAnyRole(['admin', 'manager']);
|
|
324
|
+
authActions.hasAllRoles(['admin', 'manager']);
|
|
325
|
+
authActions.hasAnyPermission(['users:read', 'users:write']);
|
|
326
|
+
authActions.hasAllPermissions(['users:read', 'users:write']);
|
|
178
327
|
|
|
179
|
-
//
|
|
180
|
-
|
|
181
|
-
await authActions.handleSSOCallback(code);
|
|
328
|
+
// Reload auth from storage
|
|
329
|
+
authActions.rehydrate();
|
|
182
330
|
```
|
|
183
331
|
|
|
184
332
|
## Auth Store State
|
|
185
333
|
|
|
186
334
|
```typescript
|
|
187
335
|
interface AuthState {
|
|
336
|
+
accessToken: string | null;
|
|
337
|
+
refreshToken: string | null;
|
|
188
338
|
user: User | null;
|
|
189
|
-
tokens: AuthTokens | null;
|
|
190
339
|
isAuthenticated: boolean;
|
|
191
|
-
isLoading: boolean;
|
|
192
|
-
error: string | null;
|
|
193
|
-
mfaRequired: boolean;
|
|
194
|
-
mfaToken: string | null;
|
|
195
340
|
}
|
|
196
341
|
```
|
|
197
342
|
|
|
198
|
-
## Route Guards
|
|
199
|
-
|
|
200
|
-
### Basic Auth Guard
|
|
201
|
-
|
|
202
|
-
```typescript
|
|
203
|
-
import { authGuard } from '@classic-homes/auth/svelte';
|
|
204
|
-
|
|
205
|
-
export const load = authGuard({
|
|
206
|
-
redirectTo: '/login',
|
|
207
|
-
returnUrl: true, // Append ?returnUrl= to redirect
|
|
208
|
-
});
|
|
209
|
-
```
|
|
210
|
-
|
|
211
|
-
### Role-Based Guard
|
|
212
|
-
|
|
213
|
-
```typescript
|
|
214
|
-
import { roleGuard } from '@classic-homes/auth/svelte';
|
|
215
|
-
|
|
216
|
-
export const load = roleGuard({
|
|
217
|
-
roles: ['admin', 'manager'],
|
|
218
|
-
redirectTo: '/unauthorized',
|
|
219
|
-
});
|
|
220
|
-
```
|
|
221
|
-
|
|
222
343
|
## Using with @classic-homes/theme-svelte
|
|
223
344
|
|
|
224
345
|
The auth package integrates with the form validation from `@classic-homes/theme-svelte`:
|
|
@@ -226,7 +347,8 @@ The auth package integrates with the form validation from `@classic-homes/theme-
|
|
|
226
347
|
```svelte
|
|
227
348
|
<script lang="ts">
|
|
228
349
|
import { useForm, loginSchema } from '@classic-homes/theme-svelte';
|
|
229
|
-
import {
|
|
350
|
+
import { authService, isMfaChallengeResponse, getMfaToken } from '@classic-homes/auth/core';
|
|
351
|
+
import { goto } from '$app/navigation';
|
|
230
352
|
|
|
231
353
|
const form = useForm({
|
|
232
354
|
schema: loginSchema,
|
|
@@ -236,7 +358,19 @@ The auth package integrates with the form validation from `@classic-homes/theme-
|
|
|
236
358
|
rememberMe: false,
|
|
237
359
|
},
|
|
238
360
|
onSubmit: async (data) => {
|
|
239
|
-
await
|
|
361
|
+
const response = await authService.login({
|
|
362
|
+
username: data.email,
|
|
363
|
+
password: data.password,
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
if (isMfaChallengeResponse(response)) {
|
|
367
|
+
const mfaToken = getMfaToken(response);
|
|
368
|
+
goto(`/auth/mfa-challenge?token=${mfaToken}`);
|
|
369
|
+
return;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
// Auth state automatically set
|
|
373
|
+
goto('/dashboard');
|
|
240
374
|
},
|
|
241
375
|
});
|
|
242
376
|
</script>
|
|
@@ -250,6 +384,277 @@ The auth package integrates with the form validation from `@classic-homes/theme-
|
|
|
250
384
|
</form>
|
|
251
385
|
```
|
|
252
386
|
|
|
387
|
+
## Automatic Token Refresh
|
|
388
|
+
|
|
389
|
+
Token refresh happens automatically when:
|
|
390
|
+
|
|
391
|
+
- An API request returns 401 Unauthorized
|
|
392
|
+
- The refresh token is valid
|
|
393
|
+
|
|
394
|
+
The Svelte store is automatically updated when tokens are refreshed, so your UI stays in sync.
|
|
395
|
+
|
|
396
|
+
## Testing Utilities
|
|
397
|
+
|
|
398
|
+
The auth package includes comprehensive testing utilities for unit and integration tests.
|
|
399
|
+
|
|
400
|
+
### Installation
|
|
401
|
+
|
|
402
|
+
```bash
|
|
403
|
+
# The testing utilities are included in the main package
|
|
404
|
+
npm install @classic-homes/auth
|
|
405
|
+
```
|
|
406
|
+
|
|
407
|
+
### Quick Start
|
|
408
|
+
|
|
409
|
+
```typescript
|
|
410
|
+
import { describe, it, beforeEach, afterEach, expect } from 'vitest';
|
|
411
|
+
import {
|
|
412
|
+
setupTestAuth,
|
|
413
|
+
mockUser,
|
|
414
|
+
configureMFAFlow,
|
|
415
|
+
assertAuthenticated,
|
|
416
|
+
} from '@classic-homes/auth/testing';
|
|
417
|
+
import { authService, isMfaChallengeResponse } from '@classic-homes/auth/core';
|
|
418
|
+
|
|
419
|
+
describe('Login Flow', () => {
|
|
420
|
+
let cleanup: () => void;
|
|
421
|
+
let mockFetch;
|
|
422
|
+
|
|
423
|
+
beforeEach(() => {
|
|
424
|
+
const ctx = setupTestAuth();
|
|
425
|
+
cleanup = ctx.cleanup;
|
|
426
|
+
mockFetch = ctx.mockFetch;
|
|
427
|
+
});
|
|
428
|
+
|
|
429
|
+
afterEach(() => cleanup());
|
|
430
|
+
|
|
431
|
+
it('handles successful login', async () => {
|
|
432
|
+
const response = await authService.login({
|
|
433
|
+
username: 'test@example.com',
|
|
434
|
+
password: 'password',
|
|
435
|
+
});
|
|
436
|
+
|
|
437
|
+
expect(response.user).toMatchObject(mockUser);
|
|
438
|
+
mockFetch.assertCalled('/auth/login');
|
|
439
|
+
});
|
|
440
|
+
|
|
441
|
+
it('handles MFA flow', async () => {
|
|
442
|
+
configureMFAFlow(mockFetch);
|
|
443
|
+
|
|
444
|
+
const response = await authService.login({
|
|
445
|
+
username: 'test@example.com',
|
|
446
|
+
password: 'password',
|
|
447
|
+
});
|
|
448
|
+
|
|
449
|
+
expect(isMfaChallengeResponse(response)).toBe(true);
|
|
450
|
+
});
|
|
451
|
+
});
|
|
452
|
+
```
|
|
453
|
+
|
|
454
|
+
### Testing Exports
|
|
455
|
+
|
|
456
|
+
```typescript
|
|
457
|
+
import {
|
|
458
|
+
// Fixtures - Pre-defined test data
|
|
459
|
+
mockUser,
|
|
460
|
+
mockAdminUser,
|
|
461
|
+
mockSSOUser,
|
|
462
|
+
mockMFAUser,
|
|
463
|
+
mockAccessToken,
|
|
464
|
+
mockRefreshToken,
|
|
465
|
+
mockLoginSuccess,
|
|
466
|
+
mockMFARequired,
|
|
467
|
+
createMockUser,
|
|
468
|
+
createMockTokenPair,
|
|
469
|
+
createMockLoginSuccess,
|
|
470
|
+
|
|
471
|
+
// Mocks - Test doubles for dependencies
|
|
472
|
+
MockStorageAdapter,
|
|
473
|
+
MockFetchInstance,
|
|
474
|
+
MockAuthStore,
|
|
475
|
+
createMockStorage,
|
|
476
|
+
createMockFetch,
|
|
477
|
+
createMockAuthStore,
|
|
478
|
+
|
|
479
|
+
// Setup Helpers
|
|
480
|
+
setupTestAuth,
|
|
481
|
+
createTestAuthHelpers,
|
|
482
|
+
quickSetupAuth,
|
|
483
|
+
withTestAuth,
|
|
484
|
+
|
|
485
|
+
// State Simulation
|
|
486
|
+
authScenarios,
|
|
487
|
+
applyScenario,
|
|
488
|
+
configureMFAFlow,
|
|
489
|
+
configureTokenRefresh,
|
|
490
|
+
configureSSOLogout,
|
|
491
|
+
simulateLogin,
|
|
492
|
+
simulateLogout,
|
|
493
|
+
|
|
494
|
+
// Assertions
|
|
495
|
+
assertAuthenticated,
|
|
496
|
+
assertUnauthenticated,
|
|
497
|
+
assertHasPermissions,
|
|
498
|
+
assertHasRoles,
|
|
499
|
+
assertTokenValid,
|
|
500
|
+
assertApiCalled,
|
|
501
|
+
assertStoreMethodCalled,
|
|
502
|
+
assertRequiresMFA,
|
|
503
|
+
} from '@classic-homes/auth/testing';
|
|
504
|
+
```
|
|
505
|
+
|
|
506
|
+
### Mock Fetch
|
|
507
|
+
|
|
508
|
+
The `MockFetchInstance` provides a configurable mock fetch with pre-defined auth routes:
|
|
509
|
+
|
|
510
|
+
```typescript
|
|
511
|
+
const ctx = setupTestAuth();
|
|
512
|
+
const { mockFetch } = ctx;
|
|
513
|
+
|
|
514
|
+
// Default routes are pre-configured for all auth endpoints
|
|
515
|
+
|
|
516
|
+
// Customize responses
|
|
517
|
+
mockFetch.requireMFA(); // Login requires MFA
|
|
518
|
+
mockFetch.failLogin('Invalid credentials'); // Login fails
|
|
519
|
+
mockFetch.enableSSOLogout(); // Logout returns SSO URL
|
|
520
|
+
|
|
521
|
+
// Add custom routes
|
|
522
|
+
mockFetch.addRoute({
|
|
523
|
+
method: 'GET',
|
|
524
|
+
path: '/custom/endpoint',
|
|
525
|
+
response: { data: 'custom response' },
|
|
526
|
+
});
|
|
527
|
+
|
|
528
|
+
// Fail specific endpoints
|
|
529
|
+
mockFetch.failEndpoint('GET', '/auth/profile', 403, 'Forbidden');
|
|
530
|
+
|
|
531
|
+
// Check call history
|
|
532
|
+
expect(mockFetch.wasCalled('/auth/login')).toBe(true);
|
|
533
|
+
mockFetch.assertCalled('/auth/profile');
|
|
534
|
+
mockFetch.assertNotCalled('/auth/logout');
|
|
535
|
+
```
|
|
536
|
+
|
|
537
|
+
### Mock Auth Store
|
|
538
|
+
|
|
539
|
+
The `MockAuthStore` mimics the Svelte auth store:
|
|
540
|
+
|
|
541
|
+
```typescript
|
|
542
|
+
const store = createMockAuthStore();
|
|
543
|
+
|
|
544
|
+
// Simulate states
|
|
545
|
+
store.simulateAuthenticated(mockAdminUser);
|
|
546
|
+
store.simulateUnauthenticated();
|
|
547
|
+
|
|
548
|
+
// Direct state manipulation
|
|
549
|
+
store.setState({ isAuthenticated: true, user: mockUser });
|
|
550
|
+
|
|
551
|
+
// Check method calls
|
|
552
|
+
store.assertMethodCalled('setAuth');
|
|
553
|
+
store.assertMethodNotCalled('logout');
|
|
554
|
+
|
|
555
|
+
// Get call history
|
|
556
|
+
const calls = store.getCallsFor('setAuth');
|
|
557
|
+
```
|
|
558
|
+
|
|
559
|
+
### Pre-defined Scenarios
|
|
560
|
+
|
|
561
|
+
Apply common auth scenarios for testing:
|
|
562
|
+
|
|
563
|
+
```typescript
|
|
564
|
+
import { authScenarios, applyScenario } from '@classic-homes/auth/testing';
|
|
565
|
+
|
|
566
|
+
// Available scenarios:
|
|
567
|
+
// - 'unauthenticated'
|
|
568
|
+
// - 'authenticated'
|
|
569
|
+
// - 'admin'
|
|
570
|
+
// - 'ssoUser'
|
|
571
|
+
// - 'mfaEnabled'
|
|
572
|
+
// - 'unverifiedEmail'
|
|
573
|
+
// - 'inactive'
|
|
574
|
+
// - 'expiredToken'
|
|
575
|
+
|
|
576
|
+
const store = createMockAuthStore();
|
|
577
|
+
applyScenario(store, 'admin');
|
|
578
|
+
expect(store.user?.roles).toContain('admin');
|
|
579
|
+
```
|
|
580
|
+
|
|
581
|
+
### Custom Assertions
|
|
582
|
+
|
|
583
|
+
Use built-in assertions for common checks:
|
|
584
|
+
|
|
585
|
+
```typescript
|
|
586
|
+
import {
|
|
587
|
+
assertAuthenticated,
|
|
588
|
+
assertHasPermissions,
|
|
589
|
+
assertTokenValid,
|
|
590
|
+
assertApiCalled,
|
|
591
|
+
assertRequiresMFA,
|
|
592
|
+
} from '@classic-homes/auth/testing';
|
|
593
|
+
|
|
594
|
+
// Auth state assertions
|
|
595
|
+
assertAuthenticated(store.getState());
|
|
596
|
+
assertUnauthenticated(store.getState());
|
|
597
|
+
|
|
598
|
+
// Permission assertions
|
|
599
|
+
assertHasPermissions(user, ['read:profile', 'write:profile']);
|
|
600
|
+
assertHasRoles(user, ['admin']);
|
|
601
|
+
|
|
602
|
+
// Token assertions
|
|
603
|
+
assertTokenValid(accessToken);
|
|
604
|
+
assertTokenExpired(oldToken);
|
|
605
|
+
|
|
606
|
+
// API call assertions
|
|
607
|
+
assertApiCalled(mockFetch, 'POST', '/auth/login', {
|
|
608
|
+
times: 1,
|
|
609
|
+
body: { username: 'test', password: 'pass' },
|
|
610
|
+
});
|
|
611
|
+
|
|
612
|
+
// MFA assertions
|
|
613
|
+
assertRequiresMFA(loginResponse);
|
|
614
|
+
assertNoMFARequired(loginResponse);
|
|
615
|
+
```
|
|
616
|
+
|
|
617
|
+
### Isolated Test Context
|
|
618
|
+
|
|
619
|
+
Run tests in isolated auth contexts:
|
|
620
|
+
|
|
621
|
+
```typescript
|
|
622
|
+
import { withTestAuth } from '@classic-homes/auth/testing';
|
|
623
|
+
|
|
624
|
+
// Automatic setup and cleanup
|
|
625
|
+
await withTestAuth(async ({ mockFetch, mockStore }) => {
|
|
626
|
+
mockFetch.requireMFA();
|
|
627
|
+
const response = await authService.login({ username: 'test', password: 'pass' });
|
|
628
|
+
expect(response.requiresMFA).toBe(true);
|
|
629
|
+
});
|
|
630
|
+
```
|
|
631
|
+
|
|
632
|
+
### User Fixtures
|
|
633
|
+
|
|
634
|
+
Create custom test users:
|
|
635
|
+
|
|
636
|
+
```typescript
|
|
637
|
+
import {
|
|
638
|
+
mockUser,
|
|
639
|
+
mockAdminUser,
|
|
640
|
+
createMockUser,
|
|
641
|
+
createMockUserWithRoles,
|
|
642
|
+
} from '@classic-homes/auth/testing';
|
|
643
|
+
|
|
644
|
+
// Use pre-defined users
|
|
645
|
+
expect(mockUser.role).toBe('user');
|
|
646
|
+
expect(mockAdminUser.permissions).toContain('manage:system');
|
|
647
|
+
|
|
648
|
+
// Create custom users
|
|
649
|
+
const customUser = createMockUser({
|
|
650
|
+
email: 'custom@example.com',
|
|
651
|
+
firstName: 'Custom',
|
|
652
|
+
});
|
|
653
|
+
|
|
654
|
+
// Create users with specific RBAC
|
|
655
|
+
const managerUser = createMockUserWithRoles(['manager', 'user'], ['read:reports', 'write:reports']);
|
|
656
|
+
```
|
|
657
|
+
|
|
253
658
|
## License
|
|
254
659
|
|
|
255
660
|
MIT
|