@svadmin/core 0.0.3 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +4 -1
- package/src/audit.ts +37 -0
- package/src/auth-hooks.svelte.ts +293 -0
- package/src/auth-hooks.test.ts +205 -0
- package/src/context.ts +77 -15
- package/src/data-transfer.test.ts +118 -0
- package/src/data-transfer.ts +148 -70
- package/src/form-hooks.svelte.ts +266 -0
- package/src/helpers.ts +157 -0
- package/src/hook-utils.svelte.ts +121 -0
- package/src/hooks.svelte.ts +278 -496
- package/src/http-error.test.ts +85 -0
- package/src/i18n.svelte.ts +24 -4
- package/src/i18n.test.ts +90 -0
- package/src/index.ts +51 -8
- package/src/inferencer-openapi.test.ts +192 -0
- package/src/inferencer-openapi.ts +290 -0
- package/src/inferencer.test.ts +189 -0
- package/src/inferencer.ts +247 -0
- package/src/live-sse.ts +146 -0
- package/src/live-websocket.ts +141 -0
- package/src/live.test.ts +134 -0
- package/src/live.ts +60 -8
- package/src/mutation-hooks.svelte.ts +301 -0
- package/src/options.ts +95 -0
- package/src/permissions.test.ts +55 -0
- package/src/query-hooks.svelte.ts +213 -0
- package/src/query-keys.ts +22 -0
- package/src/resource-types.test.ts +79 -0
- package/src/router-provider.test.ts +71 -0
- package/src/router-provider.ts +100 -0
- package/src/router.ts +28 -4
- package/src/routing-hooks.svelte.ts +99 -0
- package/src/table-hooks.svelte.ts +178 -0
- package/src/toast.svelte.ts +8 -4
- package/src/types.ts +124 -47
- package/src/unsaved-changes.svelte.ts +62 -0
- package/src/url-sync.ts +15 -0
- package/src/useParsed.ts +110 -0
- package/src/useStepsForm.svelte.ts +71 -0
- package/src/utility-hooks.svelte.ts +161 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@svadmin/core",
|
|
3
|
-
"version": "0.0
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Core SDK — hooks, types, context, i18n, permissions, router",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"sideEffects": false,
|
|
@@ -56,5 +56,8 @@
|
|
|
56
56
|
"homepage": "https://github.com/zuohuadong/svadmin#readme",
|
|
57
57
|
"bugs": {
|
|
58
58
|
"url": "https://github.com/zuohuadong/svadmin/issues"
|
|
59
|
+
},
|
|
60
|
+
"devDependencies": {
|
|
61
|
+
"@types/bun": "^1.3.11"
|
|
59
62
|
}
|
|
60
63
|
}
|
package/src/audit.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
// Audit logging — record admin operations
|
|
2
|
+
// Full AuditLogProvider interface
|
|
2
3
|
|
|
3
4
|
export interface AuditEntry {
|
|
4
5
|
timestamp: string;
|
|
@@ -7,22 +8,58 @@ export interface AuditEntry {
|
|
|
7
8
|
recordId?: string | number;
|
|
8
9
|
userId?: string;
|
|
9
10
|
details?: Record<string, unknown>;
|
|
11
|
+
data?: Record<string, unknown>;
|
|
12
|
+
previousData?: Record<string, unknown>;
|
|
13
|
+
meta?: Record<string, unknown>;
|
|
10
14
|
}
|
|
11
15
|
|
|
12
16
|
export type AuditHandler = (entry: AuditEntry) => void | Promise<void>;
|
|
13
17
|
|
|
18
|
+
/**
|
|
19
|
+
* Full AuditLogProvider interface.
|
|
20
|
+
* - create: log a new entry
|
|
21
|
+
* - get: retrieve entries for a resource
|
|
22
|
+
* - update: update an existing entry (e.g. mark as reviewed)
|
|
23
|
+
*/
|
|
24
|
+
export interface AuditLogProvider {
|
|
25
|
+
create: (params: { resource: string; action: string; data?: Record<string, unknown>; previousData?: Record<string, unknown>; meta?: Record<string, unknown> }) => Promise<AuditEntry>;
|
|
26
|
+
get: (params: { resource: string; action?: string; meta?: Record<string, unknown>; author?: Record<string, unknown> }) => Promise<AuditEntry[]>;
|
|
27
|
+
update: (params: { id: string | number; name: string; meta?: Record<string, unknown> }) => Promise<AuditEntry>;
|
|
28
|
+
}
|
|
29
|
+
|
|
14
30
|
let handler: AuditHandler = (entry) => {
|
|
15
31
|
console.info('[audit]', entry.action, entry.resource, entry.recordId);
|
|
16
32
|
};
|
|
17
33
|
|
|
34
|
+
let auditLogProvider: AuditLogProvider | null = null;
|
|
35
|
+
|
|
18
36
|
export function setAuditHandler(fn: AuditHandler): void {
|
|
19
37
|
handler = fn;
|
|
20
38
|
}
|
|
21
39
|
|
|
40
|
+
export function setAuditLogProvider(provider: AuditLogProvider): void {
|
|
41
|
+
auditLogProvider = provider;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function getAuditLogProvider(): AuditLogProvider | null {
|
|
45
|
+
return auditLogProvider;
|
|
46
|
+
}
|
|
47
|
+
|
|
22
48
|
export function audit(entry: Omit<AuditEntry, 'timestamp'>): void {
|
|
23
49
|
const fullEntry: AuditEntry = { ...entry, timestamp: new Date().toISOString() };
|
|
24
50
|
try {
|
|
25
51
|
handler(fullEntry);
|
|
52
|
+
|
|
53
|
+
// Also log via provider if set
|
|
54
|
+
if (auditLogProvider) {
|
|
55
|
+
auditLogProvider.create({
|
|
56
|
+
resource: entry.resource ?? '',
|
|
57
|
+
action: entry.action,
|
|
58
|
+
data: entry.data ?? entry.details,
|
|
59
|
+
previousData: entry.previousData,
|
|
60
|
+
meta: entry.meta,
|
|
61
|
+
}).catch(e => console.error('[audit] provider create error:', e));
|
|
62
|
+
}
|
|
26
63
|
} catch (e) {
|
|
27
64
|
console.error('[audit] handler error:', e);
|
|
28
65
|
}
|
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
// Auth Hooks — reactive wrappers around AuthProvider methods
|
|
2
|
+
// Each hook encapsulates the auth call + loading state + error handling + redirect
|
|
3
|
+
// IMPORTANT: getAuthProvider() must be called at hook creation time (during component init),
|
|
4
|
+
// not inside mutate(), because Svelte's getContext() only works during init.
|
|
5
|
+
|
|
6
|
+
import { getAuthProvider } from './context';
|
|
7
|
+
import { navigate } from './router';
|
|
8
|
+
import { toast } from './toast.svelte';
|
|
9
|
+
import { t } from './i18n.svelte';
|
|
10
|
+
import type { AuthActionResult, CheckResult, Identity } from './types';
|
|
11
|
+
|
|
12
|
+
// ─── useLogin ─────────────────────────────────────────────────
|
|
13
|
+
|
|
14
|
+
export function useLogin() {
|
|
15
|
+
const provider = getAuthProvider();
|
|
16
|
+
let isLoading = $state(false);
|
|
17
|
+
|
|
18
|
+
async function mutate(params: Record<string, unknown>): Promise<AuthActionResult> {
|
|
19
|
+
if (!provider) throw new Error('AuthProvider not configured');
|
|
20
|
+
isLoading = true;
|
|
21
|
+
try {
|
|
22
|
+
const result = await provider.login(params);
|
|
23
|
+
if (result.success) {
|
|
24
|
+
toast.success(t('common.operationSuccess'));
|
|
25
|
+
if (result.redirectTo) navigate(result.redirectTo);
|
|
26
|
+
} else {
|
|
27
|
+
const msg = result.error?.message ?? t('common.loginFailed');
|
|
28
|
+
toast.error(msg);
|
|
29
|
+
}
|
|
30
|
+
return result;
|
|
31
|
+
} catch (err) {
|
|
32
|
+
const msg = err instanceof Error ? err.message : t('common.loginFailed');
|
|
33
|
+
toast.error(msg);
|
|
34
|
+
return { success: false, error: { message: msg } };
|
|
35
|
+
} finally {
|
|
36
|
+
isLoading = false;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return {
|
|
41
|
+
mutate,
|
|
42
|
+
get isLoading() { return isLoading; },
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// ─── useLogout ────────────────────────────────────────────────
|
|
47
|
+
|
|
48
|
+
export function useLogout() {
|
|
49
|
+
const provider = getAuthProvider();
|
|
50
|
+
let isLoading = $state(false);
|
|
51
|
+
|
|
52
|
+
async function mutate(params?: Record<string, unknown>): Promise<AuthActionResult> {
|
|
53
|
+
if (!provider) throw new Error('AuthProvider not configured');
|
|
54
|
+
isLoading = true;
|
|
55
|
+
try {
|
|
56
|
+
const result = await provider.logout(params);
|
|
57
|
+
if (result.success) {
|
|
58
|
+
navigate(result.redirectTo ?? '/login');
|
|
59
|
+
}
|
|
60
|
+
return result;
|
|
61
|
+
} catch (err) {
|
|
62
|
+
const msg = err instanceof Error ? err.message : t('common.operationFailed');
|
|
63
|
+
toast.error(msg);
|
|
64
|
+
return { success: false, error: { message: msg } };
|
|
65
|
+
} finally {
|
|
66
|
+
isLoading = false;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return {
|
|
71
|
+
mutate,
|
|
72
|
+
get isLoading() { return isLoading; },
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// ─── useRegister ──────────────────────────────────────────────
|
|
77
|
+
|
|
78
|
+
export function useRegister() {
|
|
79
|
+
const provider = getAuthProvider();
|
|
80
|
+
let isLoading = $state(false);
|
|
81
|
+
|
|
82
|
+
async function mutate(params: Record<string, unknown>): Promise<AuthActionResult> {
|
|
83
|
+
if (!provider?.register) throw new Error('AuthProvider.register not implemented');
|
|
84
|
+
isLoading = true;
|
|
85
|
+
try {
|
|
86
|
+
const result = await provider.register(params);
|
|
87
|
+
if (result.success) {
|
|
88
|
+
toast.success(t('auth.registerSuccess'));
|
|
89
|
+
if (result.redirectTo) navigate(result.redirectTo);
|
|
90
|
+
} else {
|
|
91
|
+
const msg = result.error?.message ?? t('common.operationFailed');
|
|
92
|
+
toast.error(msg);
|
|
93
|
+
}
|
|
94
|
+
return result;
|
|
95
|
+
} catch (err) {
|
|
96
|
+
const msg = err instanceof Error ? err.message : t('common.operationFailed');
|
|
97
|
+
toast.error(msg);
|
|
98
|
+
return { success: false, error: { message: msg } };
|
|
99
|
+
} finally {
|
|
100
|
+
isLoading = false;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return {
|
|
105
|
+
mutate,
|
|
106
|
+
get isLoading() { return isLoading; },
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// ─── useForgotPassword ───────────────────────────────────────
|
|
111
|
+
|
|
112
|
+
export function useForgotPassword() {
|
|
113
|
+
const provider = getAuthProvider();
|
|
114
|
+
let isLoading = $state(false);
|
|
115
|
+
|
|
116
|
+
async function mutate(params: Record<string, unknown>): Promise<AuthActionResult> {
|
|
117
|
+
if (!provider?.forgotPassword) throw new Error('AuthProvider.forgotPassword not implemented');
|
|
118
|
+
isLoading = true;
|
|
119
|
+
try {
|
|
120
|
+
const result = await provider.forgotPassword(params);
|
|
121
|
+
if (result.success) {
|
|
122
|
+
toast.success(t('auth.resetLinkSent'));
|
|
123
|
+
} else {
|
|
124
|
+
const msg = result.error?.message ?? t('common.operationFailed');
|
|
125
|
+
toast.error(msg);
|
|
126
|
+
}
|
|
127
|
+
return result;
|
|
128
|
+
} catch (err) {
|
|
129
|
+
const msg = err instanceof Error ? err.message : t('common.operationFailed');
|
|
130
|
+
toast.error(msg);
|
|
131
|
+
return { success: false, error: { message: msg } };
|
|
132
|
+
} finally {
|
|
133
|
+
isLoading = false;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return {
|
|
138
|
+
mutate,
|
|
139
|
+
get isLoading() { return isLoading; },
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// ─── useUpdatePassword ───────────────────────────────────────
|
|
144
|
+
|
|
145
|
+
export function useUpdatePassword() {
|
|
146
|
+
const provider = getAuthProvider();
|
|
147
|
+
let isLoading = $state(false);
|
|
148
|
+
|
|
149
|
+
async function mutate(params: Record<string, unknown>): Promise<AuthActionResult> {
|
|
150
|
+
if (!provider?.updatePassword) throw new Error('AuthProvider.updatePassword not implemented');
|
|
151
|
+
isLoading = true;
|
|
152
|
+
try {
|
|
153
|
+
const result = await provider.updatePassword(params);
|
|
154
|
+
if (result.success) {
|
|
155
|
+
toast.success(t('common.operationSuccess'));
|
|
156
|
+
if (result.redirectTo) navigate(result.redirectTo);
|
|
157
|
+
} else {
|
|
158
|
+
const msg = result.error?.message ?? t('common.operationFailed');
|
|
159
|
+
toast.error(msg);
|
|
160
|
+
}
|
|
161
|
+
return result;
|
|
162
|
+
} catch (err) {
|
|
163
|
+
const msg = err instanceof Error ? err.message : t('common.operationFailed');
|
|
164
|
+
toast.error(msg);
|
|
165
|
+
return { success: false, error: { message: msg } };
|
|
166
|
+
} finally {
|
|
167
|
+
isLoading = false;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
return {
|
|
172
|
+
mutate,
|
|
173
|
+
get isLoading() { return isLoading; },
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// ─── useGetIdentity ──────────────────────────────────────────
|
|
178
|
+
|
|
179
|
+
export function useGetIdentity() {
|
|
180
|
+
const provider = getAuthProvider();
|
|
181
|
+
let data = $state<Identity | null>(null);
|
|
182
|
+
let isLoading = $state(true);
|
|
183
|
+
let error = $state<Error | null>(null);
|
|
184
|
+
|
|
185
|
+
if (provider) {
|
|
186
|
+
provider.getIdentity().then(identity => {
|
|
187
|
+
data = identity;
|
|
188
|
+
isLoading = false;
|
|
189
|
+
}).catch(err => {
|
|
190
|
+
error = err instanceof Error ? err : new Error(String(err));
|
|
191
|
+
isLoading = false;
|
|
192
|
+
console.warn('[svadmin] useGetIdentity failed:', err);
|
|
193
|
+
});
|
|
194
|
+
} else {
|
|
195
|
+
isLoading = false;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
return {
|
|
199
|
+
get data() { return data; },
|
|
200
|
+
get isLoading() { return isLoading; },
|
|
201
|
+
get error() { return error; },
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// ─── useIsAuthenticated ──────────────────────────────────────
|
|
206
|
+
|
|
207
|
+
export function useIsAuthenticated() {
|
|
208
|
+
const provider = getAuthProvider();
|
|
209
|
+
let isAuthenticated = $state(false);
|
|
210
|
+
let isLoading = $state(true);
|
|
211
|
+
|
|
212
|
+
if (provider) {
|
|
213
|
+
provider.check().then((result: CheckResult) => {
|
|
214
|
+
isAuthenticated = result.authenticated;
|
|
215
|
+
isLoading = false;
|
|
216
|
+
}).catch(() => {
|
|
217
|
+
isAuthenticated = false;
|
|
218
|
+
isLoading = false;
|
|
219
|
+
});
|
|
220
|
+
} else {
|
|
221
|
+
// No auth provider — treat as authenticated
|
|
222
|
+
isAuthenticated = true;
|
|
223
|
+
isLoading = false;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
return {
|
|
227
|
+
get isAuthenticated() { return isAuthenticated; },
|
|
228
|
+
get isLoading() { return isLoading; },
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// ─── useOnError ──────────────────────────────────────────────
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Handles errors from data hooks by calling authProvider.onError().
|
|
236
|
+
* If the provider returns { logout: true }, triggers logout flow.
|
|
237
|
+
* If it returns { redirectTo }, navigates there.
|
|
238
|
+
*/
|
|
239
|
+
export function useOnError() {
|
|
240
|
+
const provider = getAuthProvider();
|
|
241
|
+
|
|
242
|
+
async function mutate(error: unknown) {
|
|
243
|
+
if (!provider?.onError) {
|
|
244
|
+
console.warn('[svadmin] useOnError: authProvider.onError not implemented');
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
try {
|
|
248
|
+
const result = await provider.onError(error);
|
|
249
|
+
if (result.logout) {
|
|
250
|
+
await provider.logout?.();
|
|
251
|
+
navigate(result.redirectTo ?? '/login');
|
|
252
|
+
} else if (result.redirectTo) {
|
|
253
|
+
navigate(result.redirectTo);
|
|
254
|
+
}
|
|
255
|
+
} catch (err) {
|
|
256
|
+
console.warn('[svadmin] useOnError failed:', err);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
return { mutate };
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// ─── usePermissions ──────────────────────────────────────────
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Fetches permissions from authProvider.getPermissions().
|
|
267
|
+
* Returns a reactive object with data, isLoading, and error.
|
|
268
|
+
*/
|
|
269
|
+
export function usePermissions<T = unknown>() {
|
|
270
|
+
const provider = getAuthProvider();
|
|
271
|
+
let data = $state<T | null>(null);
|
|
272
|
+
let isLoading = $state(true);
|
|
273
|
+
let error = $state<Error | null>(null);
|
|
274
|
+
|
|
275
|
+
if (provider?.getPermissions) {
|
|
276
|
+
provider.getPermissions().then(permissions => {
|
|
277
|
+
data = permissions as T;
|
|
278
|
+
isLoading = false;
|
|
279
|
+
}).catch(err => {
|
|
280
|
+
error = err instanceof Error ? err : new Error(String(err));
|
|
281
|
+
isLoading = false;
|
|
282
|
+
console.warn('[svadmin] usePermissions failed:', err);
|
|
283
|
+
});
|
|
284
|
+
} else {
|
|
285
|
+
isLoading = false;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
return {
|
|
289
|
+
get data() { return data; },
|
|
290
|
+
get isLoading() { return isLoading; },
|
|
291
|
+
get error() { return error; },
|
|
292
|
+
};
|
|
293
|
+
}
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Auth Hooks — Unit Tests
|
|
3
|
+
*
|
|
4
|
+
* These test the auth hook *logic* by mocking the Svelte context module
|
|
5
|
+
* so `getAuthProvider()` returns a mock provider without needing a real component tree.
|
|
6
|
+
*/
|
|
7
|
+
import { describe, test, expect, mock, beforeEach } from 'bun:test';
|
|
8
|
+
import type { AuthProvider, AuthActionResult, CheckResult, Identity } from './types';
|
|
9
|
+
|
|
10
|
+
// Build a mock AuthProvider factory
|
|
11
|
+
function createMockAuthProvider(overrides: Partial<AuthProvider> = {}): AuthProvider {
|
|
12
|
+
return {
|
|
13
|
+
login: mock(async () => ({ success: true, redirectTo: '/' } as AuthActionResult)),
|
|
14
|
+
logout: mock(async () => ({ success: true, redirectTo: '/login' } as AuthActionResult)),
|
|
15
|
+
check: mock(async () => ({ authenticated: true } as CheckResult)),
|
|
16
|
+
getIdentity: mock(async () => ({ id: '1', name: 'Admin', email: 'admin@test.com' } as Identity)),
|
|
17
|
+
register: mock(async () => ({ success: true } as AuthActionResult)),
|
|
18
|
+
forgotPassword: mock(async () => ({ success: true } as AuthActionResult)),
|
|
19
|
+
updatePassword: mock(async () => ({ success: true } as AuthActionResult)),
|
|
20
|
+
onError: mock(async () => ({ logout: false })),
|
|
21
|
+
getPermissions: mock(async () => ['admin', 'editor']),
|
|
22
|
+
...overrides,
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// ─── Tests for AuthProvider mock interface ───────────────────
|
|
27
|
+
|
|
28
|
+
describe('AuthProvider mock interface', () => {
|
|
29
|
+
test('login returns success', async () => {
|
|
30
|
+
const provider = createMockAuthProvider();
|
|
31
|
+
const result = await provider.login({ email: 'test@test.com', password: '123' });
|
|
32
|
+
expect(result.success).toBe(true);
|
|
33
|
+
expect(result.redirectTo).toBe('/');
|
|
34
|
+
expect(provider.login).toHaveBeenCalledTimes(1);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
test('login returns failure', async () => {
|
|
38
|
+
const provider = createMockAuthProvider({
|
|
39
|
+
login: mock(async () => ({ success: false, error: { message: 'Invalid credentials' } })),
|
|
40
|
+
});
|
|
41
|
+
const result = await provider.login({ email: 'bad', password: 'bad' });
|
|
42
|
+
expect(result.success).toBe(false);
|
|
43
|
+
expect(result.error?.message).toBe('Invalid credentials');
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
test('logout returns success with redirect', async () => {
|
|
47
|
+
const provider = createMockAuthProvider();
|
|
48
|
+
const result = await provider.logout!();
|
|
49
|
+
expect(result.success).toBe(true);
|
|
50
|
+
expect(result.redirectTo).toBe('/login');
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
test('check returns authenticated', async () => {
|
|
54
|
+
const provider = createMockAuthProvider();
|
|
55
|
+
const result = await provider.check();
|
|
56
|
+
expect(result.authenticated).toBe(true);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
test('check returns not authenticated', async () => {
|
|
60
|
+
const provider = createMockAuthProvider({
|
|
61
|
+
check: mock(async () => ({ authenticated: false, redirectTo: '/login' })),
|
|
62
|
+
});
|
|
63
|
+
const result = await provider.check();
|
|
64
|
+
expect(result.authenticated).toBe(false);
|
|
65
|
+
expect(result.redirectTo).toBe('/login');
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
test('getIdentity returns user info', async () => {
|
|
69
|
+
const provider = createMockAuthProvider();
|
|
70
|
+
const identity = await provider.getIdentity();
|
|
71
|
+
expect(identity).not.toBeNull();
|
|
72
|
+
expect(identity!.name).toBe('Admin');
|
|
73
|
+
expect(identity!.email).toBe('admin@test.com');
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
test('getIdentity returns null when not logged in', async () => {
|
|
77
|
+
const provider = createMockAuthProvider({
|
|
78
|
+
getIdentity: mock(async () => null),
|
|
79
|
+
});
|
|
80
|
+
const identity = await provider.getIdentity();
|
|
81
|
+
expect(identity).toBeNull();
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
test('register returns success', async () => {
|
|
85
|
+
const provider = createMockAuthProvider();
|
|
86
|
+
const result = await provider.register!({ email: 'new@test.com', password: '123' });
|
|
87
|
+
expect(result.success).toBe(true);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
test('forgotPassword returns success', async () => {
|
|
91
|
+
const provider = createMockAuthProvider();
|
|
92
|
+
const result = await provider.forgotPassword!({ email: 'test@test.com' });
|
|
93
|
+
expect(result.success).toBe(true);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
test('updatePassword returns success', async () => {
|
|
97
|
+
const provider = createMockAuthProvider();
|
|
98
|
+
const result = await provider.updatePassword!({ password: 'newpass', confirmPassword: 'newpass' });
|
|
99
|
+
expect(result.success).toBe(true);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
test('onError with logout flag', async () => {
|
|
103
|
+
const provider = createMockAuthProvider({
|
|
104
|
+
onError: mock(async () => ({ logout: true, redirectTo: '/login' })),
|
|
105
|
+
});
|
|
106
|
+
const result = await provider.onError!(new Error('401'));
|
|
107
|
+
expect(result.logout).toBe(true);
|
|
108
|
+
expect(result.redirectTo).toBe('/login');
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
test('onError without logout', async () => {
|
|
112
|
+
const provider = createMockAuthProvider({
|
|
113
|
+
onError: mock(async () => ({ logout: false, redirectTo: '/error' })),
|
|
114
|
+
});
|
|
115
|
+
const result = await provider.onError!(new Error('500'));
|
|
116
|
+
expect(result.logout).toBe(false);
|
|
117
|
+
expect(result.redirectTo).toBe('/error');
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
test('getPermissions returns permissions array', async () => {
|
|
121
|
+
const provider = createMockAuthProvider();
|
|
122
|
+
const permissions = await provider.getPermissions!();
|
|
123
|
+
expect(permissions).toEqual(['admin', 'editor']);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
test('getPermissions can return custom structure', async () => {
|
|
127
|
+
const provider = createMockAuthProvider({
|
|
128
|
+
getPermissions: mock(async () => ({ role: 'admin', canDelete: true })),
|
|
129
|
+
});
|
|
130
|
+
const permissions = await provider.getPermissions!() as { role: string; canDelete: boolean };
|
|
131
|
+
expect(permissions.role).toBe('admin');
|
|
132
|
+
expect(permissions.canDelete).toBe(true);
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
// ─── Tests for auth logic patterns ──────────────────────────
|
|
137
|
+
|
|
138
|
+
describe('auth hook patterns', () => {
|
|
139
|
+
test('login flow: success triggers redirect', async () => {
|
|
140
|
+
const provider = createMockAuthProvider();
|
|
141
|
+
const result = await provider.login({ email: 'admin@test.com', password: 'pass' });
|
|
142
|
+
expect(result.success).toBe(true);
|
|
143
|
+
// In real hook: navigate(result.redirectTo)
|
|
144
|
+
expect(result.redirectTo).toBeDefined();
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
test('login flow: failure shows error', async () => {
|
|
148
|
+
const provider = createMockAuthProvider({
|
|
149
|
+
login: mock(async () => ({ success: false, error: { message: 'Bad password' } })),
|
|
150
|
+
});
|
|
151
|
+
const result = await provider.login({ email: 'admin@test.com', password: 'wrong' });
|
|
152
|
+
expect(result.success).toBe(false);
|
|
153
|
+
// In real hook: toast.error(result.error.message)
|
|
154
|
+
expect(result.error?.message).toBe('Bad password');
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
test('login flow: exception produces error result', async () => {
|
|
158
|
+
const provider = createMockAuthProvider({
|
|
159
|
+
login: mock(async () => { throw new Error('Network Error'); }),
|
|
160
|
+
});
|
|
161
|
+
try {
|
|
162
|
+
await provider.login({ email: 'test', password: 'test' });
|
|
163
|
+
expect(true).toBe(false); // should not reach
|
|
164
|
+
} catch (err) {
|
|
165
|
+
expect((err as Error).message).toBe('Network Error');
|
|
166
|
+
}
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
test('onError flow: 401 triggers logout + redirect', async () => {
|
|
170
|
+
const provider = createMockAuthProvider({
|
|
171
|
+
onError: mock(async (err: unknown) => {
|
|
172
|
+
if (err instanceof Error && err.message.includes('401')) {
|
|
173
|
+
return { logout: true, redirectTo: '/login' };
|
|
174
|
+
}
|
|
175
|
+
return { logout: false };
|
|
176
|
+
}),
|
|
177
|
+
});
|
|
178
|
+
const result = await provider.onError!(new Error('401 Unauthorized'));
|
|
179
|
+
expect(result.logout).toBe(true);
|
|
180
|
+
// In real hook: provider.logout() then navigate(result.redirectTo)
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
test('onError flow: 500 does not trigger logout', async () => {
|
|
184
|
+
const provider = createMockAuthProvider({
|
|
185
|
+
onError: mock(async (err: unknown) => {
|
|
186
|
+
if (err instanceof Error && err.message.includes('401')) {
|
|
187
|
+
return { logout: true, redirectTo: '/login' };
|
|
188
|
+
}
|
|
189
|
+
return { logout: false };
|
|
190
|
+
}),
|
|
191
|
+
});
|
|
192
|
+
const result = await provider.onError!(new Error('500 Internal Server Error'));
|
|
193
|
+
expect(result.logout).toBe(false);
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
test('check → getIdentity flow', async () => {
|
|
197
|
+
const provider = createMockAuthProvider();
|
|
198
|
+
const checkResult = await provider.check();
|
|
199
|
+
expect(checkResult.authenticated).toBe(true);
|
|
200
|
+
if (checkResult.authenticated) {
|
|
201
|
+
const identity = await provider.getIdentity();
|
|
202
|
+
expect(identity!.name).toBe('Admin');
|
|
203
|
+
}
|
|
204
|
+
});
|
|
205
|
+
});
|