mcp-oauth-provider 0.0.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.
Files changed (44) hide show
  1. package/README.md +668 -0
  2. package/dist/__tests__/config.test.js +56 -0
  3. package/dist/__tests__/config.test.js.map +1 -0
  4. package/dist/__tests__/integration.test.js +341 -0
  5. package/dist/__tests__/integration.test.js.map +1 -0
  6. package/dist/__tests__/oauth-flow.test.js +201 -0
  7. package/dist/__tests__/oauth-flow.test.js.map +1 -0
  8. package/dist/__tests__/server.test.js +271 -0
  9. package/dist/__tests__/server.test.js.map +1 -0
  10. package/dist/__tests__/storage.test.js +256 -0
  11. package/dist/__tests__/storage.test.js.map +1 -0
  12. package/dist/client/config.js +30 -0
  13. package/dist/client/config.js.map +1 -0
  14. package/dist/client/factory.js +16 -0
  15. package/dist/client/factory.js.map +1 -0
  16. package/dist/client/index.js +237 -0
  17. package/dist/client/index.js.map +1 -0
  18. package/dist/client/oauth-flow.js +73 -0
  19. package/dist/client/oauth-flow.js.map +1 -0
  20. package/dist/client/storage.js +237 -0
  21. package/dist/client/storage.js.map +1 -0
  22. package/dist/index.js +12 -0
  23. package/dist/index.js.map +1 -0
  24. package/dist/server/callback.js +164 -0
  25. package/dist/server/callback.js.map +1 -0
  26. package/dist/server/index.js +8 -0
  27. package/dist/server/index.js.map +1 -0
  28. package/dist/server/templates.js +245 -0
  29. package/dist/server/templates.js.map +1 -0
  30. package/package.json +66 -0
  31. package/src/__tests__/config.test.ts +78 -0
  32. package/src/__tests__/integration.test.ts +398 -0
  33. package/src/__tests__/oauth-flow.test.ts +276 -0
  34. package/src/__tests__/server.test.ts +391 -0
  35. package/src/__tests__/storage.test.ts +329 -0
  36. package/src/client/config.ts +134 -0
  37. package/src/client/factory.ts +19 -0
  38. package/src/client/index.ts +361 -0
  39. package/src/client/oauth-flow.ts +115 -0
  40. package/src/client/storage.ts +335 -0
  41. package/src/index.ts +31 -0
  42. package/src/server/callback.ts +257 -0
  43. package/src/server/index.ts +21 -0
  44. package/src/server/templates.ts +271 -0
