@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.
- package/dist/index.d.mts +33 -2
- package/dist/index.d.ts +33 -2
- package/dist/index.js +33 -21
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +33 -21
- package/dist/index.mjs.map +1 -1
- package/package.json +20 -10
- package/src/context.test.tsx +147 -0
- package/src/index.test.ts +255 -0
- package/src/test-setup.ts +22 -0
- package/src/test-utils.tsx +215 -0
- package/src/use-admin-auth.test.ts +175 -0
- package/src/use-admin-auth.ts +10 -2
- package/src/use-admin-hooks.test.ts +457 -0
- package/src/use-auth-config.test.ts +145 -0
- package/src/use-auth.test.ts +313 -0
- package/src/use-auth.ts +2 -1
- package/src/use-captcha.test.ts +273 -0
- package/src/use-client-keys.test.ts +286 -0
- package/src/use-graphql.test.ts +424 -0
- package/src/use-query.test.ts +348 -0
- package/src/use-query.ts +50 -4
- package/src/use-realtime.test.ts +359 -0
- package/src/use-realtime.ts +20 -15
- package/src/use-saml.test.ts +269 -0
- package/src/use-storage.test.ts +549 -0
- package/src/use-storage.ts +10 -2
- package/src/use-users.test.ts +264 -0
- package/vitest.config.ts +22 -0
|
@@ -0,0 +1,549 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for storage hooks
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
6
|
+
import { renderHook, waitFor, act } from '@testing-library/react';
|
|
7
|
+
import {
|
|
8
|
+
useStorageList,
|
|
9
|
+
useStorageUpload,
|
|
10
|
+
useStorageUploadWithProgress,
|
|
11
|
+
useStorageDownload,
|
|
12
|
+
useStorageDelete,
|
|
13
|
+
useStoragePublicUrl,
|
|
14
|
+
useStorageTransformUrl,
|
|
15
|
+
useStorageSignedUrl,
|
|
16
|
+
useStorageSignedUrlWithOptions,
|
|
17
|
+
useStorageMove,
|
|
18
|
+
useStorageCopy,
|
|
19
|
+
useStorageBuckets,
|
|
20
|
+
useCreateBucket,
|
|
21
|
+
useDeleteBucket,
|
|
22
|
+
} from './use-storage';
|
|
23
|
+
import { createMockClient, createWrapper, createTestQueryClient } from './test-utils';
|
|
24
|
+
|
|
25
|
+
describe('useStorageList', () => {
|
|
26
|
+
it('should list files in bucket', async () => {
|
|
27
|
+
const mockFiles = [{ name: 'file1.txt' }, { name: 'file2.txt' }];
|
|
28
|
+
const listMock = vi.fn().mockResolvedValue({ data: mockFiles, error: null });
|
|
29
|
+
const fromMock = vi.fn().mockReturnValue({ list: listMock });
|
|
30
|
+
|
|
31
|
+
const client = createMockClient({
|
|
32
|
+
storage: { from: fromMock },
|
|
33
|
+
} as any);
|
|
34
|
+
|
|
35
|
+
const { result } = renderHook(
|
|
36
|
+
() => useStorageList('bucket'),
|
|
37
|
+
{ wrapper: createWrapper(client) }
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
await waitFor(() => expect(result.current.isLoading).toBe(false));
|
|
41
|
+
expect(result.current.data).toEqual(mockFiles);
|
|
42
|
+
expect(fromMock).toHaveBeenCalledWith('bucket');
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('should pass list options', async () => {
|
|
46
|
+
const listMock = vi.fn().mockResolvedValue({ data: [], error: null });
|
|
47
|
+
const fromMock = vi.fn().mockReturnValue({ list: listMock });
|
|
48
|
+
|
|
49
|
+
const client = createMockClient({
|
|
50
|
+
storage: { from: fromMock },
|
|
51
|
+
} as any);
|
|
52
|
+
|
|
53
|
+
renderHook(
|
|
54
|
+
() => useStorageList('bucket', { prefix: 'folder/', limit: 10, offset: 5 }),
|
|
55
|
+
{ wrapper: createWrapper(client) }
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
await waitFor(() => {
|
|
59
|
+
expect(listMock).toHaveBeenCalledWith({ prefix: 'folder/', limit: 10, offset: 5 });
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('should throw error on list failure', async () => {
|
|
64
|
+
const error = new Error('List failed');
|
|
65
|
+
const listMock = vi.fn().mockResolvedValue({ data: null, error });
|
|
66
|
+
const fromMock = vi.fn().mockReturnValue({ list: listMock });
|
|
67
|
+
|
|
68
|
+
const client = createMockClient({
|
|
69
|
+
storage: { from: fromMock },
|
|
70
|
+
} as any);
|
|
71
|
+
|
|
72
|
+
const { result } = renderHook(
|
|
73
|
+
() => useStorageList('bucket'),
|
|
74
|
+
{ wrapper: createWrapper(client) }
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
await waitFor(() => expect(result.current.isError).toBe(true));
|
|
78
|
+
expect(result.current.error).toBe(error);
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
describe('useStorageUpload', () => {
|
|
83
|
+
it('should upload file and invalidate queries', async () => {
|
|
84
|
+
const mockResult = { path: 'file.txt' };
|
|
85
|
+
const uploadMock = vi.fn().mockResolvedValue({ data: mockResult, error: null });
|
|
86
|
+
const fromMock = vi.fn().mockReturnValue({ upload: uploadMock });
|
|
87
|
+
|
|
88
|
+
const client = createMockClient({
|
|
89
|
+
storage: { from: fromMock },
|
|
90
|
+
} as any);
|
|
91
|
+
|
|
92
|
+
const queryClient = createTestQueryClient();
|
|
93
|
+
const invalidateSpy = vi.spyOn(queryClient, 'invalidateQueries');
|
|
94
|
+
|
|
95
|
+
const { result } = renderHook(() => useStorageUpload('bucket'), {
|
|
96
|
+
wrapper: createWrapper(client, queryClient),
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
const file = new Blob(['content']);
|
|
100
|
+
await act(async () => {
|
|
101
|
+
await result.current.mutateAsync({ path: 'file.txt', file });
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
expect(fromMock).toHaveBeenCalledWith('bucket');
|
|
105
|
+
expect(uploadMock).toHaveBeenCalledWith('file.txt', file, undefined);
|
|
106
|
+
expect(invalidateSpy).toHaveBeenCalledWith({ queryKey: ['fluxbase', 'storage', 'bucket', 'list'] });
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it('should throw error on upload failure', async () => {
|
|
110
|
+
const error = new Error('Upload failed');
|
|
111
|
+
const uploadMock = vi.fn().mockResolvedValue({ data: null, error });
|
|
112
|
+
const fromMock = vi.fn().mockReturnValue({ upload: uploadMock });
|
|
113
|
+
|
|
114
|
+
const client = createMockClient({
|
|
115
|
+
storage: { from: fromMock },
|
|
116
|
+
} as any);
|
|
117
|
+
|
|
118
|
+
const { result } = renderHook(() => useStorageUpload('bucket'), {
|
|
119
|
+
wrapper: createWrapper(client),
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
const file = new Blob(['content']);
|
|
123
|
+
await expect(act(async () => {
|
|
124
|
+
await result.current.mutateAsync({ path: 'file.txt', file });
|
|
125
|
+
})).rejects.toThrow('Upload failed');
|
|
126
|
+
});
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
describe('useStorageUploadWithProgress', () => {
|
|
130
|
+
it('should track upload progress', async () => {
|
|
131
|
+
let progressCallback: Function | undefined;
|
|
132
|
+
let resolveUpload: Function;
|
|
133
|
+
|
|
134
|
+
const uploadMock = vi.fn().mockImplementation((path, file, options) => {
|
|
135
|
+
progressCallback = options?.onUploadProgress;
|
|
136
|
+
return new Promise((resolve) => {
|
|
137
|
+
resolveUpload = () => resolve({ data: { path }, error: null });
|
|
138
|
+
});
|
|
139
|
+
});
|
|
140
|
+
const fromMock = vi.fn().mockReturnValue({ upload: uploadMock });
|
|
141
|
+
|
|
142
|
+
const client = createMockClient({
|
|
143
|
+
storage: { from: fromMock },
|
|
144
|
+
} as any);
|
|
145
|
+
|
|
146
|
+
const { result } = renderHook(() => useStorageUploadWithProgress('bucket'), {
|
|
147
|
+
wrapper: createWrapper(client),
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
const file = new Blob(['content']);
|
|
151
|
+
|
|
152
|
+
// Start upload (don't await yet)
|
|
153
|
+
let uploadPromise: Promise<any>;
|
|
154
|
+
act(() => {
|
|
155
|
+
uploadPromise = result.current.upload.mutateAsync({ path: 'file.txt', file });
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
// Wait for upload to start and callback to be assigned
|
|
159
|
+
await waitFor(() => {
|
|
160
|
+
expect(progressCallback).toBeDefined();
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
// Simulate progress
|
|
164
|
+
act(() => {
|
|
165
|
+
progressCallback!({ loaded: 50, total: 100, percentage: 50 });
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
// Check progress state
|
|
169
|
+
expect(result.current.progress).toEqual({ loaded: 50, total: 100, percentage: 50 });
|
|
170
|
+
|
|
171
|
+
// Resolve upload
|
|
172
|
+
await act(async () => {
|
|
173
|
+
resolveUpload!();
|
|
174
|
+
await uploadPromise;
|
|
175
|
+
});
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
it('should reset progress on error', async () => {
|
|
179
|
+
const uploadMock = vi.fn().mockResolvedValue({ data: null, error: new Error('Failed') });
|
|
180
|
+
const fromMock = vi.fn().mockReturnValue({ upload: uploadMock });
|
|
181
|
+
|
|
182
|
+
const client = createMockClient({
|
|
183
|
+
storage: { from: fromMock },
|
|
184
|
+
} as any);
|
|
185
|
+
|
|
186
|
+
const { result } = renderHook(() => useStorageUploadWithProgress('bucket'), {
|
|
187
|
+
wrapper: createWrapper(client),
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
const file = new Blob(['content']);
|
|
191
|
+
try {
|
|
192
|
+
await act(async () => {
|
|
193
|
+
await result.current.upload.mutateAsync({ path: 'file.txt', file });
|
|
194
|
+
});
|
|
195
|
+
} catch {
|
|
196
|
+
// Expected error
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
expect(result.current.progress).toBeNull();
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
it('should have reset function', () => {
|
|
203
|
+
const client = createMockClient();
|
|
204
|
+
|
|
205
|
+
const { result } = renderHook(() => useStorageUploadWithProgress('bucket'), {
|
|
206
|
+
wrapper: createWrapper(client),
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
expect(result.current.reset).toBeDefined();
|
|
210
|
+
expect(typeof result.current.reset).toBe('function');
|
|
211
|
+
});
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
describe('useStorageDownload', () => {
|
|
215
|
+
it('should download file', async () => {
|
|
216
|
+
const mockBlob = new Blob(['content']);
|
|
217
|
+
const downloadMock = vi.fn().mockResolvedValue({ data: mockBlob, error: null });
|
|
218
|
+
const fromMock = vi.fn().mockReturnValue({ download: downloadMock });
|
|
219
|
+
|
|
220
|
+
const client = createMockClient({
|
|
221
|
+
storage: { from: fromMock },
|
|
222
|
+
} as any);
|
|
223
|
+
|
|
224
|
+
const { result } = renderHook(
|
|
225
|
+
() => useStorageDownload('bucket', 'file.txt'),
|
|
226
|
+
{ wrapper: createWrapper(client) }
|
|
227
|
+
);
|
|
228
|
+
|
|
229
|
+
await waitFor(() => expect(result.current.isLoading).toBe(false));
|
|
230
|
+
expect(result.current.data).toBe(mockBlob);
|
|
231
|
+
expect(downloadMock).toHaveBeenCalledWith('file.txt');
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
it('should not fetch when path is null', async () => {
|
|
235
|
+
const downloadMock = vi.fn();
|
|
236
|
+
const fromMock = vi.fn().mockReturnValue({ download: downloadMock });
|
|
237
|
+
|
|
238
|
+
const client = createMockClient({
|
|
239
|
+
storage: { from: fromMock },
|
|
240
|
+
} as any);
|
|
241
|
+
|
|
242
|
+
const { result } = renderHook(
|
|
243
|
+
() => useStorageDownload('bucket', null),
|
|
244
|
+
{ wrapper: createWrapper(client) }
|
|
245
|
+
);
|
|
246
|
+
|
|
247
|
+
await waitFor(() => expect(result.current.isLoading).toBe(false));
|
|
248
|
+
expect(downloadMock).not.toHaveBeenCalled();
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
it('should not fetch when disabled', async () => {
|
|
252
|
+
const downloadMock = vi.fn();
|
|
253
|
+
const fromMock = vi.fn().mockReturnValue({ download: downloadMock });
|
|
254
|
+
|
|
255
|
+
const client = createMockClient({
|
|
256
|
+
storage: { from: fromMock },
|
|
257
|
+
} as any);
|
|
258
|
+
|
|
259
|
+
const { result } = renderHook(
|
|
260
|
+
() => useStorageDownload('bucket', 'file.txt', false),
|
|
261
|
+
{ wrapper: createWrapper(client) }
|
|
262
|
+
);
|
|
263
|
+
|
|
264
|
+
await waitFor(() => expect(result.current.isLoading).toBe(false));
|
|
265
|
+
expect(downloadMock).not.toHaveBeenCalled();
|
|
266
|
+
});
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
describe('useStorageDelete', () => {
|
|
270
|
+
it('should delete files and invalidate queries', async () => {
|
|
271
|
+
const removeMock = vi.fn().mockResolvedValue({ error: null });
|
|
272
|
+
const fromMock = vi.fn().mockReturnValue({ remove: removeMock });
|
|
273
|
+
|
|
274
|
+
const client = createMockClient({
|
|
275
|
+
storage: { from: fromMock },
|
|
276
|
+
} as any);
|
|
277
|
+
|
|
278
|
+
const queryClient = createTestQueryClient();
|
|
279
|
+
const invalidateSpy = vi.spyOn(queryClient, 'invalidateQueries');
|
|
280
|
+
|
|
281
|
+
const { result } = renderHook(() => useStorageDelete('bucket'), {
|
|
282
|
+
wrapper: createWrapper(client, queryClient),
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
await act(async () => {
|
|
286
|
+
await result.current.mutateAsync(['file1.txt', 'file2.txt']);
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
expect(removeMock).toHaveBeenCalledWith(['file1.txt', 'file2.txt']);
|
|
290
|
+
expect(invalidateSpy).toHaveBeenCalledWith({ queryKey: ['fluxbase', 'storage', 'bucket', 'list'] });
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
it('should throw error on delete failure', async () => {
|
|
294
|
+
const error = new Error('Delete failed');
|
|
295
|
+
const removeMock = vi.fn().mockResolvedValue({ error });
|
|
296
|
+
const fromMock = vi.fn().mockReturnValue({ remove: removeMock });
|
|
297
|
+
|
|
298
|
+
const client = createMockClient({
|
|
299
|
+
storage: { from: fromMock },
|
|
300
|
+
} as any);
|
|
301
|
+
|
|
302
|
+
const { result } = renderHook(() => useStorageDelete('bucket'), {
|
|
303
|
+
wrapper: createWrapper(client),
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
await expect(act(async () => {
|
|
307
|
+
await result.current.mutateAsync(['file.txt']);
|
|
308
|
+
})).rejects.toThrow('Delete failed');
|
|
309
|
+
});
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
describe('useStoragePublicUrl', () => {
|
|
313
|
+
it('should return public URL', () => {
|
|
314
|
+
const getPublicUrlMock = vi.fn().mockReturnValue({ data: { publicUrl: 'http://example.com/file' } });
|
|
315
|
+
const fromMock = vi.fn().mockReturnValue({ getPublicUrl: getPublicUrlMock });
|
|
316
|
+
|
|
317
|
+
const client = createMockClient({
|
|
318
|
+
storage: { from: fromMock },
|
|
319
|
+
} as any);
|
|
320
|
+
|
|
321
|
+
const { result } = renderHook(
|
|
322
|
+
() => useStoragePublicUrl('bucket', 'file.txt'),
|
|
323
|
+
{ wrapper: createWrapper(client) }
|
|
324
|
+
);
|
|
325
|
+
|
|
326
|
+
expect(result.current).toBe('http://example.com/file');
|
|
327
|
+
expect(getPublicUrlMock).toHaveBeenCalledWith('file.txt');
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
it('should return null when path is null', () => {
|
|
331
|
+
const client = createMockClient();
|
|
332
|
+
|
|
333
|
+
const { result } = renderHook(
|
|
334
|
+
() => useStoragePublicUrl('bucket', null),
|
|
335
|
+
{ wrapper: createWrapper(client) }
|
|
336
|
+
);
|
|
337
|
+
|
|
338
|
+
expect(result.current).toBeNull();
|
|
339
|
+
});
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
describe('useStorageTransformUrl', () => {
|
|
343
|
+
it('should return transform URL', () => {
|
|
344
|
+
const getTransformUrlMock = vi.fn().mockReturnValue('http://example.com/transform/file');
|
|
345
|
+
const fromMock = vi.fn().mockReturnValue({ getTransformUrl: getTransformUrlMock });
|
|
346
|
+
|
|
347
|
+
const client = createMockClient({
|
|
348
|
+
storage: { from: fromMock },
|
|
349
|
+
} as any);
|
|
350
|
+
|
|
351
|
+
const { result } = renderHook(
|
|
352
|
+
() => useStorageTransformUrl('bucket', 'file.jpg', { width: 100, height: 100 }),
|
|
353
|
+
{ wrapper: createWrapper(client) }
|
|
354
|
+
);
|
|
355
|
+
|
|
356
|
+
expect(result.current).toBe('http://example.com/transform/file');
|
|
357
|
+
expect(getTransformUrlMock).toHaveBeenCalledWith('file.jpg', { width: 100, height: 100 });
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
it('should return null when path is null', () => {
|
|
361
|
+
const client = createMockClient();
|
|
362
|
+
|
|
363
|
+
const { result } = renderHook(
|
|
364
|
+
() => useStorageTransformUrl('bucket', null, { width: 100 }),
|
|
365
|
+
{ wrapper: createWrapper(client) }
|
|
366
|
+
);
|
|
367
|
+
|
|
368
|
+
expect(result.current).toBeNull();
|
|
369
|
+
});
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
describe('useStorageSignedUrl', () => {
|
|
373
|
+
it('should fetch signed URL', async () => {
|
|
374
|
+
const createSignedUrlMock = vi.fn().mockResolvedValue({ data: { signedUrl: 'http://example.com/signed' }, error: null });
|
|
375
|
+
const fromMock = vi.fn().mockReturnValue({ createSignedUrl: createSignedUrlMock });
|
|
376
|
+
|
|
377
|
+
const client = createMockClient({
|
|
378
|
+
storage: { from: fromMock },
|
|
379
|
+
} as any);
|
|
380
|
+
|
|
381
|
+
const { result } = renderHook(
|
|
382
|
+
() => useStorageSignedUrl('bucket', 'file.txt', 3600),
|
|
383
|
+
{ wrapper: createWrapper(client) }
|
|
384
|
+
);
|
|
385
|
+
|
|
386
|
+
await waitFor(() => expect(result.current.isLoading).toBe(false));
|
|
387
|
+
expect(result.current.data).toBe('http://example.com/signed');
|
|
388
|
+
expect(createSignedUrlMock).toHaveBeenCalledWith('file.txt', { expiresIn: 3600 });
|
|
389
|
+
});
|
|
390
|
+
|
|
391
|
+
it('should not fetch when path is null', async () => {
|
|
392
|
+
const createSignedUrlMock = vi.fn();
|
|
393
|
+
const fromMock = vi.fn().mockReturnValue({ createSignedUrl: createSignedUrlMock });
|
|
394
|
+
|
|
395
|
+
const client = createMockClient({
|
|
396
|
+
storage: { from: fromMock },
|
|
397
|
+
} as any);
|
|
398
|
+
|
|
399
|
+
const { result } = renderHook(
|
|
400
|
+
() => useStorageSignedUrl('bucket', null),
|
|
401
|
+
{ wrapper: createWrapper(client) }
|
|
402
|
+
);
|
|
403
|
+
|
|
404
|
+
await waitFor(() => expect(result.current.isLoading).toBe(false));
|
|
405
|
+
expect(createSignedUrlMock).not.toHaveBeenCalled();
|
|
406
|
+
});
|
|
407
|
+
});
|
|
408
|
+
|
|
409
|
+
describe('useStorageSignedUrlWithOptions', () => {
|
|
410
|
+
it('should fetch signed URL with transform options', async () => {
|
|
411
|
+
const createSignedUrlMock = vi.fn().mockResolvedValue({ data: { signedUrl: 'http://example.com/signed' }, error: null });
|
|
412
|
+
const fromMock = vi.fn().mockReturnValue({ createSignedUrl: createSignedUrlMock });
|
|
413
|
+
|
|
414
|
+
const client = createMockClient({
|
|
415
|
+
storage: { from: fromMock },
|
|
416
|
+
} as any);
|
|
417
|
+
|
|
418
|
+
const options = {
|
|
419
|
+
expiresIn: 3600,
|
|
420
|
+
transform: { width: 100, height: 100 },
|
|
421
|
+
};
|
|
422
|
+
|
|
423
|
+
const { result } = renderHook(
|
|
424
|
+
() => useStorageSignedUrlWithOptions('bucket', 'file.jpg', options),
|
|
425
|
+
{ wrapper: createWrapper(client) }
|
|
426
|
+
);
|
|
427
|
+
|
|
428
|
+
await waitFor(() => expect(result.current.isLoading).toBe(false));
|
|
429
|
+
expect(result.current.data).toBe('http://example.com/signed');
|
|
430
|
+
expect(createSignedUrlMock).toHaveBeenCalledWith('file.jpg', options);
|
|
431
|
+
});
|
|
432
|
+
});
|
|
433
|
+
|
|
434
|
+
describe('useStorageMove', () => {
|
|
435
|
+
it('should move file and invalidate queries', async () => {
|
|
436
|
+
const moveMock = vi.fn().mockResolvedValue({ data: { path: 'new.txt' }, error: null });
|
|
437
|
+
const fromMock = vi.fn().mockReturnValue({ move: moveMock });
|
|
438
|
+
|
|
439
|
+
const client = createMockClient({
|
|
440
|
+
storage: { from: fromMock },
|
|
441
|
+
} as any);
|
|
442
|
+
|
|
443
|
+
const queryClient = createTestQueryClient();
|
|
444
|
+
const invalidateSpy = vi.spyOn(queryClient, 'invalidateQueries');
|
|
445
|
+
|
|
446
|
+
const { result } = renderHook(() => useStorageMove('bucket'), {
|
|
447
|
+
wrapper: createWrapper(client, queryClient),
|
|
448
|
+
});
|
|
449
|
+
|
|
450
|
+
await act(async () => {
|
|
451
|
+
await result.current.mutateAsync({ fromPath: 'old.txt', toPath: 'new.txt' });
|
|
452
|
+
});
|
|
453
|
+
|
|
454
|
+
expect(moveMock).toHaveBeenCalledWith('old.txt', 'new.txt');
|
|
455
|
+
expect(invalidateSpy).toHaveBeenCalledWith({ queryKey: ['fluxbase', 'storage', 'bucket', 'list'] });
|
|
456
|
+
});
|
|
457
|
+
});
|
|
458
|
+
|
|
459
|
+
describe('useStorageCopy', () => {
|
|
460
|
+
it('should copy file and invalidate queries', async () => {
|
|
461
|
+
const copyMock = vi.fn().mockResolvedValue({ data: { path: 'copy.txt' }, error: null });
|
|
462
|
+
const fromMock = vi.fn().mockReturnValue({ copy: copyMock });
|
|
463
|
+
|
|
464
|
+
const client = createMockClient({
|
|
465
|
+
storage: { from: fromMock },
|
|
466
|
+
} as any);
|
|
467
|
+
|
|
468
|
+
const queryClient = createTestQueryClient();
|
|
469
|
+
const invalidateSpy = vi.spyOn(queryClient, 'invalidateQueries');
|
|
470
|
+
|
|
471
|
+
const { result } = renderHook(() => useStorageCopy('bucket'), {
|
|
472
|
+
wrapper: createWrapper(client, queryClient),
|
|
473
|
+
});
|
|
474
|
+
|
|
475
|
+
await act(async () => {
|
|
476
|
+
await result.current.mutateAsync({ fromPath: 'source.txt', toPath: 'copy.txt' });
|
|
477
|
+
});
|
|
478
|
+
|
|
479
|
+
expect(copyMock).toHaveBeenCalledWith('source.txt', 'copy.txt');
|
|
480
|
+
expect(invalidateSpy).toHaveBeenCalledWith({ queryKey: ['fluxbase', 'storage', 'bucket', 'list'] });
|
|
481
|
+
});
|
|
482
|
+
});
|
|
483
|
+
|
|
484
|
+
describe('useStorageBuckets', () => {
|
|
485
|
+
it('should list buckets', async () => {
|
|
486
|
+
const mockBuckets = [{ name: 'bucket1' }, { name: 'bucket2' }];
|
|
487
|
+
const listBucketsMock = vi.fn().mockResolvedValue({ data: mockBuckets, error: null });
|
|
488
|
+
|
|
489
|
+
const client = createMockClient({
|
|
490
|
+
storage: { listBuckets: listBucketsMock },
|
|
491
|
+
} as any);
|
|
492
|
+
|
|
493
|
+
const { result } = renderHook(
|
|
494
|
+
() => useStorageBuckets(),
|
|
495
|
+
{ wrapper: createWrapper(client) }
|
|
496
|
+
);
|
|
497
|
+
|
|
498
|
+
await waitFor(() => expect(result.current.isLoading).toBe(false));
|
|
499
|
+
expect(result.current.data).toEqual(mockBuckets);
|
|
500
|
+
});
|
|
501
|
+
});
|
|
502
|
+
|
|
503
|
+
describe('useCreateBucket', () => {
|
|
504
|
+
it('should create bucket and invalidate queries', async () => {
|
|
505
|
+
const createBucketMock = vi.fn().mockResolvedValue({ error: null });
|
|
506
|
+
|
|
507
|
+
const client = createMockClient({
|
|
508
|
+
storage: { createBucket: createBucketMock },
|
|
509
|
+
} as any);
|
|
510
|
+
|
|
511
|
+
const queryClient = createTestQueryClient();
|
|
512
|
+
const invalidateSpy = vi.spyOn(queryClient, 'invalidateQueries');
|
|
513
|
+
|
|
514
|
+
const { result } = renderHook(() => useCreateBucket(), {
|
|
515
|
+
wrapper: createWrapper(client, queryClient),
|
|
516
|
+
});
|
|
517
|
+
|
|
518
|
+
await act(async () => {
|
|
519
|
+
await result.current.mutateAsync('new-bucket');
|
|
520
|
+
});
|
|
521
|
+
|
|
522
|
+
expect(createBucketMock).toHaveBeenCalledWith('new-bucket');
|
|
523
|
+
expect(invalidateSpy).toHaveBeenCalledWith({ queryKey: ['fluxbase', 'storage', 'buckets'] });
|
|
524
|
+
});
|
|
525
|
+
});
|
|
526
|
+
|
|
527
|
+
describe('useDeleteBucket', () => {
|
|
528
|
+
it('should delete bucket and invalidate queries', async () => {
|
|
529
|
+
const deleteBucketMock = vi.fn().mockResolvedValue({ error: null });
|
|
530
|
+
|
|
531
|
+
const client = createMockClient({
|
|
532
|
+
storage: { deleteBucket: deleteBucketMock },
|
|
533
|
+
} as any);
|
|
534
|
+
|
|
535
|
+
const queryClient = createTestQueryClient();
|
|
536
|
+
const invalidateSpy = vi.spyOn(queryClient, 'invalidateQueries');
|
|
537
|
+
|
|
538
|
+
const { result } = renderHook(() => useDeleteBucket(), {
|
|
539
|
+
wrapper: createWrapper(client, queryClient),
|
|
540
|
+
});
|
|
541
|
+
|
|
542
|
+
await act(async () => {
|
|
543
|
+
await result.current.mutateAsync('bucket-to-delete');
|
|
544
|
+
});
|
|
545
|
+
|
|
546
|
+
expect(deleteBucketMock).toHaveBeenCalledWith('bucket-to-delete');
|
|
547
|
+
expect(invalidateSpy).toHaveBeenCalledWith({ queryKey: ['fluxbase', 'storage', 'buckets'] });
|
|
548
|
+
});
|
|
549
|
+
});
|
package/src/use-storage.ts
CHANGED
|
@@ -305,7 +305,11 @@ export function useStorageSignedUrl(
|
|
|
305
305
|
return data?.signedUrl || null;
|
|
306
306
|
},
|
|
307
307
|
enabled: !!path,
|
|
308
|
-
|
|
308
|
+
// Refresh 1 minute before expiry, but ensure staleTime is never negative
|
|
309
|
+
// For very short expirations (<60s), use half the expiration time
|
|
310
|
+
staleTime: expiresIn
|
|
311
|
+
? Math.max(expiresIn * 500, expiresIn * 1000 - 60000) // At least half the expiration time
|
|
312
|
+
: 1000 * 60 * 50, // 50 minutes default
|
|
309
313
|
});
|
|
310
314
|
}
|
|
311
315
|
|
|
@@ -373,7 +377,11 @@ export function useStorageSignedUrlWithOptions(
|
|
|
373
377
|
return data?.signedUrl || null;
|
|
374
378
|
},
|
|
375
379
|
enabled: !!path,
|
|
376
|
-
|
|
380
|
+
// Refresh 1 minute before expiry, but ensure staleTime is never negative
|
|
381
|
+
// For very short expirations (<60s), use half the expiration time
|
|
382
|
+
staleTime: expiresIn
|
|
383
|
+
? Math.max(expiresIn * 500, expiresIn * 1000 - 60000) // At least half the expiration time
|
|
384
|
+
: 1000 * 60 * 50, // 50 minutes default
|
|
377
385
|
});
|
|
378
386
|
}
|
|
379
387
|
|