@geekmidas/client 0.4.0 → 0.5.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.
Files changed (71) hide show
  1. package/dist/auth-fetcher.cjs +0 -1
  2. package/dist/auth-fetcher.cjs.map +1 -1
  3. package/dist/auth-fetcher.d.cts +1 -1
  4. package/dist/auth-fetcher.d.cts.map +1 -1
  5. package/dist/auth-fetcher.d.mts +1 -1
  6. package/dist/auth-fetcher.d.mts.map +1 -1
  7. package/dist/auth-fetcher.mjs +0 -1
  8. package/dist/auth-fetcher.mjs.map +1 -1
  9. package/dist/endpoint-hooks.cjs +10 -5
  10. package/dist/endpoint-hooks.cjs.map +1 -1
  11. package/dist/endpoint-hooks.d.cts +2 -2
  12. package/dist/endpoint-hooks.d.cts.map +1 -1
  13. package/dist/endpoint-hooks.d.mts +2 -2
  14. package/dist/endpoint-hooks.d.mts.map +1 -1
  15. package/dist/endpoint-hooks.mjs +10 -5
  16. package/dist/endpoint-hooks.mjs.map +1 -1
  17. package/dist/fetcher-5fnBE7Dk.mjs.map +1 -1
  18. package/dist/fetcher-CgLEaIAC.cjs.map +1 -1
  19. package/dist/fetcher.d.cts +1 -1
  20. package/dist/fetcher.d.cts.map +1 -1
  21. package/dist/fetcher.d.mts +1 -1
  22. package/dist/fetcher.d.mts.map +1 -1
  23. package/dist/infer.d.cts.map +1 -1
  24. package/dist/infer.d.mts.map +1 -1
  25. package/dist/openapi-hooks.cjs.map +1 -1
  26. package/dist/openapi-hooks.d.cts +2 -2
  27. package/dist/openapi-hooks.d.cts.map +1 -1
  28. package/dist/openapi-hooks.d.mts +2 -2
  29. package/dist/openapi-hooks.d.mts.map +1 -1
  30. package/dist/openapi-hooks.mjs.map +1 -1
  31. package/dist/openapi-types.d.cts.map +1 -1
  32. package/dist/openapi-types.d.mts.map +1 -1
  33. package/dist/openapi.cjs.map +1 -1
  34. package/dist/openapi.mjs.map +1 -1
  35. package/dist/react-query.cjs.map +1 -1
  36. package/dist/react-query.d.cts +1 -1
  37. package/dist/react-query.d.cts.map +1 -1
  38. package/dist/react-query.d.mts +1 -1
  39. package/dist/react-query.d.mts.map +1 -1
  40. package/dist/react-query.mjs.map +1 -1
  41. package/dist/{types-FxummKaL.d.mts → types-4K4N-Fl1.d.cts} +2 -2
  42. package/dist/types-4K4N-Fl1.d.cts.map +1 -0
  43. package/dist/{types-vutATyWv.d.cts → types-hhkZWjQn.d.mts} +2 -2
  44. package/dist/types-hhkZWjQn.d.mts.map +1 -0
  45. package/dist/types.d.cts +1 -1
  46. package/dist/types.d.mts +1 -1
  47. package/package.json +3 -3
  48. package/src/__tests__/auth-fetcher.spec.ts +476 -476
  49. package/src/__tests__/endpoint-hooks.spec.tsx +348 -348
  50. package/src/__tests__/fetcher.spec.ts +403 -403
  51. package/src/__tests__/infer.integration.spec.ts +171 -171
  52. package/src/__tests__/infer.spec.ts +182 -182
  53. package/src/__tests__/method-restrictions.spec.tsx +165 -165
  54. package/src/__tests__/openapi-hooks.spec.tsx +536 -536
  55. package/src/__tests__/react-query-infinite.spec.tsx +989 -989
  56. package/src/__tests__/react-query-invalidation.spec.tsx +223 -222
  57. package/src/__tests__/react-query.spec.tsx +567 -567
  58. package/src/__tests__/setup.ts +260 -260
  59. package/src/__tests__/types.spec.ts +127 -127
  60. package/src/__tests__/url-parsing.spec.ts +191 -191
  61. package/src/auth-fetcher.ts +160 -162
  62. package/src/endpoint-hooks.ts +151 -156
  63. package/src/fetcher.ts +163 -163
  64. package/src/infer.ts +64 -63
  65. package/src/openapi-hooks.ts +146 -146
  66. package/src/openapi-types.d.ts +428 -428
  67. package/src/openapi.json +593 -593
  68. package/src/react-query.ts +281 -281
  69. package/src/types.ts +121 -124
  70. package/dist/types-FxummKaL.d.mts.map +0 -1
  71. package/dist/types-vutATyWv.d.cts.map +0 -1
