@geekmidas/client 0.2.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/auth-fetcher.cjs +2 -2
- package/dist/auth-fetcher.cjs.map +1 -1
- package/dist/auth-fetcher.d.cts +2 -1
- package/dist/auth-fetcher.d.cts.map +1 -0
- package/dist/auth-fetcher.d.mts +2 -1
- package/dist/auth-fetcher.d.mts.map +1 -0
- package/dist/auth-fetcher.mjs +2 -2
- package/dist/auth-fetcher.mjs.map +1 -1
- package/dist/endpoint-hooks.cjs.map +1 -1
- package/dist/endpoint-hooks.d.cts +1 -1
- package/dist/endpoint-hooks.d.cts.map +1 -0
- package/dist/endpoint-hooks.d.mts +1 -1
- package/dist/endpoint-hooks.d.mts.map +1 -0
- package/dist/endpoint-hooks.mjs.map +1 -1
- package/dist/{fetcher-DLDD_7Sa.mjs → fetcher-5fnBE7Dk.mjs} +2 -2
- package/dist/{fetcher-DLDD_7Sa.mjs.map → fetcher-5fnBE7Dk.mjs.map} +1 -1
- package/dist/{fetcher-KdwHgdAl.cjs → fetcher-CgLEaIAC.cjs} +2 -2
- package/dist/{fetcher-KdwHgdAl.cjs.map → fetcher-CgLEaIAC.cjs.map} +1 -1
- package/dist/fetcher.cjs +1 -1
- package/dist/fetcher.d.cts +3 -1
- package/dist/fetcher.d.cts.map +1 -0
- package/dist/fetcher.d.mts +3 -1
- package/dist/fetcher.d.mts.map +1 -0
- package/dist/fetcher.mjs +1 -1
- package/dist/infer.d.cts.map +1 -0
- package/dist/infer.d.mts.map +1 -0
- package/dist/openapi-hooks.cjs +3 -5
- package/dist/openapi-hooks.cjs.map +1 -1
- package/dist/openapi-hooks.d.cts +2 -2
- package/dist/openapi-hooks.d.cts.map +1 -0
- package/dist/openapi-hooks.d.mts +2 -2
- package/dist/openapi-hooks.d.mts.map +1 -0
- package/dist/openapi-hooks.mjs +3 -5
- package/dist/openapi-hooks.mjs.map +1 -1
- package/dist/openapi-types.d.cts.map +1 -0
- package/dist/openapi-types.d.mts.map +1 -0
- package/dist/react-query.cjs +1 -1
- package/dist/react-query.d.cts +3 -1
- package/dist/react-query.d.cts.map +1 -0
- package/dist/react-query.d.mts +3 -1
- package/dist/react-query.d.mts.map +1 -0
- package/dist/react-query.mjs +1 -1
- package/dist/{types-D4OSWveN.d.cts → types-FxummKaL.d.mts} +7 -5
- package/dist/types-FxummKaL.d.mts.map +1 -0
- package/dist/{types-Cdv1XAWr.d.mts → types-vutATyWv.d.cts} +7 -5
- package/dist/types-vutATyWv.d.cts.map +1 -0
- package/dist/types.d.cts +1 -1
- package/dist/types.d.mts +1 -1
- package/package.json +12 -6
- package/src/__tests__/auth-fetcher.spec.ts +505 -0
- package/src/__tests__/openapi-hooks.spec.tsx +7 -8
- package/src/auth-fetcher.ts +1 -1
- package/src/endpoint-hooks.ts +14 -8
- package/src/fetcher.ts +1 -1
- package/src/openapi-hooks.ts +3 -3
- package/src/types.ts +6 -4
- package/tsconfig.json +12 -0
|
@@ -0,0 +1,505 @@
|
|
|
1
|
+
import { http, HttpResponse } from 'msw';
|
|
2
|
+
import { setupServer } from 'msw/node';
|
|
3
|
+
import {
|
|
4
|
+
afterAll,
|
|
5
|
+
afterEach,
|
|
6
|
+
beforeAll,
|
|
7
|
+
describe,
|
|
8
|
+
expect,
|
|
9
|
+
it,
|
|
10
|
+
vi,
|
|
11
|
+
} from 'vitest';
|
|
12
|
+
import {
|
|
13
|
+
type ApiKeyProvider,
|
|
14
|
+
type AwsSigner,
|
|
15
|
+
type SecuritySchemeObject,
|
|
16
|
+
type TokenProvider,
|
|
17
|
+
createAuthAwareFetcher,
|
|
18
|
+
} from '../auth-fetcher';
|
|
19
|
+
|
|
20
|
+
// Test types - structured to match OpenAPI spec format
|
|
21
|
+
interface TestPaths {
|
|
22
|
+
'/users': {
|
|
23
|
+
get: {
|
|
24
|
+
responses: {
|
|
25
|
+
200: {
|
|
26
|
+
content: {
|
|
27
|
+
'application/json': { users: Array<{ id: string; name: string }> };
|
|
28
|
+
};
|
|
29
|
+
};
|
|
30
|
+
};
|
|
31
|
+
};
|
|
32
|
+
};
|
|
33
|
+
'/protected': {
|
|
34
|
+
get: {
|
|
35
|
+
responses: {
|
|
36
|
+
200: {
|
|
37
|
+
content: {
|
|
38
|
+
'application/json': { data: string };
|
|
39
|
+
};
|
|
40
|
+
};
|
|
41
|
+
};
|
|
42
|
+
};
|
|
43
|
+
};
|
|
44
|
+
'/api-key-protected': {
|
|
45
|
+
get: {
|
|
46
|
+
responses: {
|
|
47
|
+
200: {
|
|
48
|
+
content: {
|
|
49
|
+
'application/json': { data: string };
|
|
50
|
+
};
|
|
51
|
+
};
|
|
52
|
+
};
|
|
53
|
+
};
|
|
54
|
+
};
|
|
55
|
+
'/iam-protected': {
|
|
56
|
+
get: {
|
|
57
|
+
responses: {
|
|
58
|
+
200: {
|
|
59
|
+
content: {
|
|
60
|
+
'application/json': { data: string };
|
|
61
|
+
};
|
|
62
|
+
};
|
|
63
|
+
};
|
|
64
|
+
};
|
|
65
|
+
};
|
|
66
|
+
'/data': {
|
|
67
|
+
post: {
|
|
68
|
+
requestBody: {
|
|
69
|
+
content: {
|
|
70
|
+
'application/json': { value: string };
|
|
71
|
+
};
|
|
72
|
+
};
|
|
73
|
+
responses: {
|
|
74
|
+
201: {
|
|
75
|
+
content: {
|
|
76
|
+
'application/json': { id: string };
|
|
77
|
+
};
|
|
78
|
+
};
|
|
79
|
+
};
|
|
80
|
+
};
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Track headers received by handlers
|
|
85
|
+
let lastReceivedHeaders: Record<string, string> = {};
|
|
86
|
+
|
|
87
|
+
const handlers = [
|
|
88
|
+
http.get('https://api.example.com/users', ({ request }) => {
|
|
89
|
+
lastReceivedHeaders = Object.fromEntries(request.headers.entries());
|
|
90
|
+
return HttpResponse.json({
|
|
91
|
+
users: [{ id: '1', name: 'John' }],
|
|
92
|
+
});
|
|
93
|
+
}),
|
|
94
|
+
|
|
95
|
+
http.get('https://api.example.com/protected', ({ request }) => {
|
|
96
|
+
lastReceivedHeaders = Object.fromEntries(request.headers.entries());
|
|
97
|
+
const auth = request.headers.get('Authorization');
|
|
98
|
+
if (!auth || auth !== 'Bearer valid-token') {
|
|
99
|
+
return HttpResponse.json({ message: 'Unauthorized' }, { status: 401 });
|
|
100
|
+
}
|
|
101
|
+
return HttpResponse.json({ data: 'secret' });
|
|
102
|
+
}),
|
|
103
|
+
|
|
104
|
+
http.get('https://api.example.com/api-key-protected', ({ request }) => {
|
|
105
|
+
lastReceivedHeaders = Object.fromEntries(request.headers.entries());
|
|
106
|
+
const apiKey = request.headers.get('X-API-Key');
|
|
107
|
+
if (!apiKey || apiKey !== 'my-api-key') {
|
|
108
|
+
return HttpResponse.json({ message: 'Unauthorized' }, { status: 401 });
|
|
109
|
+
}
|
|
110
|
+
return HttpResponse.json({ data: 'api-key-data' });
|
|
111
|
+
}),
|
|
112
|
+
|
|
113
|
+
http.get('https://api.example.com/iam-protected', ({ request }) => {
|
|
114
|
+
lastReceivedHeaders = Object.fromEntries(request.headers.entries());
|
|
115
|
+
return HttpResponse.json({ data: 'iam-data' });
|
|
116
|
+
}),
|
|
117
|
+
|
|
118
|
+
http.post('https://api.example.com/data', async ({ request }) => {
|
|
119
|
+
lastReceivedHeaders = Object.fromEntries(request.headers.entries());
|
|
120
|
+
return HttpResponse.json({ id: '123' }, { status: 201 });
|
|
121
|
+
}),
|
|
122
|
+
];
|
|
123
|
+
|
|
124
|
+
const server = setupServer(...handlers);
|
|
125
|
+
|
|
126
|
+
beforeAll(() => {
|
|
127
|
+
server.listen({ onUnhandledRequest: 'error' });
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
afterEach(() => {
|
|
131
|
+
server.resetHandlers();
|
|
132
|
+
lastReceivedHeaders = {};
|
|
133
|
+
vi.clearAllMocks();
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
afterAll(() => {
|
|
137
|
+
server.close();
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
describe('createAuthAwareFetcher', () => {
|
|
141
|
+
const securitySchemes: Record<string, SecuritySchemeObject> = {
|
|
142
|
+
bearer: {
|
|
143
|
+
type: 'http',
|
|
144
|
+
scheme: 'bearer',
|
|
145
|
+
bearerFormat: 'JWT',
|
|
146
|
+
},
|
|
147
|
+
apiKey: {
|
|
148
|
+
type: 'apiKey',
|
|
149
|
+
in: 'header',
|
|
150
|
+
name: 'X-API-Key',
|
|
151
|
+
},
|
|
152
|
+
iam: {
|
|
153
|
+
type: 'http',
|
|
154
|
+
scheme: 'aws4-hmac-sha256',
|
|
155
|
+
},
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
describe('bearer auth', () => {
|
|
159
|
+
it('should add Authorization header for bearer auth endpoints', async () => {
|
|
160
|
+
const tokenProvider: TokenProvider = {
|
|
161
|
+
getValidAccessToken: vi.fn().mockResolvedValue('valid-token'),
|
|
162
|
+
createValidAuthHeaders: vi.fn().mockResolvedValue({
|
|
163
|
+
Authorization: 'Bearer valid-token',
|
|
164
|
+
}),
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
const endpointAuth = {
|
|
168
|
+
'GET /users': null,
|
|
169
|
+
'GET /protected': 'bearer',
|
|
170
|
+
} as const;
|
|
171
|
+
|
|
172
|
+
const api = createAuthAwareFetcher<TestPaths>({
|
|
173
|
+
baseURL: 'https://api.example.com',
|
|
174
|
+
endpointAuth,
|
|
175
|
+
securitySchemes,
|
|
176
|
+
authStrategies: {
|
|
177
|
+
bearer: { type: 'bearer', tokenProvider },
|
|
178
|
+
},
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
const result = await api('GET /protected');
|
|
182
|
+
|
|
183
|
+
expect(tokenProvider.createValidAuthHeaders).toHaveBeenCalled();
|
|
184
|
+
expect(result).toEqual({ data: 'secret' });
|
|
185
|
+
expect(lastReceivedHeaders.authorization).toBe('Bearer valid-token');
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
it('should not add auth headers for public endpoints', async () => {
|
|
189
|
+
const tokenProvider: TokenProvider = {
|
|
190
|
+
getValidAccessToken: vi.fn().mockResolvedValue('valid-token'),
|
|
191
|
+
createValidAuthHeaders: vi.fn().mockResolvedValue({
|
|
192
|
+
Authorization: 'Bearer valid-token',
|
|
193
|
+
}),
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
const endpointAuth = {
|
|
197
|
+
'GET /users': null,
|
|
198
|
+
'GET /protected': 'bearer',
|
|
199
|
+
} as const;
|
|
200
|
+
|
|
201
|
+
const api = createAuthAwareFetcher<TestPaths>({
|
|
202
|
+
baseURL: 'https://api.example.com',
|
|
203
|
+
endpointAuth,
|
|
204
|
+
securitySchemes,
|
|
205
|
+
authStrategies: {
|
|
206
|
+
bearer: { type: 'bearer', tokenProvider },
|
|
207
|
+
},
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
const result = await api('GET /users');
|
|
211
|
+
|
|
212
|
+
expect(tokenProvider.createValidAuthHeaders).not.toHaveBeenCalled();
|
|
213
|
+
expect(result).toEqual({ users: [{ id: '1', name: 'John' }] });
|
|
214
|
+
expect(lastReceivedHeaders.authorization).toBeUndefined();
|
|
215
|
+
});
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
describe('apiKey auth', () => {
|
|
219
|
+
it('should add API key header for apiKey auth endpoints', async () => {
|
|
220
|
+
const apiKeyProvider: ApiKeyProvider = {
|
|
221
|
+
getApiKey: vi.fn().mockResolvedValue('my-api-key'),
|
|
222
|
+
};
|
|
223
|
+
|
|
224
|
+
const endpointAuth = {
|
|
225
|
+
'GET /api-key-protected': 'apiKey',
|
|
226
|
+
} as const;
|
|
227
|
+
|
|
228
|
+
const api = createAuthAwareFetcher<TestPaths>({
|
|
229
|
+
baseURL: 'https://api.example.com',
|
|
230
|
+
endpointAuth,
|
|
231
|
+
securitySchemes,
|
|
232
|
+
authStrategies: {
|
|
233
|
+
apiKey: { type: 'apiKey', apiKeyProvider },
|
|
234
|
+
},
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
const result = await api('GET /api-key-protected');
|
|
238
|
+
|
|
239
|
+
expect(apiKeyProvider.getApiKey).toHaveBeenCalled();
|
|
240
|
+
expect(result).toEqual({ data: 'api-key-data' });
|
|
241
|
+
expect(lastReceivedHeaders['x-api-key']).toBe('my-api-key');
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
it('should use custom header name if provided', async () => {
|
|
245
|
+
const apiKeyProvider: ApiKeyProvider = {
|
|
246
|
+
getApiKey: vi.fn().mockResolvedValue('custom-key'),
|
|
247
|
+
};
|
|
248
|
+
|
|
249
|
+
const customSchemes: Record<string, SecuritySchemeObject> = {
|
|
250
|
+
customApiKey: {
|
|
251
|
+
type: 'apiKey',
|
|
252
|
+
in: 'header',
|
|
253
|
+
name: 'X-Custom-Key',
|
|
254
|
+
},
|
|
255
|
+
};
|
|
256
|
+
|
|
257
|
+
const endpointAuth = {
|
|
258
|
+
'GET /api-key-protected': 'customApiKey',
|
|
259
|
+
} as const;
|
|
260
|
+
|
|
261
|
+
server.use(
|
|
262
|
+
http.get('https://api.example.com/api-key-protected', ({ request }) => {
|
|
263
|
+
lastReceivedHeaders = Object.fromEntries(request.headers.entries());
|
|
264
|
+
return HttpResponse.json({ data: 'custom-key-data' });
|
|
265
|
+
}),
|
|
266
|
+
);
|
|
267
|
+
|
|
268
|
+
const api = createAuthAwareFetcher<TestPaths>({
|
|
269
|
+
baseURL: 'https://api.example.com',
|
|
270
|
+
endpointAuth,
|
|
271
|
+
securitySchemes: customSchemes,
|
|
272
|
+
authStrategies: {
|
|
273
|
+
customApiKey: {
|
|
274
|
+
type: 'apiKey',
|
|
275
|
+
apiKeyProvider,
|
|
276
|
+
headerName: 'X-Custom-Key',
|
|
277
|
+
},
|
|
278
|
+
},
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
await api('GET /api-key-protected');
|
|
282
|
+
|
|
283
|
+
expect(lastReceivedHeaders['x-custom-key']).toBe('custom-key');
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
it('should handle synchronous getApiKey', async () => {
|
|
287
|
+
const apiKeyProvider: ApiKeyProvider = {
|
|
288
|
+
getApiKey: vi.fn().mockReturnValue('sync-api-key'),
|
|
289
|
+
};
|
|
290
|
+
|
|
291
|
+
server.use(
|
|
292
|
+
http.get('https://api.example.com/api-key-protected', ({ request }) => {
|
|
293
|
+
lastReceivedHeaders = Object.fromEntries(request.headers.entries());
|
|
294
|
+
return HttpResponse.json({ data: 'sync-data' });
|
|
295
|
+
}),
|
|
296
|
+
);
|
|
297
|
+
|
|
298
|
+
const endpointAuth = {
|
|
299
|
+
'GET /api-key-protected': 'apiKey',
|
|
300
|
+
} as const;
|
|
301
|
+
|
|
302
|
+
const api = createAuthAwareFetcher<TestPaths>({
|
|
303
|
+
baseURL: 'https://api.example.com',
|
|
304
|
+
endpointAuth,
|
|
305
|
+
securitySchemes,
|
|
306
|
+
authStrategies: {
|
|
307
|
+
apiKey: { type: 'apiKey', apiKeyProvider },
|
|
308
|
+
},
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
await api('GET /api-key-protected');
|
|
312
|
+
|
|
313
|
+
expect(lastReceivedHeaders['x-api-key']).toBe('sync-api-key');
|
|
314
|
+
});
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
describe('IAM auth', () => {
|
|
318
|
+
it('should call signer for IAM auth endpoints', async () => {
|
|
319
|
+
const signer: AwsSigner = {
|
|
320
|
+
sign: vi.fn().mockResolvedValue({}),
|
|
321
|
+
};
|
|
322
|
+
|
|
323
|
+
const endpointAuth = {
|
|
324
|
+
'GET /iam-protected': 'iam',
|
|
325
|
+
} as const;
|
|
326
|
+
|
|
327
|
+
const api = createAuthAwareFetcher<TestPaths>({
|
|
328
|
+
baseURL: 'https://api.example.com',
|
|
329
|
+
endpointAuth,
|
|
330
|
+
securitySchemes,
|
|
331
|
+
authStrategies: {
|
|
332
|
+
iam: { type: 'iam', signer },
|
|
333
|
+
},
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
const result = await api('GET /iam-protected');
|
|
337
|
+
|
|
338
|
+
// IAM returns empty headers in current implementation
|
|
339
|
+
expect(result).toEqual({ data: 'iam-data' });
|
|
340
|
+
});
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
describe('header merging', () => {
|
|
344
|
+
it('should merge auth headers with user-provided headers', async () => {
|
|
345
|
+
const tokenProvider: TokenProvider = {
|
|
346
|
+
getValidAccessToken: vi.fn().mockResolvedValue('valid-token'),
|
|
347
|
+
createValidAuthHeaders: vi.fn().mockResolvedValue({
|
|
348
|
+
Authorization: 'Bearer valid-token',
|
|
349
|
+
}),
|
|
350
|
+
};
|
|
351
|
+
|
|
352
|
+
const endpointAuth = {
|
|
353
|
+
'GET /protected': 'bearer',
|
|
354
|
+
} as const;
|
|
355
|
+
|
|
356
|
+
const api = createAuthAwareFetcher<TestPaths>({
|
|
357
|
+
baseURL: 'https://api.example.com',
|
|
358
|
+
endpointAuth,
|
|
359
|
+
securitySchemes,
|
|
360
|
+
authStrategies: {
|
|
361
|
+
bearer: { type: 'bearer', tokenProvider },
|
|
362
|
+
},
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
await api('GET /protected', {
|
|
366
|
+
headers: { 'X-Custom-Header': 'custom-value' },
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
expect(lastReceivedHeaders.authorization).toBe('Bearer valid-token');
|
|
370
|
+
expect(lastReceivedHeaders['x-custom-header']).toBe('custom-value');
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
it('should allow user headers to override auth headers', async () => {
|
|
374
|
+
const tokenProvider: TokenProvider = {
|
|
375
|
+
getValidAccessToken: vi.fn().mockResolvedValue('valid-token'),
|
|
376
|
+
createValidAuthHeaders: vi.fn().mockResolvedValue({
|
|
377
|
+
Authorization: 'Bearer valid-token',
|
|
378
|
+
}),
|
|
379
|
+
};
|
|
380
|
+
|
|
381
|
+
// Override handler to accept any valid Bearer token
|
|
382
|
+
server.use(
|
|
383
|
+
http.get('https://api.example.com/protected', ({ request }) => {
|
|
384
|
+
lastReceivedHeaders = Object.fromEntries(request.headers.entries());
|
|
385
|
+
const auth = request.headers.get('Authorization');
|
|
386
|
+
if (!auth || !auth.startsWith('Bearer ')) {
|
|
387
|
+
return HttpResponse.json(
|
|
388
|
+
{ message: 'Unauthorized' },
|
|
389
|
+
{ status: 401 },
|
|
390
|
+
);
|
|
391
|
+
}
|
|
392
|
+
return HttpResponse.json({ data: 'secret' });
|
|
393
|
+
}),
|
|
394
|
+
);
|
|
395
|
+
|
|
396
|
+
const endpointAuth = {
|
|
397
|
+
'GET /protected': 'bearer',
|
|
398
|
+
} as const;
|
|
399
|
+
|
|
400
|
+
const api = createAuthAwareFetcher<TestPaths>({
|
|
401
|
+
baseURL: 'https://api.example.com',
|
|
402
|
+
endpointAuth,
|
|
403
|
+
securitySchemes,
|
|
404
|
+
authStrategies: {
|
|
405
|
+
bearer: { type: 'bearer', tokenProvider },
|
|
406
|
+
},
|
|
407
|
+
});
|
|
408
|
+
|
|
409
|
+
await api('GET /protected', {
|
|
410
|
+
headers: { Authorization: 'Bearer override-token' },
|
|
411
|
+
});
|
|
412
|
+
|
|
413
|
+
expect(lastReceivedHeaders.authorization).toBe('Bearer override-token');
|
|
414
|
+
});
|
|
415
|
+
});
|
|
416
|
+
|
|
417
|
+
describe('onRequest interceptor', () => {
|
|
418
|
+
it('should call user onRequest interceptor after auth headers', async () => {
|
|
419
|
+
const onRequest = vi.fn((config: RequestInit) => ({
|
|
420
|
+
...config,
|
|
421
|
+
headers: {
|
|
422
|
+
...config.headers,
|
|
423
|
+
'X-Intercepted': 'true',
|
|
424
|
+
},
|
|
425
|
+
}));
|
|
426
|
+
|
|
427
|
+
const tokenProvider: TokenProvider = {
|
|
428
|
+
getValidAccessToken: vi.fn().mockResolvedValue('valid-token'),
|
|
429
|
+
createValidAuthHeaders: vi.fn().mockResolvedValue({
|
|
430
|
+
Authorization: 'Bearer valid-token',
|
|
431
|
+
}),
|
|
432
|
+
};
|
|
433
|
+
|
|
434
|
+
const endpointAuth = {
|
|
435
|
+
'GET /protected': 'bearer',
|
|
436
|
+
} as const;
|
|
437
|
+
|
|
438
|
+
const api = createAuthAwareFetcher<TestPaths>({
|
|
439
|
+
baseURL: 'https://api.example.com',
|
|
440
|
+
endpointAuth,
|
|
441
|
+
securitySchemes,
|
|
442
|
+
authStrategies: {
|
|
443
|
+
bearer: { type: 'bearer', tokenProvider },
|
|
444
|
+
},
|
|
445
|
+
onRequest,
|
|
446
|
+
});
|
|
447
|
+
|
|
448
|
+
await api('GET /protected');
|
|
449
|
+
|
|
450
|
+
expect(onRequest).toHaveBeenCalled();
|
|
451
|
+
expect(lastReceivedHeaders['x-intercepted']).toBe('true');
|
|
452
|
+
});
|
|
453
|
+
});
|
|
454
|
+
|
|
455
|
+
describe('none auth strategy', () => {
|
|
456
|
+
it('should not add any headers for none strategy', async () => {
|
|
457
|
+
const endpointAuth = {
|
|
458
|
+
'GET /users': 'noAuth',
|
|
459
|
+
} as const;
|
|
460
|
+
|
|
461
|
+
const customSchemes: Record<string, SecuritySchemeObject> = {
|
|
462
|
+
noAuth: {
|
|
463
|
+
type: 'http',
|
|
464
|
+
scheme: 'none',
|
|
465
|
+
},
|
|
466
|
+
};
|
|
467
|
+
|
|
468
|
+
const api = createAuthAwareFetcher<TestPaths>({
|
|
469
|
+
baseURL: 'https://api.example.com',
|
|
470
|
+
endpointAuth,
|
|
471
|
+
securitySchemes: customSchemes,
|
|
472
|
+
authStrategies: {
|
|
473
|
+
noAuth: { type: 'none' },
|
|
474
|
+
},
|
|
475
|
+
});
|
|
476
|
+
|
|
477
|
+
const result = await api('GET /users');
|
|
478
|
+
|
|
479
|
+
expect(result).toEqual({ users: [{ id: '1', name: 'John' }] });
|
|
480
|
+
expect(lastReceivedHeaders.authorization).toBeUndefined();
|
|
481
|
+
});
|
|
482
|
+
});
|
|
483
|
+
|
|
484
|
+
describe('missing strategy handling', () => {
|
|
485
|
+
it('should not add headers when scheme exists but strategy is missing', async () => {
|
|
486
|
+
const endpointAuth = {
|
|
487
|
+
'GET /users': 'unknownScheme',
|
|
488
|
+
} as const;
|
|
489
|
+
|
|
490
|
+
const api = createAuthAwareFetcher<TestPaths>({
|
|
491
|
+
baseURL: 'https://api.example.com',
|
|
492
|
+
endpointAuth,
|
|
493
|
+
securitySchemes: {
|
|
494
|
+
unknownScheme: { type: 'http', scheme: 'custom' },
|
|
495
|
+
},
|
|
496
|
+
// No strategy for unknownScheme
|
|
497
|
+
authStrategies: {} as any,
|
|
498
|
+
});
|
|
499
|
+
|
|
500
|
+
const result = await api('GET /users');
|
|
501
|
+
|
|
502
|
+
expect(result).toEqual({ users: [{ id: '1', name: 'John' }] });
|
|
503
|
+
});
|
|
504
|
+
});
|
|
505
|
+
});
|
|
@@ -3,8 +3,6 @@ import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
|
|
3
3
|
* @vitest-environment jsdom
|
|
4
4
|
*/
|
|
5
5
|
import { renderHook, waitFor } from '@testing-library/react';
|
|
6
|
-
// biome-ignore lint/style/useImportType: required for React provider
|
|
7
|
-
import React from 'react';
|
|
8
6
|
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
9
7
|
import { createOpenAPIHooks } from '../openapi-hooks';
|
|
10
8
|
import './setup';
|
|
@@ -152,7 +150,7 @@ function createWrapper() {
|
|
|
152
150
|
},
|
|
153
151
|
});
|
|
154
152
|
|
|
155
|
-
return ({ children }: { children:
|
|
153
|
+
return ({ children }: { children: any }) => (
|
|
156
154
|
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
|
|
157
155
|
);
|
|
158
156
|
}
|
|
@@ -426,11 +424,12 @@ describe('createOpenAPIHooks', () => {
|
|
|
426
424
|
expect(result.current.isSuccess).toBe(true);
|
|
427
425
|
});
|
|
428
426
|
|
|
429
|
-
expect(onSuccess).
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
427
|
+
expect(onSuccess).toHaveBeenCalled();
|
|
428
|
+
expect(onSuccess.mock.calls[0]?.[0]).toEqual({
|
|
429
|
+
id: '123',
|
|
430
|
+
name: 'Test',
|
|
431
|
+
email: 'test@test.com',
|
|
432
|
+
});
|
|
434
433
|
expect(onError).not.toHaveBeenCalled();
|
|
435
434
|
});
|
|
436
435
|
});
|
package/src/auth-fetcher.ts
CHANGED
|
@@ -199,7 +199,7 @@ export function createAuthAwareFetcher<
|
|
|
199
199
|
const strategy =
|
|
200
200
|
authStrategies[schemeName as UsedSecuritySchemes<EndpointAuth>];
|
|
201
201
|
|
|
202
|
-
if (strategy) {
|
|
202
|
+
if (strategy && scheme) {
|
|
203
203
|
authHeaders = await resolveAuthHeaders(strategy, scheme);
|
|
204
204
|
}
|
|
205
205
|
}
|
package/src/endpoint-hooks.ts
CHANGED
|
@@ -149,10 +149,13 @@ export function createEndpointHooks<Paths>(
|
|
|
149
149
|
() => ({
|
|
150
150
|
queryKey,
|
|
151
151
|
queryFn: () =>
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
152
|
+
// Type assertion needed due to complex conditional types
|
|
153
|
+
(
|
|
154
|
+
fetcher as (
|
|
155
|
+
endpoint: T,
|
|
156
|
+
config?: unknown,
|
|
157
|
+
) => Promise<ExtractEndpointResponse<Paths, T>>
|
|
158
|
+
)(endpoint, config),
|
|
156
159
|
...queryOptions,
|
|
157
160
|
}),
|
|
158
161
|
[
|
|
@@ -182,10 +185,13 @@ export function createEndpointHooks<Paths>(
|
|
|
182
185
|
const memoizedOptions = useMemo(
|
|
183
186
|
() => ({
|
|
184
187
|
mutationFn: (config: FilteredRequestConfig<Paths, T>) =>
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
188
|
+
// Type assertion needed due to complex conditional types
|
|
189
|
+
(
|
|
190
|
+
fetcher as (
|
|
191
|
+
endpoint: T,
|
|
192
|
+
config?: unknown,
|
|
193
|
+
) => Promise<ExtractEndpointResponse<Paths, T>>
|
|
194
|
+
)(endpoint, config),
|
|
189
195
|
...mutationOptions,
|
|
190
196
|
}),
|
|
191
197
|
[endpoint, JSON.stringify(mutationOptions)],
|
package/src/fetcher.ts
CHANGED
|
@@ -160,7 +160,7 @@ export class TypedFetcher<Paths> {
|
|
|
160
160
|
): ParseEndpoint<T> {
|
|
161
161
|
const [method, ...routeParts] = endpoint.split(' ');
|
|
162
162
|
const route = routeParts.join(' ');
|
|
163
|
-
return { method: method
|
|
163
|
+
return { method: method?.toLowerCase() ?? '', route } as ParseEndpoint<T>;
|
|
164
164
|
}
|
|
165
165
|
}
|
|
166
166
|
|
package/src/openapi-hooks.ts
CHANGED
|
@@ -66,7 +66,7 @@ type OperationParams<
|
|
|
66
66
|
: Spec extends { parameters: { path: infer P } }
|
|
67
67
|
? P
|
|
68
68
|
: never;
|
|
69
|
-
query?: Spec extends { parameters
|
|
69
|
+
query?: Spec extends { parameters?: { query?: infer Q } } ? Q : never;
|
|
70
70
|
body?: Spec extends {
|
|
71
71
|
requestBody: { content: { 'application/json': infer Body } };
|
|
72
72
|
}
|
|
@@ -129,8 +129,8 @@ export function createOpenAPIHooks<Paths>(
|
|
|
129
129
|
operationId: OpId,
|
|
130
130
|
): string {
|
|
131
131
|
// Runtime lookup from registry
|
|
132
|
-
|
|
133
|
-
|
|
132
|
+
const op = operations?.[operationId as string];
|
|
133
|
+
if (op) {
|
|
134
134
|
return `${op.method.toUpperCase()} ${op.path}`;
|
|
135
135
|
}
|
|
136
136
|
// Fallback for compile-time only usage
|
package/src/types.ts
CHANGED
|
@@ -154,15 +154,17 @@ export type FilteredRequestConfig<
|
|
|
154
154
|
: never;
|
|
155
155
|
|
|
156
156
|
/**
|
|
157
|
-
* Helper to build request config with correct required/optional fields
|
|
157
|
+
* Helper to build request config with correct required/optional fields.
|
|
158
|
+
* Uses [T] extends [never] pattern to prevent distribution over union types,
|
|
159
|
+
* which would cause the entire type to become `never`.
|
|
158
160
|
*/
|
|
159
161
|
type BuildRequestConfig<TParams, TQuery, TBody> = SimplifyIntersection<
|
|
160
162
|
// params: required if not never
|
|
161
|
-
(TParams extends never ? {} : { params: TParams }) &
|
|
163
|
+
([TParams] extends [never] ? {} : { params: TParams }) &
|
|
162
164
|
// body: required if not never
|
|
163
|
-
(TBody extends never ? {} : { body: TBody }) &
|
|
165
|
+
([TBody] extends [never] ? {} : { body: TBody }) &
|
|
164
166
|
// query: optional if not never
|
|
165
|
-
(TQuery extends never ? {} : { query?: TQuery }) & {
|
|
167
|
+
([TQuery] extends [never] ? {} : { query?: TQuery }) & {
|
|
166
168
|
// headers: always optional
|
|
167
169
|
headers?: Record<string, string>;
|
|
168
170
|
}
|
package/tsconfig.json
ADDED