@fluxbase/sdk-react 2026.1.22 → 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,359 @@
1
+ /**
2
+ * Tests for realtime subscription hooks
3
+ */
4
+
5
+ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
6
+ import { renderHook, waitFor, act } from '@testing-library/react';
7
+ import {
8
+ useRealtime,
9
+ useTableSubscription,
10
+ useTableInserts,
11
+ useTableUpdates,
12
+ useTableDeletes,
13
+ } from './use-realtime';
14
+ import { createMockClient, createWrapper, createTestQueryClient } from './test-utils';
15
+
16
+ describe('useRealtime', () => {
17
+ it('should create channel and subscribe', () => {
18
+ const onMock = vi.fn().mockReturnThis();
19
+ const subscribeMock = vi.fn().mockReturnThis();
20
+ const unsubscribeMock = vi.fn();
21
+ const channelMock = vi.fn().mockReturnValue({
22
+ on: onMock,
23
+ subscribe: subscribeMock,
24
+ unsubscribe: unsubscribeMock,
25
+ });
26
+
27
+ const client = createMockClient({
28
+ realtime: { channel: channelMock },
29
+ } as any);
30
+
31
+ renderHook(
32
+ () => useRealtime({ channel: 'table:products' }),
33
+ { wrapper: createWrapper(client) }
34
+ );
35
+
36
+ expect(channelMock).toHaveBeenCalledWith('table:products');
37
+ expect(onMock).toHaveBeenCalledWith('*', expect.any(Function));
38
+ expect(subscribeMock).toHaveBeenCalled();
39
+ });
40
+
41
+ it('should unsubscribe on unmount', () => {
42
+ const onMock = vi.fn().mockReturnThis();
43
+ const subscribeMock = vi.fn().mockReturnThis();
44
+ const unsubscribeMock = vi.fn();
45
+ const channelMock = vi.fn().mockReturnValue({
46
+ on: onMock,
47
+ subscribe: subscribeMock,
48
+ unsubscribe: unsubscribeMock,
49
+ });
50
+
51
+ const client = createMockClient({
52
+ realtime: { channel: channelMock },
53
+ } as any);
54
+
55
+ const { unmount } = renderHook(
56
+ () => useRealtime({ channel: 'table:products' }),
57
+ { wrapper: createWrapper(client) }
58
+ );
59
+
60
+ unmount();
61
+
62
+ expect(unsubscribeMock).toHaveBeenCalled();
63
+ });
64
+
65
+ it('should not subscribe when disabled', () => {
66
+ const channelMock = vi.fn();
67
+ const client = createMockClient({
68
+ realtime: { channel: channelMock },
69
+ } as any);
70
+
71
+ renderHook(
72
+ () => useRealtime({ channel: 'table:products', enabled: false }),
73
+ { wrapper: createWrapper(client) }
74
+ );
75
+
76
+ expect(channelMock).not.toHaveBeenCalled();
77
+ });
78
+
79
+ it('should subscribe to specific event type', () => {
80
+ const onMock = vi.fn().mockReturnThis();
81
+ const subscribeMock = vi.fn().mockReturnThis();
82
+ const channelMock = vi.fn().mockReturnValue({
83
+ on: onMock,
84
+ subscribe: subscribeMock,
85
+ unsubscribe: vi.fn(),
86
+ });
87
+
88
+ const client = createMockClient({
89
+ realtime: { channel: channelMock },
90
+ } as any);
91
+
92
+ renderHook(
93
+ () => useRealtime({ channel: 'table:products', event: 'INSERT' }),
94
+ { wrapper: createWrapper(client) }
95
+ );
96
+
97
+ expect(onMock).toHaveBeenCalledWith('INSERT', expect.any(Function));
98
+ });
99
+
100
+ it('should call callback on change', () => {
101
+ const callback = vi.fn();
102
+ let changeHandler: Function;
103
+ const onMock = vi.fn().mockImplementation((event, handler) => {
104
+ changeHandler = handler;
105
+ return { subscribe: vi.fn().mockReturnThis(), unsubscribe: vi.fn() };
106
+ });
107
+ const channelMock = vi.fn().mockReturnValue({
108
+ on: onMock,
109
+ subscribe: vi.fn().mockReturnThis(),
110
+ unsubscribe: vi.fn(),
111
+ });
112
+
113
+ const client = createMockClient({
114
+ realtime: { channel: channelMock },
115
+ } as any);
116
+
117
+ renderHook(
118
+ () => useRealtime({ channel: 'table:products', callback }),
119
+ { wrapper: createWrapper(client) }
120
+ );
121
+
122
+ const payload = { eventType: 'INSERT', new: { id: 1 }, old: null };
123
+ changeHandler!(payload);
124
+
125
+ expect(callback).toHaveBeenCalledWith(payload);
126
+ });
127
+
128
+ it('should auto-invalidate queries when enabled', () => {
129
+ let changeHandler: Function;
130
+ const onMock = vi.fn().mockImplementation((event, handler) => {
131
+ changeHandler = handler;
132
+ return { subscribe: vi.fn().mockReturnThis(), unsubscribe: vi.fn() };
133
+ });
134
+ const channelMock = vi.fn().mockReturnValue({
135
+ on: onMock,
136
+ subscribe: vi.fn().mockReturnThis(),
137
+ unsubscribe: vi.fn(),
138
+ });
139
+
140
+ const client = createMockClient({
141
+ realtime: { channel: channelMock },
142
+ } as any);
143
+
144
+ const queryClient = createTestQueryClient();
145
+ const invalidateSpy = vi.spyOn(queryClient, 'invalidateQueries');
146
+
147
+ renderHook(
148
+ () => useRealtime({ channel: 'table:public.products', autoInvalidate: true }),
149
+ { wrapper: createWrapper(client, queryClient) }
150
+ );
151
+
152
+ const payload = { eventType: 'INSERT', new: { id: 1 }, old: null };
153
+ changeHandler!(payload);
154
+
155
+ expect(invalidateSpy).toHaveBeenCalledWith({ queryKey: ['fluxbase', 'table', 'public.products'] });
156
+ });
157
+
158
+ it('should use custom invalidate key', () => {
159
+ let changeHandler: Function;
160
+ const onMock = vi.fn().mockImplementation((event, handler) => {
161
+ changeHandler = handler;
162
+ return { subscribe: vi.fn().mockReturnThis(), unsubscribe: vi.fn() };
163
+ });
164
+ const channelMock = vi.fn().mockReturnValue({
165
+ on: onMock,
166
+ subscribe: vi.fn().mockReturnThis(),
167
+ unsubscribe: vi.fn(),
168
+ });
169
+
170
+ const client = createMockClient({
171
+ realtime: { channel: channelMock },
172
+ } as any);
173
+
174
+ const queryClient = createTestQueryClient();
175
+ const invalidateSpy = vi.spyOn(queryClient, 'invalidateQueries');
176
+
177
+ renderHook(
178
+ () => useRealtime({
179
+ channel: 'table:products',
180
+ autoInvalidate: true,
181
+ invalidateKey: ['custom', 'key'],
182
+ }),
183
+ { wrapper: createWrapper(client, queryClient) }
184
+ );
185
+
186
+ const payload = { eventType: 'INSERT', new: { id: 1 }, old: null };
187
+ changeHandler!(payload);
188
+
189
+ expect(invalidateSpy).toHaveBeenCalledWith({ queryKey: ['custom', 'key'] });
190
+ });
191
+
192
+ it('should not auto-invalidate when disabled', () => {
193
+ let changeHandler: Function;
194
+ const onMock = vi.fn().mockImplementation((event, handler) => {
195
+ changeHandler = handler;
196
+ return { subscribe: vi.fn().mockReturnThis(), unsubscribe: vi.fn() };
197
+ });
198
+ const channelMock = vi.fn().mockReturnValue({
199
+ on: onMock,
200
+ subscribe: vi.fn().mockReturnThis(),
201
+ unsubscribe: vi.fn(),
202
+ });
203
+
204
+ const client = createMockClient({
205
+ realtime: { channel: channelMock },
206
+ } as any);
207
+
208
+ const queryClient = createTestQueryClient();
209
+ const invalidateSpy = vi.spyOn(queryClient, 'invalidateQueries');
210
+
211
+ renderHook(
212
+ () => useRealtime({ channel: 'table:products', autoInvalidate: false }),
213
+ { wrapper: createWrapper(client, queryClient) }
214
+ );
215
+
216
+ const payload = { eventType: 'INSERT', new: { id: 1 }, old: null };
217
+ changeHandler!(payload);
218
+
219
+ expect(invalidateSpy).not.toHaveBeenCalled();
220
+ });
221
+
222
+ it('should return channel property', () => {
223
+ const mockChannel = {
224
+ on: vi.fn().mockReturnThis(),
225
+ subscribe: vi.fn().mockReturnThis(),
226
+ unsubscribe: vi.fn(),
227
+ };
228
+ const channelMock = vi.fn().mockReturnValue(mockChannel);
229
+
230
+ const client = createMockClient({
231
+ realtime: { channel: channelMock },
232
+ } as any);
233
+
234
+ const { result } = renderHook(
235
+ () => useRealtime({ channel: 'table:products' }),
236
+ { wrapper: createWrapper(client) }
237
+ );
238
+
239
+ // The hook returns a channel property (may be null initially, but is set by useEffect)
240
+ expect(result.current).toHaveProperty('channel');
241
+ // Verify the channel was created
242
+ expect(channelMock).toHaveBeenCalledWith('table:products');
243
+ });
244
+ });
245
+
246
+ describe('useTableSubscription', () => {
247
+ it('should subscribe to table with correct channel name', () => {
248
+ const onMock = vi.fn().mockReturnThis();
249
+ const channelMock = vi.fn().mockReturnValue({
250
+ on: onMock,
251
+ subscribe: vi.fn().mockReturnThis(),
252
+ unsubscribe: vi.fn(),
253
+ });
254
+
255
+ const client = createMockClient({
256
+ realtime: { channel: channelMock },
257
+ } as any);
258
+
259
+ renderHook(
260
+ () => useTableSubscription('products'),
261
+ { wrapper: createWrapper(client) }
262
+ );
263
+
264
+ expect(channelMock).toHaveBeenCalledWith('table:products');
265
+ });
266
+
267
+ it('should pass options through', () => {
268
+ const callback = vi.fn();
269
+ const onMock = vi.fn().mockReturnThis();
270
+ const channelMock = vi.fn().mockReturnValue({
271
+ on: onMock,
272
+ subscribe: vi.fn().mockReturnThis(),
273
+ unsubscribe: vi.fn(),
274
+ });
275
+
276
+ const client = createMockClient({
277
+ realtime: { channel: channelMock },
278
+ } as any);
279
+
280
+ renderHook(
281
+ () => useTableSubscription('products', { callback, autoInvalidate: false }),
282
+ { wrapper: createWrapper(client) }
283
+ );
284
+
285
+ expect(channelMock).toHaveBeenCalledWith('table:products');
286
+ });
287
+ });
288
+
289
+ describe('useTableInserts', () => {
290
+ it('should subscribe to INSERT events', () => {
291
+ const onMock = vi.fn().mockReturnThis();
292
+ const channelMock = vi.fn().mockReturnValue({
293
+ on: onMock,
294
+ subscribe: vi.fn().mockReturnThis(),
295
+ unsubscribe: vi.fn(),
296
+ });
297
+
298
+ const client = createMockClient({
299
+ realtime: { channel: channelMock },
300
+ } as any);
301
+
302
+ const callback = vi.fn();
303
+ renderHook(
304
+ () => useTableInserts('products', callback),
305
+ { wrapper: createWrapper(client) }
306
+ );
307
+
308
+ expect(channelMock).toHaveBeenCalledWith('table:products');
309
+ expect(onMock).toHaveBeenCalledWith('INSERT', expect.any(Function));
310
+ });
311
+ });
312
+
313
+ describe('useTableUpdates', () => {
314
+ it('should subscribe to UPDATE events', () => {
315
+ const onMock = vi.fn().mockReturnThis();
316
+ const channelMock = vi.fn().mockReturnValue({
317
+ on: onMock,
318
+ subscribe: vi.fn().mockReturnThis(),
319
+ unsubscribe: vi.fn(),
320
+ });
321
+
322
+ const client = createMockClient({
323
+ realtime: { channel: channelMock },
324
+ } as any);
325
+
326
+ const callback = vi.fn();
327
+ renderHook(
328
+ () => useTableUpdates('products', callback),
329
+ { wrapper: createWrapper(client) }
330
+ );
331
+
332
+ expect(channelMock).toHaveBeenCalledWith('table:products');
333
+ expect(onMock).toHaveBeenCalledWith('UPDATE', expect.any(Function));
334
+ });
335
+ });
336
+
337
+ describe('useTableDeletes', () => {
338
+ it('should subscribe to DELETE events', () => {
339
+ const onMock = vi.fn().mockReturnThis();
340
+ const channelMock = vi.fn().mockReturnValue({
341
+ on: onMock,
342
+ subscribe: vi.fn().mockReturnThis(),
343
+ unsubscribe: vi.fn(),
344
+ });
345
+
346
+ const client = createMockClient({
347
+ realtime: { channel: channelMock },
348
+ } as any);
349
+
350
+ const callback = vi.fn();
351
+ renderHook(
352
+ () => useTableDeletes('products', callback),
353
+ { wrapper: createWrapper(client) }
354
+ );
355
+
356
+ expect(channelMock).toHaveBeenCalledWith('table:products');
357
+ expect(onMock).toHaveBeenCalledWith('DELETE', expect.any(Function));
358
+ });
359
+ });
@@ -47,6 +47,9 @@ export interface UseRealtimeOptions {
47
47
 
48
48
  /**
49
49
  * Hook to subscribe to realtime changes for a channel
50
+ *
51
+ * NOTE: The callback and invalidateKey are stored in refs to prevent
52
+ * subscription recreation on every render when inline functions/arrays are used.
50
53
  */
51
54
  export function useRealtime(options: UseRealtimeOptions) {
52
55
  const client = useFluxbaseClient();
@@ -64,6 +67,17 @@ export function useRealtime(options: UseRealtimeOptions) {
64
67
  enabled = true,
65
68
  } = options;
66
69
 
70
+ // Store callback and invalidateKey in refs to avoid subscription recreation
71
+ // when inline functions/arrays are passed
72
+ const callbackRef = useRef(callback);
73
+ const invalidateKeyRef = useRef(invalidateKey);
74
+ const autoInvalidateRef = useRef(autoInvalidate);
75
+
76
+ // Keep refs up to date
77
+ callbackRef.current = callback;
78
+ invalidateKeyRef.current = invalidateKey;
79
+ autoInvalidateRef.current = autoInvalidate;
80
+
67
81
  useEffect(() => {
68
82
  if (!enabled) {
69
83
  return;
@@ -74,17 +88,17 @@ export function useRealtime(options: UseRealtimeOptions) {
74
88
  channelRef.current = channel;
75
89
 
76
90
  const handleChange = (payload: RealtimePostgresChangesPayload) => {
77
- // Call user callback
78
- if (callback) {
79
- callback(payload);
91
+ // Call user callback (using ref for latest value)
92
+ if (callbackRef.current) {
93
+ callbackRef.current(payload);
80
94
  }
81
95
 
82
96
  // Auto-invalidate queries if enabled
83
- if (autoInvalidate) {
97
+ if (autoInvalidateRef.current) {
84
98
  // Extract table name from channel (e.g., 'table:public.products' -> 'public.products')
85
99
  const tableName = channelName.replace(/^table:/, "");
86
100
 
87
- const key = invalidateKey || ["fluxbase", "table", tableName];
101
+ const key = invalidateKeyRef.current || ["fluxbase", "table", tableName];
88
102
  queryClient.invalidateQueries({ queryKey: key });
89
103
  }
90
104
  };
@@ -95,16 +109,7 @@ export function useRealtime(options: UseRealtimeOptions) {
95
109
  channel.unsubscribe();
96
110
  channelRef.current = null;
97
111
  };
98
- }, [
99
- client,
100
- channelName,
101
- event,
102
- callback,
103
- autoInvalidate,
104
- invalidateKey,
105
- queryClient,
106
- enabled,
107
- ]);
112
+ }, [client, channelName, event, queryClient, enabled]);
108
113
 
109
114
  return {
110
115
  channel: channelRef.current,
@@ -0,0 +1,269 @@
1
+ /**
2
+ * Tests for SAML SSO hooks
3
+ */
4
+
5
+ import { describe, it, expect, vi } from 'vitest';
6
+ import { renderHook, waitFor, act } from '@testing-library/react';
7
+ import {
8
+ useSAMLProviders,
9
+ useGetSAMLLoginUrl,
10
+ useSignInWithSAML,
11
+ useHandleSAMLCallback,
12
+ useSAMLMetadataUrl,
13
+ } from './use-saml';
14
+ import { createMockClient, createWrapper, createTestQueryClient } from './test-utils';
15
+
16
+ describe('useSAMLProviders', () => {
17
+ it('should fetch SAML providers', async () => {
18
+ const mockProviders = [
19
+ { id: '1', name: 'okta', display_name: 'Okta' },
20
+ { id: '2', name: 'azure', display_name: 'Azure AD' },
21
+ ];
22
+ const getSAMLProvidersMock = vi.fn().mockResolvedValue({
23
+ data: { providers: mockProviders },
24
+ error: null,
25
+ });
26
+
27
+ const client = createMockClient({
28
+ auth: { getSAMLProviders: getSAMLProvidersMock },
29
+ } as any);
30
+
31
+ const { result } = renderHook(
32
+ () => useSAMLProviders(),
33
+ { wrapper: createWrapper(client) }
34
+ );
35
+
36
+ await waitFor(() => expect(result.current.isLoading).toBe(false));
37
+ expect(result.current.data).toEqual(mockProviders);
38
+ });
39
+
40
+ it('should throw error on fetch failure', async () => {
41
+ const error = new Error('Failed to fetch');
42
+ const getSAMLProvidersMock = vi.fn().mockResolvedValue({ data: null, error });
43
+
44
+ const client = createMockClient({
45
+ auth: { getSAMLProviders: getSAMLProvidersMock },
46
+ } as any);
47
+
48
+ const { result } = renderHook(
49
+ () => useSAMLProviders(),
50
+ { wrapper: createWrapper(client) }
51
+ );
52
+
53
+ await waitFor(() => expect(result.current.isError).toBe(true));
54
+ expect(result.current.error).toBe(error);
55
+ });
56
+ });
57
+
58
+ describe('useGetSAMLLoginUrl', () => {
59
+ it('should get SAML login URL', async () => {
60
+ const mockUrl = 'https://idp.example.com/sso/saml?SAMLRequest=...';
61
+ const getSAMLLoginUrlMock = vi.fn().mockResolvedValue({
62
+ data: { url: mockUrl },
63
+ error: null,
64
+ });
65
+
66
+ const client = createMockClient({
67
+ auth: { getSAMLLoginUrl: getSAMLLoginUrlMock },
68
+ } as any);
69
+
70
+ const { result } = renderHook(
71
+ () => useGetSAMLLoginUrl(),
72
+ { wrapper: createWrapper(client) }
73
+ );
74
+
75
+ await act(async () => {
76
+ await result.current.mutateAsync({
77
+ provider: 'okta',
78
+ options: { redirectUrl: 'https://app.example.com/callback' },
79
+ });
80
+ });
81
+
82
+ expect(getSAMLLoginUrlMock).toHaveBeenCalledWith('okta', {
83
+ redirectUrl: 'https://app.example.com/callback',
84
+ });
85
+ });
86
+
87
+ it('should get SAML login URL without options', async () => {
88
+ const getSAMLLoginUrlMock = vi.fn().mockResolvedValue({
89
+ data: { url: 'https://idp.example.com/sso' },
90
+ error: null,
91
+ });
92
+
93
+ const client = createMockClient({
94
+ auth: { getSAMLLoginUrl: getSAMLLoginUrlMock },
95
+ } as any);
96
+
97
+ const { result } = renderHook(
98
+ () => useGetSAMLLoginUrl(),
99
+ { wrapper: createWrapper(client) }
100
+ );
101
+
102
+ await act(async () => {
103
+ await result.current.mutateAsync({ provider: 'okta' });
104
+ });
105
+
106
+ expect(getSAMLLoginUrlMock).toHaveBeenCalledWith('okta', undefined);
107
+ });
108
+ });
109
+
110
+ describe('useSignInWithSAML', () => {
111
+ it('should initiate SAML sign in', async () => {
112
+ const signInWithSAMLMock = vi.fn().mockResolvedValue({ data: null, error: null });
113
+
114
+ const client = createMockClient({
115
+ auth: { signInWithSAML: signInWithSAMLMock },
116
+ } as any);
117
+
118
+ const { result } = renderHook(
119
+ () => useSignInWithSAML(),
120
+ { wrapper: createWrapper(client) }
121
+ );
122
+
123
+ await act(async () => {
124
+ await result.current.mutateAsync({ provider: 'okta' });
125
+ });
126
+
127
+ expect(signInWithSAMLMock).toHaveBeenCalledWith('okta', undefined);
128
+ });
129
+
130
+ it('should pass options to sign in', async () => {
131
+ const signInWithSAMLMock = vi.fn().mockResolvedValue({ data: null, error: null });
132
+
133
+ const client = createMockClient({
134
+ auth: { signInWithSAML: signInWithSAMLMock },
135
+ } as any);
136
+
137
+ const { result } = renderHook(
138
+ () => useSignInWithSAML(),
139
+ { wrapper: createWrapper(client) }
140
+ );
141
+
142
+ await act(async () => {
143
+ await result.current.mutateAsync({
144
+ provider: 'okta',
145
+ options: { redirectUrl: 'https://app.example.com' },
146
+ });
147
+ });
148
+
149
+ expect(signInWithSAMLMock).toHaveBeenCalledWith('okta', {
150
+ redirectUrl: 'https://app.example.com',
151
+ });
152
+ });
153
+ });
154
+
155
+ describe('useHandleSAMLCallback', () => {
156
+ it('should handle SAML callback successfully', async () => {
157
+ const mockResult = {
158
+ data: {
159
+ user: { id: '1', email: 'user@example.com' },
160
+ session: { access_token: 'token' },
161
+ },
162
+ error: null,
163
+ };
164
+ const handleSAMLCallbackMock = vi.fn().mockResolvedValue(mockResult);
165
+
166
+ const client = createMockClient({
167
+ auth: { handleSAMLCallback: handleSAMLCallbackMock },
168
+ } as any);
169
+
170
+ const { result } = renderHook(
171
+ () => useHandleSAMLCallback(),
172
+ { wrapper: createWrapper(client) }
173
+ );
174
+
175
+ let response;
176
+ await act(async () => {
177
+ response = await result.current.mutateAsync({ samlResponse: 'base64-response' });
178
+ });
179
+
180
+ expect(handleSAMLCallbackMock).toHaveBeenCalledWith('base64-response', undefined);
181
+ expect(response).toEqual(mockResult);
182
+ expect(result.current.isSuccess).toBe(true);
183
+ });
184
+
185
+ it('should pass provider to callback handler', async () => {
186
+ const handleSAMLCallbackMock = vi.fn().mockResolvedValue({
187
+ data: { user: {}, session: {} },
188
+ error: null,
189
+ });
190
+
191
+ const client = createMockClient({
192
+ auth: { handleSAMLCallback: handleSAMLCallbackMock },
193
+ } as any);
194
+
195
+ const { result } = renderHook(
196
+ () => useHandleSAMLCallback(),
197
+ { wrapper: createWrapper(client) }
198
+ );
199
+
200
+ await act(async () => {
201
+ await result.current.mutateAsync({
202
+ samlResponse: 'base64-response',
203
+ provider: 'okta',
204
+ });
205
+ });
206
+
207
+ expect(handleSAMLCallbackMock).toHaveBeenCalledWith('base64-response', 'okta');
208
+ });
209
+
210
+ it('should not update cache when no data', async () => {
211
+ const handleSAMLCallbackMock = vi.fn().mockResolvedValue({
212
+ data: null,
213
+ error: null,
214
+ });
215
+
216
+ const client = createMockClient({
217
+ auth: { handleSAMLCallback: handleSAMLCallbackMock },
218
+ } as any);
219
+
220
+ const queryClient = createTestQueryClient();
221
+ const { result } = renderHook(
222
+ () => useHandleSAMLCallback(),
223
+ { wrapper: createWrapper(client, queryClient) }
224
+ );
225
+
226
+ await act(async () => {
227
+ await result.current.mutateAsync({ samlResponse: 'base64-response' });
228
+ });
229
+
230
+ // Cache should not be updated
231
+ expect(queryClient.getQueryData(['fluxbase', 'auth', 'session'])).toBeUndefined();
232
+ });
233
+ });
234
+
235
+ describe('useSAMLMetadataUrl', () => {
236
+ it('should return metadata URL generator function', () => {
237
+ const getSAMLMetadataUrlMock = vi.fn().mockReturnValue('https://api.example.com/saml/metadata/okta');
238
+
239
+ const client = createMockClient({
240
+ auth: { getSAMLMetadataUrl: getSAMLMetadataUrlMock },
241
+ } as any);
242
+
243
+ const { result } = renderHook(
244
+ () => useSAMLMetadataUrl(),
245
+ { wrapper: createWrapper(client) }
246
+ );
247
+
248
+ const url = result.current('okta');
249
+
250
+ expect(getSAMLMetadataUrlMock).toHaveBeenCalledWith('okta');
251
+ expect(url).toBe('https://api.example.com/saml/metadata/okta');
252
+ });
253
+
254
+ it('should work with different providers', () => {
255
+ const getSAMLMetadataUrlMock = vi.fn().mockImplementation((provider) => `https://api.example.com/saml/metadata/${provider}`);
256
+
257
+ const client = createMockClient({
258
+ auth: { getSAMLMetadataUrl: getSAMLMetadataUrlMock },
259
+ } as any);
260
+
261
+ const { result } = renderHook(
262
+ () => useSAMLMetadataUrl(),
263
+ { wrapper: createWrapper(client) }
264
+ );
265
+
266
+ expect(result.current('okta')).toBe('https://api.example.com/saml/metadata/okta');
267
+ expect(result.current('azure')).toBe('https://api.example.com/saml/metadata/azure');
268
+ });
269
+ });