@@ -9,549 +9,549 @@ import './setup';
9
9
 
10
10
  // Test OpenAPI types
11
11
  interface TestPaths {
12
- '/users': {
13
- get: {
14
- operationId: 'listUsers';
15
- parameters?: {
16
- query?: {
17
- page?: number;
18
- limit?: number;
19
- search?: string;
20
- };
21
- };
22
- responses: {
23
- 200: {
24
- content: {
25
- 'application/json': {
26
- users: Array<{
27
- id: string;
28
- name: string;
29
- email: string;
30
- }>;
31
- total: number;
32
- };
33
- };
34
- };
35
- };
36
- };
37
- post: {
38
- operationId: 'createUser';
39
- requestBody: {
40
- content: {
41
- 'application/json': {
42
- name: string;
43
- email: string;
44
- password: string;
45
- };
46
- };
47
- };
48
- responses: {
49
- 201: {
50
- content: {
51
- 'application/json': {
52
- id: string;
53
- name: string;
54
- email: string;
55
- };
56
- };
57
- };
58
- };
59
- };
60
- };
61
- '/users/{id}': {
62
- parameters: {
63
- path: {
64
- id: string;
65
- };
66
- };
67
- get: {
68
- operationId: 'getUser';
69
- responses: {
70
- 200: {
71
- content: {
72
- 'application/json': {
73
- id: string;
74
- name: string;
75
- email: string;
76
- createdAt: string;
77
- };
78
- };
79
- };
80
- };
81
- };
82
- patch: {
83
- operationId: 'updateUser';
84
- requestBody: {
85
- content: {
86
- 'application/json': {
87
- name?: string;
88
- email?: string;
89
- };
90
- };
91
- };
92
- responses: {
93
- 200: {
94
- content: {
95
- 'application/json': {
96
- id: string;
97
- name: string;
98
- email: string;
99
- };
100
- };
101
- };
102
- };
103
- };
104
- delete: {
105
- operationId: 'deleteUser';
106
- responses: {
107
- 204: {};
108
- };
109
- };
110
- };
111
- '/posts/{postId}': {
112
- get: {
113
- operationId: 'getPost';
114
- parameters: {
115
- path: {
116
- postId: string;
117
- };
118
- query?: {
119
- includeAuthor?: boolean;
120
- };
121
- };
122
- responses: {
123
- 200: {
124
- content: {
125
- 'application/json': {
126
- id: string;
127
- title: string;
128
- content: string;
129
- authorId: string;
130
- author?: {
131
- id: string;
132
- name: string;
133
- };
134
- };
135
- };
136
- };
137
- };
138
- };
139
- };
12
+ '/users': {
13
+ get: {
14
+ operationId: 'listUsers';
15
+ parameters?: {
16
+ query?: {
17
+ page?: number;
18
+ limit?: number;
19
+ search?: string;
20
+ };
21
+ };
22
+ responses: {
23
+ 200: {
24
+ content: {
25
+ 'application/json': {
26
+ users: Array<{
27
+ id: string;
28
+ name: string;
29
+ email: string;
30
+ }>;
31
+ total: number;
32
+ };
33
+ };
34
+ };
35
+ };
36
+ };
37
+ post: {
38
+ operationId: 'createUser';
39
+ requestBody: {
40
+ content: {
41
+ 'application/json': {
42
+ name: string;
43
+ email: string;
44
+ password: string;
45
+ };
46
+ };
47
+ };
48
+ responses: {
49
+ 201: {
50
+ content: {
51
+ 'application/json': {
52
+ id: string;
53
+ name: string;
54
+ email: string;
55
+ };
56
+ };
57
+ };
58
+ };
59
+ };
60
+ };
61
+ '/users/{id}': {
62
+ parameters: {
63
+ path: {
64
+ id: string;
65
+ };
66
+ };
67
+ get: {
68
+ operationId: 'getUser';
69
+ responses: {
70
+ 200: {
71
+ content: {
72
+ 'application/json': {
73
+ id: string;
74
+ name: string;
75
+ email: string;
76
+ createdAt: string;
77
+ };
78
+ };
79
+ };
80
+ };
81
+ };
82
+ patch: {
83
+ operationId: 'updateUser';
84
+ requestBody: {
85
+ content: {
86
+ 'application/json': {
87
+ name?: string;
88
+ email?: string;
89
+ };
90
+ };
91
+ };
92
+ responses: {
93
+ 200: {
94
+ content: {
95
+ 'application/json': {
96
+ id: string;
97
+ name: string;
98
+ email: string;
99
+ };
100
+ };
101
+ };
102
+ };
103
+ };
104
+ delete: {
105
+ operationId: 'deleteUser';
106
+ responses: {
107
+ 204: {};
108
+ };
109
+ };
110
+ };
111
+ '/posts/{postId}': {
112
+ get: {
113
+ operationId: 'getPost';
114
+ parameters: {
115
+ path: {
116
+ postId: string;
117
+ };
118
+ query?: {
119
+ includeAuthor?: boolean;
120
+ };
121
+ };
122
+ responses: {
123
+ 200: {
124
+ content: {
125
+ 'application/json': {
126
+ id: string;
127
+ title: string;
128
+ content: string;
129
+ authorId: string;
130
+ author?: {
131
+ id: string;
132
+ name: string;
133
+ };
134
+ };
135
+ };
136
+ };
137
+ };
138
+ };
139
+ };
140
140
  }
