@fluxbase/sdk-react 2026.1.22-rc.9 → 2026.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,175 @@
1
+ /**
2
+ * Tests for admin authentication hook
3
+ */
4
+
5
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
6
+ import { renderHook, waitFor, act } from '@testing-library/react';
7
+ import { useAdminAuth } from './use-admin-auth';
8
+ import { createMockClient, createWrapper } from './test-utils';
9
+
10
+ describe('useAdminAuth', () => {
11
+ it('should check auth status on mount when autoCheck is true', async () => {
12
+ const mockUser = { id: '1', email: 'admin@example.com', role: 'admin' };
13
+ const meMock = vi.fn().mockResolvedValue({ data: { user: mockUser }, error: null });
14
+
15
+ const client = createMockClient({
16
+ admin: { me: meMock },
17
+ } as any);
18
+
19
+ const { result } = renderHook(
20
+ () => useAdminAuth({ autoCheck: true }),
21
+ { wrapper: createWrapper(client) }
22
+ );
23
+
24
+ await waitFor(() => expect(result.current.isLoading).toBe(false));
25
+ expect(result.current.user).toEqual(mockUser);
26
+ expect(result.current.isAuthenticated).toBe(true);
27
+ expect(meMock).toHaveBeenCalled();
28
+ });
29
+
30
+ it('should not check auth status when autoCheck is false', async () => {
31
+ const meMock = vi.fn();
32
+
33
+ const client = createMockClient({
34
+ admin: { me: meMock },
35
+ } as any);
36
+
37
+ const { result } = renderHook(
38
+ () => useAdminAuth({ autoCheck: false }),
39
+ { wrapper: createWrapper(client) }
40
+ );
41
+
42
+ await waitFor(() => expect(result.current.isLoading).toBe(false));
43
+ expect(meMock).not.toHaveBeenCalled();
44
+ expect(result.current.isAuthenticated).toBe(false);
45
+ });
46
+
47
+ it('should handle auth check error', async () => {
48
+ const error = new Error('Not authenticated');
49
+ const meMock = vi.fn().mockResolvedValue({ data: null, error });
50
+
51
+ const client = createMockClient({
52
+ admin: { me: meMock },
53
+ } as any);
54
+
55
+ const { result } = renderHook(
56
+ () => useAdminAuth({ autoCheck: true }),
57
+ { wrapper: createWrapper(client) }
58
+ );
59
+
60
+ await waitFor(() => expect(result.current.isLoading).toBe(false));
61
+ expect(result.current.user).toBeNull();
62
+ expect(result.current.isAuthenticated).toBe(false);
63
+ expect(result.current.error).toBe(error);
64
+ });
65
+
66
+ it('should login successfully', async () => {
67
+ const mockUser = { id: '1', email: 'admin@example.com', role: 'admin' };
68
+ const loginMock = vi.fn().mockResolvedValue({
69
+ data: { user: mockUser, token: 'token' },
70
+ error: null,
71
+ });
72
+
73
+ const client = createMockClient({
74
+ admin: { login: loginMock, me: vi.fn().mockResolvedValue({ data: null, error: null }) },
75
+ } as any);
76
+
77
+ const { result } = renderHook(
78
+ () => useAdminAuth({ autoCheck: false }),
79
+ { wrapper: createWrapper(client) }
80
+ );
81
+
82
+ await act(async () => {
83
+ await result.current.login('admin@example.com', 'password');
84
+ });
85
+
86
+ expect(loginMock).toHaveBeenCalledWith({ email: 'admin@example.com', password: 'password' });
87
+ expect(result.current.user).toEqual(mockUser);
88
+ expect(result.current.isAuthenticated).toBe(true);
89
+ });
90
+
91
+ it('should handle login error', async () => {
92
+ const error = new Error('Invalid credentials');
93
+ const loginMock = vi.fn().mockResolvedValue({ data: null, error });
94
+
95
+ const client = createMockClient({
96
+ admin: { login: loginMock, me: vi.fn().mockResolvedValue({ data: null, error: null }) },
97
+ } as any);
98
+
99
+ const { result } = renderHook(
100
+ () => useAdminAuth({ autoCheck: false }),
101
+ { wrapper: createWrapper(client) }
102
+ );
103
+
104
+ await expect(act(async () => {
105
+ await result.current.login('admin@example.com', 'wrong-password');
106
+ })).rejects.toThrow();
107
+
108
+ // User should remain null after failed login
109
+ expect(result.current.user).toBeNull();
110
+ expect(result.current.isAuthenticated).toBe(false);
111
+ });
112
+
113
+ it('should logout', async () => {
114
+ const mockUser = { id: '1', email: 'admin@example.com', role: 'admin' };
115
+ const meMock = vi.fn().mockResolvedValue({ data: { user: mockUser }, error: null });
116
+
117
+ const client = createMockClient({
118
+ admin: { me: meMock },
119
+ } as any);
120
+
121
+ const { result } = renderHook(
122
+ () => useAdminAuth({ autoCheck: true }),
123
+ { wrapper: createWrapper(client) }
124
+ );
125
+
126
+ await waitFor(() => expect(result.current.isAuthenticated).toBe(true));
127
+
128
+ await act(async () => {
129
+ await result.current.logout();
130
+ });
131
+
132
+ expect(result.current.user).toBeNull();
133
+ expect(result.current.isAuthenticated).toBe(false);
134
+ });
135
+
136
+ it('should refresh user info', async () => {
137
+ const mockUser = { id: '1', email: 'admin@example.com', role: 'admin' };
138
+ const meMock = vi.fn().mockResolvedValue({ data: { user: mockUser }, error: null });
139
+
140
+ const client = createMockClient({
141
+ admin: { me: meMock },
142
+ } as any);
143
+
144
+ const { result } = renderHook(
145
+ () => useAdminAuth({ autoCheck: false }),
146
+ { wrapper: createWrapper(client) }
147
+ );
148
+
149
+ await act(async () => {
150
+ await result.current.refresh();
151
+ });
152
+
153
+ expect(meMock).toHaveBeenCalledTimes(1);
154
+ expect(result.current.user).toEqual(mockUser);
155
+ });
156
+
157
+ it('should show loading state during operations', async () => {
158
+ const meMock = vi.fn().mockImplementation(() => new Promise((resolve) => {
159
+ setTimeout(() => resolve({ data: { user: {} }, error: null }), 100);
160
+ }));
161
+
162
+ const client = createMockClient({
163
+ admin: { me: meMock },
164
+ } as any);
165
+
166
+ const { result } = renderHook(
167
+ () => useAdminAuth({ autoCheck: true }),
168
+ { wrapper: createWrapper(client) }
169
+ );
170
+
171
+ expect(result.current.isLoading).toBe(true);
172
+
173
+ await waitFor(() => expect(result.current.isLoading).toBe(false));
174
+ });
175
+ });
@@ -137,14 +137,22 @@ export function useAdminAuth(
137
137
 
138
138
  /**
139
139
  * Logout admin
140
+ *
141
+ * WARNING: Currently only clears local state. The server-side session/token
142
+ * remains valid until it expires. This should call a logout endpoint to
143
+ * invalidate the session on the server for proper security.
140
144
  */
141
145
  const logout = useCallback(async (): Promise<void> => {
142
146
  try {
143
147
  setIsLoading(true);
144
148
  setError(null);
145
- // Clear user state
149
+
150
+ // TODO: Call server-side logout endpoint when available
151
+ // This is a security concern - the token remains valid on the server
152
+ // await client.admin.logout();
153
+
154
+ // Clear local user state
146
155
  setUser(null);
147
- // Note: Add logout endpoint call here when available
148
156
  } catch (err) {
149
157
  setError(err as Error);
150
158
  throw err;
@@ -0,0 +1,457 @@
1
+ /**
2
+ * Tests for admin hooks (settings, webhooks)
3
+ */
4
+
5
+ import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
6
+ import { renderHook, waitFor, act } from "@testing-library/react";
7
+ import {
8
+ useAppSettings,
9
+ useSystemSettings,
10
+ useWebhooks,
11
+ } from "./use-admin-hooks";
12
+ import { createMockClient, createWrapper } from "./test-utils";
13
+
14
+ describe("useAppSettings", () => {
15
+ it("should fetch settings on mount when autoFetch is true", async () => {
16
+ const mockSettings = { features: { darkMode: true } };
17
+ const getMock = vi.fn().mockResolvedValue(mockSettings);
18
+
19
+ const client = createMockClient({
20
+ admin: {
21
+ settings: {
22
+ app: { get: getMock, update: vi.fn() },
23
+ },
24
+ },
25
+ } as any);
26
+
27
+ const { result } = renderHook(() => useAppSettings({ autoFetch: true }), {
28
+ wrapper: createWrapper(client),
29
+ });
30
+
31
+ await waitFor(() => expect(result.current.isLoading).toBe(false));
32
+ expect(result.current.settings).toEqual(mockSettings);
33
+ expect(getMock).toHaveBeenCalled();
34
+ });
35
+
36
+ it("should not fetch settings on mount when autoFetch is false", async () => {
37
+ const getMock = vi.fn();
38
+
39
+ const client = createMockClient({
40
+ admin: {
41
+ settings: {
42
+ app: { get: getMock, update: vi.fn() },
43
+ },
44
+ },
45
+ } as any);
46
+
47
+ const { result } = renderHook(() => useAppSettings({ autoFetch: false }), {
48
+ wrapper: createWrapper(client),
49
+ });
50
+
51
+ await waitFor(() => expect(result.current.isLoading).toBe(false));
52
+ expect(getMock).not.toHaveBeenCalled();
53
+ });
54
+
55
+ it("should update settings", async () => {
56
+ const mockSettings = { features: { darkMode: true } };
57
+ const getMock = vi.fn().mockResolvedValue(mockSettings);
58
+ const updateMock = vi.fn().mockResolvedValue({});
59
+
60
+ const client = createMockClient({
61
+ admin: {
62
+ settings: {
63
+ app: { get: getMock, update: updateMock },
64
+ },
65
+ },
66
+ } as any);
67
+
68
+ const { result } = renderHook(() => useAppSettings({ autoFetch: true }), {
69
+ wrapper: createWrapper(client),
70
+ });
71
+
72
+ await waitFor(() => expect(result.current.isLoading).toBe(false));
73
+
74
+ await act(async () => {
75
+ await result.current.updateSettings({
76
+ features: { enable_realtime: false },
77
+ });
78
+ });
79
+
80
+ expect(updateMock).toHaveBeenCalledWith({
81
+ features: { enable_realtime: false },
82
+ });
83
+ // Should refetch after update
84
+ expect(getMock).toHaveBeenCalledTimes(2);
85
+ });
86
+
87
+ it("should handle errors", async () => {
88
+ const error = new Error("Failed to fetch");
89
+ const getMock = vi.fn().mockRejectedValue(error);
90
+
91
+ const client = createMockClient({
92
+ admin: {
93
+ settings: {
94
+ app: { get: getMock, update: vi.fn() },
95
+ },
96
+ },
97
+ } as any);
98
+
99
+ const { result } = renderHook(() => useAppSettings({ autoFetch: true }), {
100
+ wrapper: createWrapper(client),
101
+ });
102
+
103
+ await waitFor(() => expect(result.current.isLoading).toBe(false));
104
+ expect(result.current.error).toBe(error);
105
+ });
106
+
107
+ it("should refetch on demand", async () => {
108
+ const mockSettings = { features: {} };
109
+ const getMock = vi.fn().mockResolvedValue(mockSettings);
110
+
111
+ const client = createMockClient({
112
+ admin: {
113
+ settings: {
114
+ app: { get: getMock, update: vi.fn() },
115
+ },
116
+ },
117
+ } as any);
118
+
119
+ const { result } = renderHook(() => useAppSettings({ autoFetch: false }), {
120
+ wrapper: createWrapper(client),
121
+ });
122
+
123
+ await act(async () => {
124
+ await result.current.refetch();
125
+ });
126
+
127
+ expect(getMock).toHaveBeenCalledTimes(1);
128
+ });
129
+ });
130
+
131
+ describe("useSystemSettings", () => {
132
+ it("should fetch settings on mount when autoFetch is true", async () => {
133
+ const mockSettings = [{ key: "theme", value: "dark" }];
134
+ const listMock = vi.fn().mockResolvedValue({ settings: mockSettings });
135
+
136
+ const client = createMockClient({
137
+ admin: {
138
+ settings: {
139
+ system: { list: listMock, update: vi.fn(), delete: vi.fn() },
140
+ },
141
+ },
142
+ } as any);
143
+
144
+ const { result } = renderHook(
145
+ () => useSystemSettings({ autoFetch: true }),
146
+ { wrapper: createWrapper(client) },
147
+ );
148
+
149
+ await waitFor(() => expect(result.current.isLoading).toBe(false));
150
+ expect(result.current.settings).toEqual(mockSettings);
151
+ });
152
+
153
+ it("should get setting by key", async () => {
154
+ const mockSettings = [
155
+ { key: "theme", value: "dark" },
156
+ { key: "language", value: "en" },
157
+ ];
158
+ const listMock = vi.fn().mockResolvedValue({ settings: mockSettings });
159
+
160
+ const client = createMockClient({
161
+ admin: {
162
+ settings: {
163
+ system: { list: listMock, update: vi.fn(), delete: vi.fn() },
164
+ },
165
+ },
166
+ } as any);
167
+
168
+ const { result } = renderHook(
169
+ () => useSystemSettings({ autoFetch: true }),
170
+ { wrapper: createWrapper(client) },
171
+ );
172
+
173
+ await waitFor(() => expect(result.current.isLoading).toBe(false));
174
+
175
+ const setting = result.current.getSetting("theme");
176
+ expect(setting).toEqual({ key: "theme", value: "dark" });
177
+
178
+ const notFound = result.current.getSetting("nonexistent");
179
+ expect(notFound).toBeUndefined();
180
+ });
181
+
182
+ it("should update setting", async () => {
183
+ const mockSettings = [{ key: "theme", value: "dark" }];
184
+ const listMock = vi.fn().mockResolvedValue({ settings: mockSettings });
185
+ const updateMock = vi.fn().mockResolvedValue({});
186
+
187
+ const client = createMockClient({
188
+ admin: {
189
+ settings: {
190
+ system: { list: listMock, update: updateMock, delete: vi.fn() },
191
+ },
192
+ },
193
+ } as any);
194
+
195
+ const { result } = renderHook(
196
+ () => useSystemSettings({ autoFetch: true }),
197
+ { wrapper: createWrapper(client) },
198
+ );
199
+
200
+ await waitFor(() => expect(result.current.isLoading).toBe(false));
201
+
202
+ await act(async () => {
203
+ await result.current.updateSetting("theme", {
204
+ value: { theme: "light" },
205
+ });
206
+ });
207
+
208
+ expect(updateMock).toHaveBeenCalledWith("theme", {
209
+ value: { theme: "light" },
210
+ });
211
+ // Should refetch after update
212
+ expect(listMock).toHaveBeenCalledTimes(2);
213
+ });
214
+
215
+ it("should delete setting", async () => {
216
+ const mockSettings = [{ key: "theme", value: "dark" }];
217
+ const listMock = vi.fn().mockResolvedValue({ settings: mockSettings });
218
+ const deleteMock = vi.fn().mockResolvedValue({});
219
+
220
+ const client = createMockClient({
221
+ admin: {
222
+ settings: {
223
+ system: { list: listMock, update: vi.fn(), delete: deleteMock },
224
+ },
225
+ },
226
+ } as any);
227
+
228
+ const { result } = renderHook(
229
+ () => useSystemSettings({ autoFetch: true }),
230
+ { wrapper: createWrapper(client) },
231
+ );
232
+
233
+ await waitFor(() => expect(result.current.isLoading).toBe(false));
234
+
235
+ await act(async () => {
236
+ await result.current.deleteSetting("theme");
237
+ });
238
+
239
+ expect(deleteMock).toHaveBeenCalledWith("theme");
240
+ // Should refetch after delete
241
+ expect(listMock).toHaveBeenCalledTimes(2);
242
+ });
243
+ });
244
+
245
+ describe("useWebhooks", () => {
246
+ it("should fetch webhooks on mount when autoFetch is true", async () => {
247
+ const mockWebhooks = [{ id: "1", url: "https://example.com/webhook" }];
248
+ const listMock = vi.fn().mockResolvedValue({ webhooks: mockWebhooks });
249
+
250
+ const client = createMockClient({
251
+ admin: {
252
+ management: {
253
+ webhooks: {
254
+ list: listMock,
255
+ create: vi.fn(),
256
+ update: vi.fn(),
257
+ delete: vi.fn(),
258
+ test: vi.fn(),
259
+ },
260
+ },
261
+ },
262
+ } as any);
263
+
264
+ const { result } = renderHook(() => useWebhooks({ autoFetch: true }), {
265
+ wrapper: createWrapper(client),
266
+ });
267
+
268
+ await waitFor(() => expect(result.current.isLoading).toBe(false));
269
+ expect(result.current.webhooks).toEqual(mockWebhooks);
270
+ });
271
+
272
+ it("should create webhook", async () => {
273
+ const mockWebhooks: any[] = [];
274
+ const listMock = vi.fn().mockResolvedValue({ webhooks: mockWebhooks });
275
+ const createMock = vi
276
+ .fn()
277
+ .mockResolvedValue({ id: "1", url: "https://example.com/webhook" });
278
+
279
+ const client = createMockClient({
280
+ admin: {
281
+ management: {
282
+ webhooks: {
283
+ list: listMock,
284
+ create: createMock,
285
+ update: vi.fn(),
286
+ delete: vi.fn(),
287
+ test: vi.fn(),
288
+ },
289
+ },
290
+ },
291
+ } as any);
292
+
293
+ const { result } = renderHook(() => useWebhooks({ autoFetch: true }), {
294
+ wrapper: createWrapper(client),
295
+ });
296
+
297
+ await waitFor(() => expect(result.current.isLoading).toBe(false));
298
+
299
+ await act(async () => {
300
+ await result.current.createWebhook({
301
+ url: "https://example.com/webhook",
302
+ events: ["user.created"],
303
+ });
304
+ });
305
+
306
+ expect(createMock).toHaveBeenCalledWith({
307
+ url: "https://example.com/webhook",
308
+ events: ["user.created"],
309
+ });
310
+ // Should refetch after create
311
+ expect(listMock).toHaveBeenCalledTimes(2);
312
+ });
313
+
314
+ it("should update webhook", async () => {
315
+ const mockWebhooks = [{ id: "1", url: "https://example.com/webhook" }];
316
+ const listMock = vi.fn().mockResolvedValue({ webhooks: mockWebhooks });
317
+ const updateMock = vi
318
+ .fn()
319
+ .mockResolvedValue({ id: "1", url: "https://new.com/webhook" });
320
+
321
+ const client = createMockClient({
322
+ admin: {
323
+ management: {
324
+ webhooks: {
325
+ list: listMock,
326
+ create: vi.fn(),
327
+ update: updateMock,
328
+ delete: vi.fn(),
329
+ test: vi.fn(),
330
+ },
331
+ },
332
+ },
333
+ } as any);
334
+
335
+ const { result } = renderHook(() => useWebhooks({ autoFetch: true }), {
336
+ wrapper: createWrapper(client),
337
+ });
338
+
339
+ await waitFor(() => expect(result.current.isLoading).toBe(false));
340
+
341
+ await act(async () => {
342
+ await result.current.updateWebhook("1", {
343
+ url: "https://new.com/webhook",
344
+ });
345
+ });
346
+
347
+ expect(updateMock).toHaveBeenCalledWith("1", {
348
+ url: "https://new.com/webhook",
349
+ });
350
+ // Should refetch after update
351
+ expect(listMock).toHaveBeenCalledTimes(2);
352
+ });
353
+
354
+ it("should delete webhook", async () => {
355
+ const mockWebhooks = [{ id: "1", url: "https://example.com/webhook" }];
356
+ const listMock = vi.fn().mockResolvedValue({ webhooks: mockWebhooks });
357
+ const deleteMock = vi.fn().mockResolvedValue({});
358
+
359
+ const client = createMockClient({
360
+ admin: {
361
+ management: {
362
+ webhooks: {
363
+ list: listMock,
364
+ create: vi.fn(),
365
+ update: vi.fn(),
366
+ delete: deleteMock,
367
+ test: vi.fn(),
368
+ },
369
+ },
370
+ },
371
+ } as any);
372
+
373
+ const { result } = renderHook(() => useWebhooks({ autoFetch: true }), {
374
+ wrapper: createWrapper(client),
375
+ });
376
+
377
+ await waitFor(() => expect(result.current.isLoading).toBe(false));
378
+
379
+ await act(async () => {
380
+ await result.current.deleteWebhook("1");
381
+ });
382
+
383
+ expect(deleteMock).toHaveBeenCalledWith("1");
384
+ // Should refetch after delete
385
+ expect(listMock).toHaveBeenCalledTimes(2);
386
+ });
387
+
388
+ it("should test webhook", async () => {
389
+ const mockWebhooks = [{ id: "1", url: "https://example.com/webhook" }];
390
+ const listMock = vi.fn().mockResolvedValue({ webhooks: mockWebhooks });
391
+ const testMock = vi.fn().mockResolvedValue({});
392
+
393
+ const client = createMockClient({
394
+ admin: {
395
+ management: {
396
+ webhooks: {
397
+ list: listMock,
398
+ create: vi.fn(),
399
+ update: vi.fn(),
400
+ delete: vi.fn(),
401
+ test: testMock,
402
+ },
403
+ },
404
+ },
405
+ } as any);
406
+
407
+ const { result } = renderHook(() => useWebhooks({ autoFetch: true }), {
408
+ wrapper: createWrapper(client),
409
+ });
410
+
411
+ await waitFor(() => expect(result.current.isLoading).toBe(false));
412
+
413
+ await act(async () => {
414
+ await result.current.testWebhook("1");
415
+ });
416
+
417
+ expect(testMock).toHaveBeenCalledWith("1");
418
+ });
419
+
420
+ it("should set up refetch interval", async () => {
421
+ vi.useFakeTimers();
422
+ const mockWebhooks: any[] = [];
423
+ const listMock = vi.fn().mockResolvedValue({ webhooks: mockWebhooks });
424
+
425
+ const client = createMockClient({
426
+ admin: {
427
+ management: {
428
+ webhooks: {
429
+ list: listMock,
430
+ create: vi.fn(),
431
+ update: vi.fn(),
432
+ delete: vi.fn(),
433
+ test: vi.fn(),
434
+ },
435
+ },
436
+ },
437
+ } as any);
438
+
439
+ const { unmount } = renderHook(
440
+ () => useWebhooks({ autoFetch: true, refetchInterval: 5000 }),
441
+ { wrapper: createWrapper(client) },
442
+ );
443
+
444
+ // Initial fetch
445
+ expect(listMock).toHaveBeenCalledTimes(1);
446
+
447
+ // Advance timer
448
+ await act(async () => {
449
+ vi.advanceTimersByTime(5000);
450
+ });
451
+
452
+ expect(listMock).toHaveBeenCalledTimes(2);
453
+
454
+ unmount();
455
+ vi.useRealTimers();
456
+ });
457
+ });