@@ -0,0 +1,276 @@
1
+ import { describe, expect, mock, test } from 'bun:test';
2
+ import type { OAuthTokens } from '../client/config.js';
3
+ import {
4
+ areTokensExpired,
5
+ calculateTokenExpiry,
6
+ refreshTokensWithRetry,
7
+ } from '../client/oauth-flow.js';
8
+
9
+ describe('oauth-flow utilities', () => {
10
+ describe('areTokensExpired', () => {
11
+ test('should return true for undefined tokens', () => {
12
+ expect(areTokensExpired(undefined)).toBe(true);
13
+ });
14
+
15
+ test('should return true when tokenExpiryTime is in the past', () => {
16
+ const tokens: OAuthTokens = {
17
+ access_token: 'test-token',
18
+ token_type: 'Bearer',
19
+ expires_in: 3600,
20
+ };
21
+ const pastExpiryTime = Date.now() - 1000;
22
+
23
+ expect(areTokensExpired(tokens, pastExpiryTime)).toBe(true);
24
+ });
25
+
26
+ test('should return true when tokenExpiryTime is within buffer period', () => {
27
+ const tokens: OAuthTokens = {
28
+ access_token: 'test-token',
29
+ token_type: 'Bearer',
30
+ expires_in: 3600,
31
+ };
32
+ // Expiry time 2 minutes in future (less than 5 minute buffer)
33
+ const soonExpiryTime = Date.now() + 120 * 1000;
34
+
35
+ expect(areTokensExpired(tokens, soonExpiryTime, 300)).toBe(true);
36
+ });
37
+
38
+ test('should return false when tokenExpiryTime is beyond buffer period', () => {
39
+ const tokens: OAuthTokens = {
40
+ access_token: 'test-token',
41
+ token_type: 'Bearer',
42
+ expires_in: 3600,
43
+ };
44
+ // Expiry time 10 minutes in future (beyond 5 minute buffer)
45
+ const futureExpiryTime = Date.now() + 600 * 1000;
46
+
47
+ expect(areTokensExpired(tokens, futureExpiryTime, 300)).toBe(false);
48
+ });
49
+
50
+ test('should return false when no expiry info and no tokenExpiryTime', () => {
51
+ const tokens: OAuthTokens = {
52
+ access_token: 'test-token',
53
+ token_type: 'Bearer',
54
+ };
55
+
56
+ expect(areTokensExpired(tokens)).toBe(false);
57
+ });
58
+
59
+ test('should return true when expires_in is less than buffer', () => {
60
+ const tokens: OAuthTokens = {
61
+ access_token: 'test-token',
62
+ token_type: 'Bearer',
63
+ expires_in: 100, // 100 seconds
64
+ };
65
+
66
+ expect(areTokensExpired(tokens, undefined, 300)).toBe(true);
67
+ });
68
+
69
+ test('should return false when expires_in is greater than buffer', () => {
70
+ const tokens: OAuthTokens = {
71
+ access_token: 'test-token',
72
+ token_type: 'Bearer',
73
+ expires_in: 600, // 600 seconds
74
+ };
75
+
76
+ expect(areTokensExpired(tokens, undefined, 300)).toBe(false);
77
+ });
78
+
79
+ test('should use custom buffer period', () => {
80
+ const tokens: OAuthTokens = {
81
+ access_token: 'test-token',
82
+ token_type: 'Bearer',
83
+ expires_in: 150,
84
+ };
85
+
86
+ expect(areTokensExpired(tokens, undefined, 100)).toBe(false);
87
+ expect(areTokensExpired(tokens, undefined, 200)).toBe(true);
88
+ });
89
+ });
90
+
91
+ describe('calculateTokenExpiry', () => {
92
+ test('should calculate correct expiry timestamp', () => {
93
+ const expiresIn = 3600; // 1 hour
94
+ const before = Date.now();
95
+ const expiry = calculateTokenExpiry(expiresIn);
96
+ const after = Date.now();
97
+ const expectedMin = before + expiresIn * 1000;
98
+ const expectedMax = after + expiresIn * 1000;
99
+
100
+ expect(expiry).toBeGreaterThanOrEqual(expectedMin);
101
+ expect(expiry).toBeLessThanOrEqual(expectedMax);
102
+ });
103
+
104
+ test('should handle different expiry values', () => {
105
+ const testCases = [60, 300, 3600, 7200];
106
+
107
+ for (const expiresIn of testCases) {
108
+ const expiry = calculateTokenExpiry(expiresIn);
109
+ const expectedApprox = Date.now() + expiresIn * 1000;
110
+ // Allow 100ms tolerance
111
+
112
+ expect(Math.abs(expiry - expectedApprox)).toBeLessThan(100);
113
+ }
114
+ });
115
+ });
116
+
117
+ describe('refreshTokensWithRetry', () => {
118
+ test('should successfully refresh tokens on first attempt', async () => {
119
+ const mockTokens: OAuthTokens = {
120
+ access_token: 'new-access-token',
121
+ token_type: 'Bearer',
122
+ expires_in: 3600,
123
+ refresh_token: 'new-refresh-token',
124
+ };
125
+ const mockFetch = mock(() =>
126
+ Promise.resolve(
127
+ new Response(JSON.stringify(mockTokens), {
128
+ status: 200,
129
+ headers: { 'Content-Type': 'application/json' },
130
+ })
131
+ )
132
+ );
133
+ const clientInfo = {
134
+ client_id: 'test-client-id',
135
+ client_secret: 'test-client-secret',
136
+ };
137
+
138
+ const result = await refreshTokensWithRetry(
139
+ 'https://auth.example.com',
140
+ clientInfo,
141
+ 'old-refresh-token',
142
+ undefined,
143
+ 3,
144
+ 100,
145
+ mockFetch as unknown as typeof fetch
146
+ );
147
+
148
+ expect(result).toEqual(mockTokens);
149
+ expect(mockFetch).toHaveBeenCalledTimes(1);
150
+ });
151
+
152
+ test('should retry on failure and eventually succeed', async () => {
153
+ const mockTokens: OAuthTokens = {
154
+ access_token: 'new-access-token',
155
+ token_type: 'Bearer',
156
+ expires_in: 3600,
157
+ refresh_token: 'refresh-token', // Include the refresh token that was sent
158
+ };
159
+ let callCount = 0;
160
+ const mockFetch = mock(() => {
161
+ callCount++;
162
+ if (callCount < 2) {
163
+ return Promise.reject(new Error('Network error'));
164
+ }
165
+
166
+ return Promise.resolve(
167
+ new Response(JSON.stringify(mockTokens), {
168
+ status: 200,
169
+ headers: { 'Content-Type': 'application/json' },
170
+ })
171
+ );
172
+ });
173
+ const clientInfo = { client_id: 'test-client-id' };
174
+
175
+ const result = await refreshTokensWithRetry(
176
+ 'https://auth.example.com',
177
+ clientInfo,
178
+ 'refresh-token',
179
+ undefined,
180
+ 3,
181
+ 10, // Short delay for testing
182
+ mockFetch as unknown as typeof fetch
183
+ );
184
+
185
+ expect(result).toEqual(mockTokens);
186
+ expect(callCount).toBe(2);
187
+ });
188
+
189
+ test('should throw error after max retries', async () => {
190
+ const mockFetch = mock(() => Promise.reject(new Error('Network error')));
191
+ const clientInfo = { client_id: 'test-client-id' };
192
+
193
+ await expect(
194
+ refreshTokensWithRetry(
195
+ 'https://auth.example.com',
196
+ clientInfo,
197
+ 'refresh-token',
198
+ undefined,
199
+ 3,
200
+ 10,
201
+ mockFetch as unknown as typeof fetch
202
+ )
203
+ ).rejects.toThrow('Token refresh failed after 3 attempts');
204
+
205
+ expect(mockFetch).toHaveBeenCalledTimes(3);
206
+ });
207
+
208
+ test('should use exponential backoff for retries', async () => {
209
+ const callTimes: number[] = [];
210
+ const mockFetch = mock(() => {
211
+ callTimes.push(Date.now());
212
+
213
+ return Promise.reject(new Error('Network error'));
214
+ });
215
+ const clientInfo = { client_id: 'test-client-id' };
216
+ const baseDelay = 50;
217
+
218
+ try {
219
+ await refreshTokensWithRetry(
220
+ 'https://auth.example.com',
221
+ clientInfo,
222
+ 'refresh-token',
223
+ undefined,
224
+ 3,
225
+ baseDelay,
226
+ mockFetch as unknown as typeof fetch
227
+ );
228
+ } catch {
229
+ // Expected to fail
230
+ }
231
+
232
+ // Should have made 3 attempts
233
+ expect(callTimes.length).toBe(3);
234
+
235
+ // Calculate delays between attempts
236
+ const delay1 = callTimes[1]! - callTimes[0]!;
237
+ const delay2 = callTimes[2]! - callTimes[1]!;
238
+
239
+ // Verify exponential backoff (delays should increase)
240
+ // First retry should be approximately baseDelay * 1
241
+ expect(delay1).toBeGreaterThanOrEqual(baseDelay);
242
+ // Second retry should be approximately baseDelay * 2 and greater than first
243
+ expect(delay2).toBeGreaterThan(delay1);
244
+ expect(delay2).toBeGreaterThanOrEqual(baseDelay * 2);
245
+ });
246
+
247
+ test('should pass custom addClientAuth function', async () => {
248
+ const mockTokens: OAuthTokens = {
249
+ access_token: 'new-token',
250
+ token_type: 'Bearer',
251
+ };
252
+ const mockFetch = mock(() =>
253
+ Promise.resolve(
254
+ new Response(JSON.stringify(mockTokens), { status: 200 })
255
+ )
256
+ );
257
+ const mockAddClientAuth = mock(() => {
258
+ // Mock implementation
259
+ });
260
+ const clientInfo = { client_id: 'test-client-id' };
261
+
262
+ await refreshTokensWithRetry(
263
+ 'https://auth.example.com',
264
+ clientInfo,
265
+ 'refresh-token',
266
+ mockAddClientAuth,
267
+ 3,
268
+ 10,
269
+ mockFetch as unknown as typeof fetch
270
+ );
271
+
272
+ // The SDK's refreshAuthorization should have been called with addClientAuth
273
+ expect(mockFetch).toHaveBeenCalled();
274
+ });
275
+ });
276
+ });
@@ -0,0 +1,391 @@
1
+ import { afterEach, describe, expect, test } from 'bun:test';
2
+ import { OAuthCallbackServer } from '../server/callback.js';
3
+
4
+ describe('OAuthCallbackServer', () => {
5
+ let server: OAuthCallbackServer;
6
+ let testPort = 9876;
7
+ const testHostname = 'localhost';
8
+
9
+ const startServer = async () => {
10
+ if (server) {
11
+ await server.stop();
12
+ }
13
+
14
+ // Use a new port for each test to avoid port conflicts
15
+ testPort++;
16
+
17
+ server = new OAuthCallbackServer();
18
+
19
+ return server.start({
20
+ port: testPort,
21
+ hostname: testHostname,
22
+ });
23
+ };
24
+
25
+ afterEach(async () => {
26
+ if (server) {
27
+ try {
28
+ await server.stop();
29
+ } catch (error) {
30
+ // Ignore "Server stopped" errors from rejected promises
31
+ if (
32
+ error instanceof Error &&
33
+ !error.message.includes('Server stopped')
34
+ ) {
35
+ throw error;
36
+ }
37
+ }
38
+ }
39
+ });
40
+
41
+ describe('server lifecycle', () => {
42
+ test('should start server successfully', async () => {
43
+ await startServer();
44
+
45
+ // Verify server is running by making a request
46
+ const response = await fetch(
47
+ `http://${testHostname}:${testPort}/unknown`
48
+ );
49
+
50
+ expect(response.status).toBe(404);
51
+ });
52
+
53
+ test('should stop server successfully', async () => {
54
+ await startServer();
55
+
56
+ expect(server.isRunning()).toBe(true);
57
+
58
+ await server.stop();
59
+
60
+ expect(server.isRunning()).toBe(false);
61
+ });
62
+
63
+ test('should throw error when starting already running server', async () => {
64
+ await startServer();
65
+
66
+ await expect(
67
+ server.start({
68
+ port: testPort + 1,
69
+ hostname: testHostname,
70
+ })
71
+ ).rejects.toThrow('Server is already running');
72
+ });
73
+
74
+ test('should handle abort signal during start', async () => {
75
+ const abortController = new AbortController();
76
+
77
+ abortController.abort();
78
+
79
+ await expect(
80
+ server.start({
81
+ port: testPort,
82
+ hostname: testHostname,
83
+ signal: abortController.signal,
84
+ })
85
+ ).rejects.toThrow('Operation aborted');
86
+ });
87
+
88
+ test('should stop server when abort signal is triggered', async () => {
89
+ const abortController = new AbortController();
90
+
91
+ await server.start({
92
+ port: testPort,
93
+ hostname: testHostname,
94
+ signal: abortController.signal,
95
+ });
96
+
97
+ expect(server.isRunning()).toBe(true);
98
+
99
+ // Trigger abort
100
+ abortController.abort();
101
+
102
+ // Give it a moment to cleanup
103
+ await new Promise(resolve => setTimeout(resolve, 100));
104
+
105
+ expect(server.isRunning()).toBe(false);
106
+ });
107
+ });
108
+
109
+ describe('callback handling', () => {
110
+ test('should handle successful OAuth callback', async () => {
111
+ await startServer();
112
+ const callbackPath = '/callback/success';
113
+ const expectedCode = 'test-auth-code-12345';
114
+ const expectedState = 'test-state-67890';
115
+
116
+ // Start waiting for callback first
117
+ const callbackPromise = server.waitForCallback(callbackPath, 5000);
118
+
119
+ // Delay to ensure listener registration completes
120
+ await new Promise(resolve => setTimeout(resolve, 10));
121
+
122
+ // Make the HTTP request
123
+ const fetchPromise = fetch(
124
+ `http://${testHostname}:${testPort}${callbackPath}?code=${expectedCode}&state=${expectedState}`
125
+ );
126
+
127
+ const [result, response] = await Promise.all([
128
+ callbackPromise,
129
+ fetchPromise,
130
+ ]);
131
+
132
+ // Should get success page
133
+ expect(response.status).toBe(200);
134
+
135
+ const html = await response.text();
136
+
137
+ expect(html).toContain('Authorization Successful');
138
+
139
+ // Should resolve with callback data
140
+ expect(result).toMatchObject({
141
+ code: expectedCode,
142
+ state: expectedState,
143
+ });
144
+ });
145
+
146
+ test('should handle OAuth error callback', async () => {
147
+ await startServer();
148
+ const callbackPath = '/callback/error';
149
+ const expectedError = 'access_denied';
150
+ const expectedDescription = 'User denied authorization';
151
+
152
+ const callbackPromise = server.waitForCallback(callbackPath, 5000);
153
+
154
+ await new Promise(resolve => setTimeout(resolve, 10));
155
+
156
+ const fetchPromise = fetch(
157
+ `http://${testHostname}:${testPort}${callbackPath}?error=${expectedError}&error_description=${encodeURIComponent(expectedDescription)}`
158
+ );
159
+
160
+ const [result, response] = await Promise.all([
161
+ callbackPromise,
162
+ fetchPromise,
163
+ ]);
164
+
165
+ expect(response.status).toBe(400);
166
+
167
+ const html = await response.text();
168
+
169
+ expect(html).toContain('Authorization Failed');
170
+
171
+ expect(result).toMatchObject({
172
+ error: expectedError,
173
+ error_description: expectedDescription,
174
+ });
175
+ });
176
+
177
+ test('should timeout when callback takes too long', async () => {
178
+ await startServer();
179
+ const callbackPath = '/callback/timeout';
180
+
181
+ // Wait with very short timeout
182
+ await expect(server.waitForCallback(callbackPath, 100)).rejects.toThrow(
183
+ 'OAuth callback timeout'
184
+ );
185
+ });
186
+
187
+ test('should handle multiple callback parameters', async () => {
188
+ await startServer();
189
+ const callbackPath = '/oauth/callback';
190
+ const params = {
191
+ code: 'auth-code',
192
+ state: 'state-value',
193
+ scope: 'openid profile email',
194
+ session_state: 'session-123',
195
+ };
196
+ const queryString = new URLSearchParams(params).toString();
197
+
198
+ const callbackPromise = server.waitForCallback(callbackPath, 5000);
199
+
200
+ await new Promise(resolve => setTimeout(resolve, 10));
201
+
202
+ const fetchPromise = fetch(
203
+ `http://${testHostname}:${testPort}${callbackPath}?${queryString}`
204
+ );
205
+
206
+ const [result] = await Promise.all([callbackPromise, fetchPromise]);
207
+
208
+ expect(result).toMatchObject(params);
209
+ });
210
+
211
+ test('should return 404 for unknown paths', async () => {
212
+ await startServer();
213
+ const response = await fetch(
214
+ `http://${testHostname}:${testPort}/unknown-path`
215
+ );
216
+
217
+ expect(response.status).toBe(404);
218
+
219
+ const text = await response.text();
220
+
221
+ expect(text).toBe('Not Found');
222
+ });
223
+
224
+ test('should cleanup listeners after successful callback', async () => {
225
+ await startServer();
226
+ const callbackPath = '/callback/cleanup';
227
+
228
+ // First callback
229
+ await Promise.all([
230
+ server.waitForCallback(callbackPath, 5000),
231
+ fetch(
232
+ `http://${testHostname}:${testPort}${callbackPath}?code=code1&state=state1`
233
+ ),
234
+ ]);
235
+
236
+ // Second callback to same path should work
237
+ const [result2] = await Promise.all([
238
+ server.waitForCallback(callbackPath, 5000),
239
+ fetch(
240
+ `http://${testHostname}:${testPort}${callbackPath}?code=code2&state=state2`
241
+ ),
242
+ ]);
243
+
244
+ expect(result2.code).toBe('code2');
245
+ });
246
+
247
+ test('should use custom success HTML template', async () => {
248
+ await startServer();
249
+ const customServer = new OAuthCallbackServer();
250
+
251
+ await customServer.start({
252
+ hostname: testHostname,
253
+ port: testPort + 1,
254
+ successHtml: '<html><body>Custom Success!</body></html>',
255
+ });
256
+
257
+ const callbackPath = '/callback/custom-success';
258
+
259
+ const [, response] = await Promise.all([
260
+ customServer.waitForCallback(callbackPath, 5000),
261
+ fetch(
262
+ `http://${testHostname}:${testPort + 1}${callbackPath}?code=test`
263
+ ),
264
+ ]);
265
+
266
+ const html = await response.text();
267
+
268
+ expect(html).toContain('Custom Success!');
269
+
270
+ await customServer.stop();
271
+ });
272
+
273
+ test('should use custom error HTML template', async () => {
274
+ await startServer();
275
+ const customServer = new OAuthCallbackServer();
276
+
277
+ await customServer.start({
278
+ hostname: testHostname,
279
+ port: testPort + 2,
280
+ errorHtml: '<html><body>Custom Error!</body></html>',
281
+ });
282
+
283
+ const callbackPath = '/callback/custom-error';
284
+
285
+ const [, response] = await Promise.all([
286
+ customServer.waitForCallback(callbackPath, 5000),
287
+ fetch(
288
+ `http://${testHostname}:${testPort + 2}${callbackPath}?error=test`
289
+ ),
290
+ ]);
291
+
292
+ const html = await response.text();
293
+
294
+ expect(html).toContain('Custom Error!');
295
+
296
+ await customServer.stop();
297
+ });
298
+
299
+ test('should call onRequest callback', async () => {
300
+ await startServer();
301
+ await server.stop();
302
+
303
+ const requests: Request[] = [];
304
+
305
+ // Use a new port for this second server instance
306
+ testPort++;
307
+
308
+ await server.start({
309
+ port: testPort,
310
+ hostname: testHostname,
311
+ onRequest: req => requests.push(req),
312
+ });
313
+
314
+ await fetch(`http://${testHostname}:${testPort}/test`);
315
+
316
+ expect(requests).toHaveLength(1);
317
+ expect(requests[0]).toBeInstanceOf(Request);
318
+ });
319
+ });
320
+
321
+ describe('edge cases', () => {
322
+ test('should handle callback with no query parameters', async () => {
323
+ await startServer();
324
+ const callbackPath = '/callback/no-params';
325
+
326
+ // Start waiting for callback first to ensure listener is registered
327
+ const callbackPromise = server.waitForCallback(callbackPath, 5000);
328
+
329
+ // Small delay to ensure listener registration completes
330
+ await new Promise(resolve => setImmediate(resolve));
331
+
332
+ // Now make the HTTP request
333
+ const fetchPromise = fetch(
334
+ `http://${testHostname}:${testPort}${callbackPath}`
335
+ );
336
+
337
+ const [result] = await Promise.all([callbackPromise, fetchPromise]);
338
+
339
+ expect(result).toEqual({});
340
+ });
341
+
342
+ test('should handle special characters in parameters', async () => {
343
+ await startServer();
344
+ const callbackPath = '/callback/special-chars';
345
+ const specialState = 'state-with-special-chars_123';
346
+
347
+ const callbackPromise = server.waitForCallback(callbackPath, 5000);
348
+
349
+ await new Promise(resolve => setTimeout(resolve, 10));
350
+
351
+ const fetchPromise = fetch(
352
+ `http://${testHostname}:${testPort}${callbackPath}?code=test&state=${encodeURIComponent(specialState)}`
353
+ );
354
+
355
+ const [result] = await Promise.all([callbackPromise, fetchPromise]);
356
+
357
+ expect(result.state).toBe(specialState);
358
+ });
359
+
360
+ test('should handle concurrent callbacks to different paths', async () => {
361
+ await startServer();
362
+ const path1 = '/callback1';
363
+ const path2 = '/callback2';
364
+
365
+ // Register both listeners first
366
+ const promise1 = server.waitForCallback(path1, 5000);
367
+ const promise2 = server.waitForCallback(path2, 5000);
368
+
369
+ // Use a longer delay to ensure both listeners are registered
370
+ await new Promise(resolve => setTimeout(resolve, 10));
371
+
372
+ // Now make both HTTP requests
373
+ const fetch1 = fetch(
374
+ `http://${testHostname}:${testPort}${path1}?code=code1&state=state1`
375
+ );
376
+ const fetch2 = fetch(
377
+ `http://${testHostname}:${testPort}${path2}?code=code2&state=state2`
378
+ );
379
+
380
+ const [result1, result2] = await Promise.all([
381
+ promise1,
382
+ promise2,
383
+ fetch1,
384
+ fetch2,
385
+ ]);
386
+
387
+ expect(result1.code).toBe('code1');
388
+ expect(result2.code).toBe('code2');
389
+ });
390
+ });
391
+ });