141
141
 
142
142
  // Note: fetch is mocked by MSW in setup.ts
143
143
 
144
144
  // Test wrapper
145
145
  function createWrapper() {
146
- const queryClient = new QueryClient({
147
- defaultOptions: {
148
- queries: { retry: false },
149
- mutations: { retry: false },
150
- },
151
- });
152
-
153
- return ({ children }: { children: any }) => (
154
- <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
155
- );
146
+ const queryClient = new QueryClient({
147
+ defaultOptions: {
148
+ queries: { retry: false },
149
+ mutations: { retry: false },
150
+ },
151
+ });
152
+
153
+ return ({ children }: { children: any }) => (
154
+ <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
155
+ );
156
156
  }
157
157
 
158
158
  describe('createOpenAPIHooks', () => {
159
- const operations = {
160
- listUsers: { path: '/users', method: 'get' },
161
- createUser: { path: '/users', method: 'post' },
162
- getUser: { path: '/users/{id}', method: 'get' },
163
- updateUser: { path: '/users/{id}', method: 'patch' },
164
- deleteUser: { path: '/users/{id}', method: 'delete' },
165
- getPost: { path: '/posts/{postId}', method: 'get' },
166
- };
167
-
168
- afterEach(() => {
169
- vi.clearAllMocks();
170
- });
171
-
172
- describe('useQuery', () => {
173
- it('should fetch data for simple GET operation', async () => {
174
- // MSW will return the predefined response from setup.ts
175
-
176
- const api = createOpenAPIHooks<TestPaths>({
177
- baseURL: 'https://api.example.com',
178
- operations,
179
- });
180
-
181
- const { result } = renderHook(() => api.useQuery('listUsers'), {
182
- wrapper: createWrapper(),
183
- });
184
-
185
- await waitFor(() => {
186
- expect(result.current.isSuccess).toBe(true);
187
- });
188
-
189
- expect(result.current.data).toEqual({
190
- users: [
191
- { id: '1', name: 'John Doe', email: 'john@example.com' },
192
- { id: '2', name: 'Jane Smith', email: 'jane@example.com' },
193
- ],
194
- });
195
- });
196
-
197
- it('should handle query parameters', async () => {
198
- // MSW will return the predefined response from setup.ts
199
-
200
- const api = createOpenAPIHooks<TestPaths>({
201
- baseURL: 'https://api.example.com',
202
- operations,
203
- });
204
-
205
- const { result } = renderHook(
206
- () =>
207
- api.useQuery('listUsers', {
208
- // query: { page: 2, limit: 10, search: 'john' },
209
- }),
210
- { wrapper: createWrapper() },
211
- );
212
-
213
- await waitFor(() => {
214
- expect(result.current.isSuccess).toBe(true);
215
- });
216
-
217
- // Query parameters are handled by MSW, verify data structure
218
- expect(result.current.data).toBeDefined();
219
- });
220
-
221
- it('should handle path parameters', async () => {
222
- // MSW will return the predefined response from setup.ts
223
-
224
- const api = createOpenAPIHooks<TestPaths>({
225
- baseURL: 'https://api.example.com',
226
- operations,
227
- });
228
-
229
- const { result } = renderHook(
230
- () =>
231
- api.useQuery('getUser', {
232
- params: { id: '123' },
233
- }),
234
- { wrapper: createWrapper() },
235
- );
236
-
237
- await waitFor(() => {
238
- expect(result.current.isSuccess).toBe(true);
239
- });
240
-
241
- expect(result.current.data).toEqual({
242
- id: '123',
243
- name: 'John Doe',
244
- email: 'john@example.com',
245
- });
246
- });
247
-
248
- it('should handle both path and query parameters', async () => {
249
- // MSW will return the predefined response from setup.ts
250
-
251
- const api = createOpenAPIHooks<TestPaths>({
252
- baseURL: 'https://api.example.com',
253
- operations,
254
- });
255
-
256
- const { result } = renderHook(
257
- () =>
258
- api.useQuery('getPost', {
259
- params: { postId: '456' },
260
- query: { includeAuthor: true },
261
- }),
262
- { wrapper: createWrapper() },
263
- );
264
-
265
- await waitFor(() => {
266
- expect(result.current.isSuccess).toBe(true);
267
- });
268
-
269
- expect(result.current.data).toEqual({
270
- id: '456',
271
- title: 'Test Post',
272
- content: 'Test content',
273
- authorId: '123',
274
- author: { id: '123', name: 'John' },
275
- });
276
- });
277
-
278
- it('should use proper query key for caching', async () => {
279
- // MSW will handle the responses
280
-
281
- const api = createOpenAPIHooks<TestPaths>({
282
- baseURL: 'https://api.example.com',
283
- operations,
284
- });
285
-
286
- const { result: result1 } = renderHook(
287
- () =>
288
- api.useQuery('listUsers', {
289
- query: { page: 1 },
290
- }),
291
- { wrapper: createWrapper() },
292
- );
293
-
294
- const { result: result2 } = renderHook(
295
- () =>
296
- api.useQuery('listUsers', {
297
- query: { page: 1 },
298
- }),
299
- { wrapper: createWrapper() },
300
- );
301
-
302
- await waitFor(() => {
303
- expect(result1.current.isSuccess).toBe(true);
304
- expect(result2.current.isSuccess).toBe(true);
305
- });
306
-
307
- // Both queries should have the same data due to caching
308
- expect(result1.current.data).toEqual(result2.current.data);
309
- });
310
- });
311
-
312
- describe('useMutation', () => {
313
- it('should handle POST mutation', async () => {
314
- // MSW will return the predefined response from setup.ts
315
-
316
- const api = createOpenAPIHooks<TestPaths>({
317
- baseURL: 'https://api.example.com',
318
- operations,
319
- });
320
-
321
- const { result } = renderHook(() => api.useMutation('createUser'), {
322
- wrapper: createWrapper(),
323
- });
324
-
325
- result.current.mutate({
326
- body: {
327
- name: 'New User',
328
- email: 'new@example.com',
329
- password: 'secret123',
330
- },
331
- });
332
-
333
- await waitFor(() => {
334
- expect(result.current.isSuccess).toBe(true);
335
- });
336
-
337
- expect(result.current.data).toEqual({
338
- id: '123',
339
- name: 'New User',
340
- email: 'new@example.com',
341
- });
342
- });
343
-
344
- it('should handle PATCH mutation with path params', async () => {
345
- // MSW will return the predefined response from setup.ts
346
-
347
- const api = createOpenAPIHooks<TestPaths>({
348
- baseURL: 'https://api.example.com',
349
- operations,
350
- });
351
-
352
- const { result } = renderHook(() => api.useMutation('updateUser'), {
353
- wrapper: createWrapper(),
354
- });
355
-
356
- result.current.mutate({
357
- params: { id: '123' },
358
- body: { name: 'Updated Name' },
359
- });
360
-
361
- await waitFor(() => {
362
- expect(result.current.isSuccess).toBe(true);
363
- });
364
-
365
- expect(result.current.data).toEqual({
366
- id: '123',
367
- name: 'Updated Name',
368
- email: 'john@example.com',
369
- });
370
- });
371
-
372
- it('should handle DELETE mutation returning void', async () => {
373
- // MSW will return 204 No Content for DELETE
374
-
375
- const api = createOpenAPIHooks<TestPaths>({
376
- baseURL: 'https://api.example.com',
377
- operations,
378
- });
379
-
380
- const { result } = renderHook(() => api.useMutation('deleteUser'), {
381
- wrapper: createWrapper(),
382
- });
383
-
384
- result.current.mutate({
385
- params: { id: '123' },
386
- });
387
-
388
- await waitFor(() => {
389
- expect(result.current.isSuccess).toBe(true);
390
- });
391
-
392
- expect(result.current.data).toBeUndefined();
393
- });
394
-
395
- it('should handle mutation options', async () => {
396
- // MSW will handle the response
397
-
398
- const onSuccess = vi.fn();
399
- const onError = vi.fn();
400
-
401
- const api = createOpenAPIHooks<TestPaths>({
402
- baseURL: 'https://api.example.com',
403
- operations,
404
- });
405
-
406
- const { result } = renderHook(
407
- () =>
408
- api.useMutation('createUser', {
409
- onSuccess,
410
- onError,
411
- }),
412
- { wrapper: createWrapper() },
413
- );
414
-
415
- result.current.mutate({
416
- body: {
417
- name: 'Test',
418
- email: 'test@test.com',
419
- password: 'password',
420
- },
421
- });
422
-
423
- await waitFor(() => {
424
- expect(result.current.isSuccess).toBe(true);
425
- });
426
-
427
- expect(onSuccess).toHaveBeenCalled();
428
- expect(onSuccess.mock.calls[0]?.[0]).toEqual({
429
- id: '123',
430
- name: 'Test',
431
- email: 'test@test.com',
432
- });
433
- expect(onError).not.toHaveBeenCalled();
434
- });
435
- });
436
-
437
- describe('buildEndpoint', () => {
438
- it('should use operation registry when provided', () => {
439
- const api = createOpenAPIHooks<TestPaths>({
440
- baseURL: 'https://api.example.com',
441
- operations,
442
- });
443
-
444
- // The endpoint should be built correctly based on the registry
445
- const { result } = renderHook(() => api.useQuery('listUsers'), {
446
- wrapper: createWrapper(),
447
- });
448
-
449
- // Wait for the query to be initiated
450
- expect(result.current.isLoading).toBe(true);
451
- });
452
-
453
- it('should work without operation registry', () => {
454
- const api = createOpenAPIHooks<TestPaths>({
455
- baseURL: 'https://api.example.com',
456
- });
457
-
458
- // Without registry, it should use operationId as fallback
459
- const { result } = renderHook(() => api.useQuery('listUsers'), {
460
- wrapper: createWrapper(),
461
- });
462
-
463
- expect(result.current.isLoading).toBe(true);
464
- });
465
- });
466
-
467
- describe('type inference', () => {
468
- it('should infer correct response types', () => {
469
- const api = createOpenAPIHooks<TestPaths>({
470
- baseURL: 'https://api.example.com',
471
- operations,
472
- });
473
-
474
- const { result } = renderHook(() => api.useQuery('listUsers'), {
475
- wrapper: createWrapper(),
476
- });
477
-
478
- // Type test - this should compile without errors
479
- if (result.current.data) {
480
- const users: Array<{ id: string; name: string; email: string }> =
481
- result.current.data.users;
482
- const total: number = result.current.data.total;
483
- expect(users).toBeDefined();
484
- expect(total).toBeDefined();
485
- }
486
- });
487
-
488
- it('should enforce required parameters', () => {
489
- const api = createOpenAPIHooks<TestPaths>({
490
- baseURL: 'https://api.example.com',
491
- operations,
492
- });
493
-
494
- // This test is about type checking, not runtime behavior
495
- // The TypeScript compiler enforces these constraints:
496
-
497
- // ✓ This compiles - no params required for listUsers
498
- const listUsersHook = () => api.useQuery('listUsers');
499
-
500
- // ✓ This compiles - params are provided for getUser
501
- const getUserHook = () =>
502
- api.useQuery('getUser', { params: { id: '123' } });
503
-
504
- // ✗ This would not compile - missing required params
505
- // const invalidHook = () => api.useQuery('getUser');
506
-
507
- expect(listUsersHook).toBeDefined();
508
- expect(getUserHook).toBeDefined();
509
- });
510
-
511
- it('should handle optional parameters correctly', () => {
512
- const api = createOpenAPIHooks<TestPaths>({
513
- baseURL: 'https://api.example.com',
514
- operations,
515
- });
516
-
517
- // All valid ways to call listUsers - wrapped in functions
518
- const hook1 = () => api.useQuery('listUsers');
519
- const hook2 = () => api.useQuery('listUsers', {});
520
- const hook3 = () => api.useQuery('listUsers', { query: {} });
521
- const hook4 = () => api.useQuery('listUsers', { query: { page: 1 } });
522
- const hook5 = () =>
523
- api.useQuery('listUsers', { query: { page: 1, limit: 10 } });
524
-
525
- expect(hook1).toBeDefined();
526
- expect(hook2).toBeDefined();
527
- expect(hook3).toBeDefined();
528
- expect(hook4).toBeDefined();
529
- expect(hook5).toBeDefined();
530
- });
531
- });
532
-
533
- describe('error handling', () => {
534
- it.skip('should handle fetch errors', async () => {
535
- // This test is skipped because we need a way to trigger actual fetch errors
536
- // The current implementation always returns successful responses from MSW
537
- });
538
-
539
- it('should handle non-ok responses', async () => {
540
- // MSW will return 404 for user id '404'
541
-
542
- const api = createOpenAPIHooks<TestPaths>({
543
- baseURL: 'https://api.example.com',
544
- operations,
545
- });
546
-
547
- const { result } = renderHook(
548
- () => api.useQuery('getUser', { params: { id: '404' } }),
549
- { wrapper: createWrapper() },
550
- );
551
-
552
- await waitFor(() => {
553
- expect(result.current.isError).toBe(true);
554
- });
555
- });
556
- });
159
+ const operations = {
160
+ listUsers: { path: '/users', method: 'get' },
161
+ createUser: { path: '/users', method: 'post' },
162
+ getUser: { path: '/users/{id}', method: 'get' },
163
+ updateUser: { path: '/users/{id}', method: 'patch' },
164
+ deleteUser: { path: '/users/{id}', method: 'delete' },
165
+ getPost: { path: '/posts/{postId}', method: 'get' },
166
+ };
167
+
168
+ afterEach(() => {
169
+ vi.clearAllMocks();
170
+ });
171
+
172
+ describe('useQuery', () => {
173
+ it('should fetch data for simple GET operation', async () => {
174
+ // MSW will return the predefined response from setup.ts
175
+
176
+ const api = createOpenAPIHooks<TestPaths>({
177
+ baseURL: 'https://api.example.com',
178
+ operations,
179
+ });
180
+
181
+ const { result } = renderHook(() => api.useQuery('listUsers'), {
182
+ wrapper: createWrapper(),
183
+ });
184
+
185
+ await waitFor(() => {
186
+ expect(result.current.isSuccess).toBe(true);
187
+ });
188
+
189
+ expect(result.current.data).toEqual({
190
+ users: [
191
+ { id: '1', name: 'John Doe', email: 'john@example.com' },
192
+ { id: '2', name: 'Jane Smith', email: 'jane@example.com' },
193
+ ],
194
+ });
195
+ });
196
+
197
+ it('should handle query parameters', async () => {
198
+ // MSW will return the predefined response from setup.ts
199
+
200
+ const api = createOpenAPIHooks<TestPaths>({
201
+ baseURL: 'https://api.example.com',
202
+ operations,
203
+ });
204
+
205
+ const { result } = renderHook(
206
+ () =>
207
+ api.useQuery('listUsers', {
208
+ // query: { page: 2, limit: 10, search: 'john' },
209
+ }),
210
+ { wrapper: createWrapper() },
211
+ );
212
+
213
+ await waitFor(() => {
214
+ expect(result.current.isSuccess).toBe(true);
215
+ });
216
+
217
+ // Query parameters are handled by MSW, verify data structure
218
+ expect(result.current.data).toBeDefined();
219
+ });
220
+
221
+ it('should handle path parameters', async () => {
222
+ // MSW will return the predefined response from setup.ts
223
+
224
+ const api = createOpenAPIHooks<TestPaths>({
225
+ baseURL: 'https://api.example.com',
226
+ operations,
227
+ });
228
+
229
+ const { result } = renderHook(
230
+ () =>
231
+ api.useQuery('getUser', {
232
+ params: { id: '123' },
233
+ }),
234
+ { wrapper: createWrapper() },
235
+ );
236
+
237
+ await waitFor(() => {
238
+ expect(result.current.isSuccess).toBe(true);
239
+ });
240
+
241
+ expect(result.current.data).toEqual({
242
+ id: '123',
243
+ name: 'John Doe',
244
+ email: 'john@example.com',
245
+ });
246
+ });
247
+
248
+ it('should handle both path and query parameters', async () => {
249
+ // MSW will return the predefined response from setup.ts
250
+
251
+ const api = createOpenAPIHooks<TestPaths>({
252
+ baseURL: 'https://api.example.com',
253
+ operations,
254
+ });
255
+
256
+ const { result } = renderHook(
257
+ () =>
258
+ api.useQuery('getPost', {
259
+ params: { postId: '456' },
260
+ query: { includeAuthor: true },
261
+ }),
262
+ { wrapper: createWrapper() },
263
+ );
264
+
265
+ await waitFor(() => {
266
+ expect(result.current.isSuccess).toBe(true);
267
+ });
268
+
269
+ expect(result.current.data).toEqual({
270
+ id: '456',
271
+ title: 'Test Post',
272
+ content: 'Test content',
273
+ authorId: '123',
274
+ author: { id: '123', name: 'John' },
275
+ });
276
+ });
277
+
278
+ it('should use proper query key for caching', async () => {
279
+ // MSW will handle the responses
280
+
281
+ const api = createOpenAPIHooks<TestPaths>({
282
+ baseURL: 'https://api.example.com',
283
+ operations,
284
+ });
285
+
286
+ const { result: result1 } = renderHook(
287
+ () =>
288
+ api.useQuery('listUsers', {
289
+ query: { page: 1 },
290
+ }),
291
+ { wrapper: createWrapper() },
292
+ );
293
+
294
+ const { result: result2 } = renderHook(
295
+ () =>
296
+ api.useQuery('listUsers', {
297
+ query: { page: 1 },
298
+ }),
299
+ { wrapper: createWrapper() },
300
+ );
301
+
302
+ await waitFor(() => {
303
+ expect(result1.current.isSuccess).toBe(true);
304
+ expect(result2.current.isSuccess).toBe(true);
305
+ });
306
+
307
+ // Both queries should have the same data due to caching
308
+ expect(result1.current.data).toEqual(result2.current.data);
309
+ });
310
+ });
311
+
312
+ describe('useMutation', () => {
313
+ it('should handle POST mutation', async () => {
314
+ // MSW will return the predefined response from setup.ts
315
+
316
+ const api = createOpenAPIHooks<TestPaths>({
317
+ baseURL: 'https://api.example.com',
318
+ operations,
319
+ });
320
+
321
+ const { result } = renderHook(() => api.useMutation('createUser'), {
322
+ wrapper: createWrapper(),
323
+ });
324
+
325
+ result.current.mutate({
326
+ body: {
327
+ name: 'New User',
328
+ email: 'new@example.com',
329
+ password: 'secret123',
330
+ },
331
+ });
332
+
333
+ await waitFor(() => {
334
+ expect(result.current.isSuccess).toBe(true);
335
+ });
336
+
337
+ expect(result.current.data).toEqual({
338
+ id: '123',
339
+ name: 'New User',
340
+ email: 'new@example.com',
341
+ });
342
+ });
343
+
344
+ it('should handle PATCH mutation with path params', async () => {
345
+ // MSW will return the predefined response from setup.ts
346
+
347
+ const api = createOpenAPIHooks<TestPaths>({
348
+ baseURL: 'https://api.example.com',
349
+ operations,
350
+ });
351
+
352
+ const { result } = renderHook(() => api.useMutation('updateUser'), {
353
+ wrapper: createWrapper(),
354
+ });
355
+
356
+ result.current.mutate({
357
+ params: { id: '123' },
358
+ body: { name: 'Updated Name' },
359
+ });
360
+
361
+ await waitFor(() => {
362
+ expect(result.current.isSuccess).toBe(true);
363
+ });
364
+
365
+ expect(result.current.data).toEqual({
366
+ id: '123',
367
+ name: 'Updated Name',
368
+ email: 'john@example.com',
369
+ });
370
+ });
371
+
372
+ it('should handle DELETE mutation returning void', async () => {
373
+ // MSW will return 204 No Content for DELETE
374
+
375
+ const api = createOpenAPIHooks<TestPaths>({
376
+ baseURL: 'https://api.example.com',
377
+ operations,
378
+ });
379
+
380
+ const { result } = renderHook(() => api.useMutation('deleteUser'), {
381
+ wrapper: createWrapper(),
382
+ });
383
+
384
+ result.current.mutate({
385
+ params: { id: '123' },
386
+ });
387
+
388
+ await waitFor(() => {
389
+ expect(result.current.isSuccess).toBe(true);
390
+ });
391
+
392
+ expect(result.current.data).toBeUndefined();
393
+ });
394
+
395
+ it('should handle mutation options', async () => {
396
+ // MSW will handle the response
397
+
398
+ const onSuccess = vi.fn();
399
+ const onError = vi.fn();
400
+
401
+ const api = createOpenAPIHooks<TestPaths>({
402
+ baseURL: 'https://api.example.com',
403
+ operations,
404
+ });
405
+
406
+ const { result } = renderHook(
407
+ () =>
408
+ api.useMutation('createUser', {
409
+ onSuccess,
410
+ onError,
411
+ }),
412
+ { wrapper: createWrapper() },
413
+ );
414
+
415
+ result.current.mutate({
416
+ body: {
417
+ name: 'Test',
418
+ email: 'test@test.com',
419
+ password: 'password',
420
+ },
421
+ });
422
+
423
+ await waitFor(() => {
424
+ expect(result.current.isSuccess).toBe(true);
425
+ });
426
+
427
+ expect(onSuccess).toHaveBeenCalled();
428
+ expect(onSuccess.mock.calls[0]?.[0]).toEqual({
429
+ id: '123',
430
+ name: 'Test',
431
+ email: 'test@test.com',
432
+ });
433
+ expect(onError).not.toHaveBeenCalled();
434
+ });
435
+ });
436
+
437
+ describe('buildEndpoint', () => {
438
+ it('should use operation registry when provided', () => {
439
+ const api = createOpenAPIHooks<TestPaths>({
440
+ baseURL: 'https://api.example.com',
441
+ operations,
442
+ });
443
+
444
+ // The endpoint should be built correctly based on the registry
445
+ const { result } = renderHook(() => api.useQuery('listUsers'), {
446
+ wrapper: createWrapper(),
447
+ });
448
+
449
+ // Wait for the query to be initiated
450
+ expect(result.current.isLoading).toBe(true);
451
+ });
452
+
453
+ it('should work without operation registry', () => {
454
+ const api = createOpenAPIHooks<TestPaths>({
455
+ baseURL: 'https://api.example.com',
456
+ });
457
+
458
+ // Without registry, it should use operationId as fallback
459
+ const { result } = renderHook(() => api.useQuery('listUsers'), {
460
+ wrapper: createWrapper(),
461
+ });
462
+
463
+ expect(result.current.isLoading).toBe(true);
464
+ });
465
+ });
466
+
467
+ describe('type inference', () => {
468
+ it('should infer correct response types', () => {
469
+ const api = createOpenAPIHooks<TestPaths>({
470
+ baseURL: 'https://api.example.com',
471
+ operations,
472
+ });
473
+
474
+ const { result } = renderHook(() => api.useQuery('listUsers'), {
475
+ wrapper: createWrapper(),
476
+ });
477
+
478
+ // Type test - this should compile without errors
479
+ if (result.current.data) {
480
+ const users: Array<{ id: string; name: string; email: string }> =
481
+ result.current.data.users;
482
+ const total: number = result.current.data.total;
483
+ expect(users).toBeDefined();
484
+ expect(total).toBeDefined();
485
+ }
486
+ });
487
+
488
+ it('should enforce required parameters', () => {
489
+ const api = createOpenAPIHooks<TestPaths>({
490
+ baseURL: 'https://api.example.com',
491
+ operations,
492
+ });
493
+
494
+ // This test is about type checking, not runtime behavior
495
+ // The TypeScript compiler enforces these constraints:
496
+
497
+ // ✓ This compiles - no params required for listUsers
498
+ const listUsersHook = () => api.useQuery('listUsers');
499
+
500
+ // ✓ This compiles - params are provided for getUser
501
+ const getUserHook = () =>
502
+ api.useQuery('getUser', { params: { id: '123' } });
503
+
504
+ // ✗ This would not compile - missing required params
505
+ // const invalidHook = () => api.useQuery('getUser');
506
+
507
+ expect(listUsersHook).toBeDefined();
508
+ expect(getUserHook).toBeDefined();
509
+ });
510
+
511
+ it('should handle optional parameters correctly', () => {
512
+ const api = createOpenAPIHooks<TestPaths>({
513
+ baseURL: 'https://api.example.com',
514
+ operations,
515
+ });
516
+
517
+ // All valid ways to call listUsers - wrapped in functions
518
+ const hook1 = () => api.useQuery('listUsers');
519
+ const hook2 = () => api.useQuery('listUsers', {});
520
+ const hook3 = () => api.useQuery('listUsers', { query: {} });
521
+ const hook4 = () => api.useQuery('listUsers', { query: { page: 1 } });
522
+ const hook5 = () =>
523
+ api.useQuery('listUsers', { query: { page: 1, limit: 10 } });
524
+
525
+ expect(hook1).toBeDefined();
526
+ expect(hook2).toBeDefined();
527
+ expect(hook3).toBeDefined();
528
+ expect(hook4).toBeDefined();
529
+ expect(hook5).toBeDefined();
530
+ });
531
+ });
532
+
533
+ describe('error handling', () => {
534
+ it.skip('should handle fetch errors', async () => {
535
+ // This test is skipped because we need a way to trigger actual fetch errors
536
+ // The current implementation always returns successful responses from MSW
537
+ });
538
+
539
+ it('should handle non-ok responses', async () => {
540
+ // MSW will return 404 for user id '404'
541
+
542
+ const api = createOpenAPIHooks<TestPaths>({
543
+ baseURL: 'https://api.example.com',
544
+ operations,
545
+ });
546
+
547
+ const { result } = renderHook(
548
+ () => api.useQuery('getUser', { params: { id: '404' } }),
549
+ { wrapper: createWrapper() },
550
+ );
551
+
552
+ await waitFor(() => {
553
+ expect(result.current.isError).toBe(true);
554
+ });
555
+ });
556
+ });
557
557
  });