@mcpfn/testing 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.
@@ -0,0 +1,545 @@
1
+ // src/playwright.ts
2
+ import { randomUUID as randomUUID2 } from "crypto";
3
+ import {
4
+ expect,
5
+ test as base
6
+ } from "@playwright/test";
7
+
8
+ // src/mock-oauth-server.ts
9
+ import { createHash, randomBytes, randomUUID } from "crypto";
10
+ import { createServer } from "http";
11
+
12
+ // src/auth-core.ts
13
+ var McpFnAuthAssertionError = class extends Error {
14
+ constructor(message) {
15
+ super(message);
16
+ this.name = "McpFnAuthAssertionError";
17
+ }
18
+ };
19
+ var MCPFN_OAUTH_EXTENSION_GRANTS = {
20
+ jwtBearer: "urn:ietf:params:oauth:grant-type:jwt-bearer",
21
+ deviceCode: "urn:ietf:params:oauth:grant-type:device_code",
22
+ generic: "urn:example:params:oauth:grant-type:extension"
23
+ };
24
+ function createOAuthClientMetadataFixture(options) {
25
+ return {
26
+ ...options.extraMetadata,
27
+ client_id: new URL(options.clientId).toString(),
28
+ client_name: options.clientName ?? "McpFn OAuth test client",
29
+ redirect_uris: [...new Set(options.redirectUris.map((uri) => new URL(uri).toString()))],
30
+ response_types: ["code"],
31
+ grant_types: [
32
+ "authorization_code",
33
+ "refresh_token",
34
+ ...new Set(options.extraGrantTypes ?? [])
35
+ ],
36
+ token_endpoint_auth_method: "none"
37
+ };
38
+ }
39
+ function createOAuthClientMetadataVariants(baseUrl, redirectUri = new URL("/callback", baseUrl).toString()) {
40
+ const base2 = new URL(baseUrl);
41
+ const fixture = (name, extraGrantTypes = []) => createOAuthClientMetadataFixture({
42
+ clientId: new URL(`/client-metadata/${name}`, base2).toString(),
43
+ redirectUris: [redirectUri],
44
+ clientName: `McpFn ${name} client`,
45
+ extraGrantTypes
46
+ });
47
+ return {
48
+ basic: fixture("basic"),
49
+ jwtBearerExtension: fixture("jwt-bearer-extension", [MCPFN_OAUTH_EXTENSION_GRANTS.jwtBearer]),
50
+ deviceCodeExtension: fixture("device-code-extension", [MCPFN_OAUTH_EXTENSION_GRANTS.deviceCode]),
51
+ genericExtension: fixture("generic-extension", [MCPFN_OAUTH_EXTENSION_GRANTS.generic])
52
+ };
53
+ }
54
+ function assertAuthorizationCodeClientMetadata(metadata, expectedClientId) {
55
+ if (expectedClientId && metadata.client_id !== new URL(expectedClientId).toString()) {
56
+ throw new McpFnAuthAssertionError("OAuth client metadata client_id does not match its document URL");
57
+ }
58
+ if (!metadata.response_types.includes("code")) {
59
+ throw new McpFnAuthAssertionError("OAuth client metadata does not support response_type=code");
60
+ }
61
+ if (!metadata.grant_types.includes("authorization_code")) {
62
+ throw new McpFnAuthAssertionError("OAuth client metadata does not support authorization_code");
63
+ }
64
+ if (!metadata.redirect_uris.length) {
65
+ throw new McpFnAuthAssertionError("OAuth client metadata must declare at least one redirect URI");
66
+ }
67
+ return metadata;
68
+ }
69
+
70
+ // src/mock-oauth-server.ts
71
+ function base64Url(value) {
72
+ return value.toString("base64url");
73
+ }
74
+ function createPkcePair() {
75
+ const verifier = base64Url(randomBytes(48));
76
+ return {
77
+ verifier,
78
+ challenge: createHash("sha256").update(verifier).digest("base64url"),
79
+ method: "S256"
80
+ };
81
+ }
82
+ function uniqueScopes(value) {
83
+ return [...new Set((value ?? "").split(/\s+/).filter(Boolean))].sort();
84
+ }
85
+ function json(status, body) {
86
+ return Response.json(body, {
87
+ status,
88
+ headers: { "cache-control": "no-store" }
89
+ });
90
+ }
91
+ function oauthError(status, error, description) {
92
+ return json(status, { error, error_description: description });
93
+ }
94
+ function escapeHtml(value) {
95
+ return value.replaceAll("&", "&amp;").replaceAll('"', "&quot;").replaceAll("<", "&lt;").replaceAll(">", "&gt;");
96
+ }
97
+ function formValue(form, name) {
98
+ const value = form.get(name);
99
+ return value === null || value === "" ? void 0 : value;
100
+ }
101
+ var McpFnMockOAuthAuthorizationServer = class {
102
+ issuer;
103
+ clientId;
104
+ redirectUris;
105
+ scopesSupported;
106
+ clientMetadata;
107
+ codes = /* @__PURE__ */ new Map();
108
+ accessTokens = /* @__PURE__ */ new Map();
109
+ refreshTokens = /* @__PURE__ */ new Map();
110
+ callbacks = [];
111
+ clock;
112
+ accessTokenLifetimeSeconds;
113
+ authorizationCodeLifetimeSeconds;
114
+ refreshTokenLifetimeSeconds;
115
+ constructor(options) {
116
+ this.issuer = new URL(options.issuer);
117
+ this.issuer.pathname = this.issuer.pathname.replace(/\/?$/, "/");
118
+ this.clientId = options.clientId ?? "mcpfn-playwright";
119
+ this.redirectUris = options.redirectUris?.map((uri) => new URL(uri).toString()) ?? [
120
+ new URL("callback", this.issuer).toString()
121
+ ];
122
+ this.scopesSupported = [...new Set(options.scopesSupported ?? ["mcp:read", "mcp:write"])].sort();
123
+ this.clock = options.clock ?? Date.now;
124
+ this.accessTokenLifetimeSeconds = options.accessTokenLifetimeSeconds ?? 300;
125
+ this.authorizationCodeLifetimeSeconds = options.authorizationCodeLifetimeSeconds ?? 120;
126
+ this.refreshTokenLifetimeSeconds = options.refreshTokenLifetimeSeconds ?? 3600;
127
+ this.clientMetadata = createOAuthClientMetadataVariants(
128
+ this.issuer,
129
+ this.redirectUris[0]
130
+ );
131
+ }
132
+ get metadataUrl() {
133
+ return new URL(".well-known/oauth-authorization-server", this.issuer).toString();
134
+ }
135
+ get authorizationEndpoint() {
136
+ return new URL("authorize", this.issuer).toString();
137
+ }
138
+ get tokenEndpoint() {
139
+ return new URL("token", this.issuer).toString();
140
+ }
141
+ get revocationEndpoint() {
142
+ return new URL("revoke", this.issuer).toString();
143
+ }
144
+ get callbackUrl() {
145
+ return this.redirectUris[0];
146
+ }
147
+ clientMetadataUrl(variant) {
148
+ return this.clientMetadata[variant].client_id;
149
+ }
150
+ authorizationUrl(input) {
151
+ const url = new URL(this.authorizationEndpoint);
152
+ url.searchParams.set("response_type", "code");
153
+ url.searchParams.set("client_id", input.clientId ?? this.clientId);
154
+ url.searchParams.set("redirect_uri", input.redirectUri ?? this.callbackUrl);
155
+ url.searchParams.set("code_challenge", input.codeChallenge);
156
+ url.searchParams.set("code_challenge_method", "S256");
157
+ url.searchParams.set("scope", (input.scopes ?? ["mcp:read"]).join(" "));
158
+ if (input.state) url.searchParams.set("state", input.state);
159
+ if (input.resource) url.searchParams.set("resource", input.resource);
160
+ for (const [name, value] of Object.entries(input.extraParameters ?? {})) {
161
+ url.searchParams.set(name, value);
162
+ }
163
+ return url.toString();
164
+ }
165
+ latestCallback() {
166
+ return this.callbacks.at(-1);
167
+ }
168
+ expireAccessToken(token) {
169
+ const record = this.accessTokens.get(token);
170
+ if (record) record.expiresAt = this.nowSeconds() - 1;
171
+ }
172
+ revokeToken(token) {
173
+ const access = this.accessTokens.get(token);
174
+ if (access) access.revoked = true;
175
+ const refresh = this.refreshTokens.get(token);
176
+ if (refresh) refresh.revoked = true;
177
+ }
178
+ async verifyAccessToken(token) {
179
+ const record = this.accessTokens.get(token);
180
+ if (!record || record.revoked || record.expiresAt <= this.nowSeconds()) {
181
+ throw new Error("Invalid access token");
182
+ }
183
+ return {
184
+ token,
185
+ clientId: record.clientId,
186
+ scopes: record.scopes,
187
+ expiresAt: record.expiresAt,
188
+ ...record.resource ? { resource: new URL(record.resource) } : {}
189
+ };
190
+ }
191
+ async handle(request) {
192
+ const url = new URL(request.url);
193
+ const path = url.pathname.replace(this.issuer.pathname.replace(/\/$/, ""), "") || "/";
194
+ if (request.method === "GET" && path === "/.well-known/oauth-authorization-server") {
195
+ return json(200, {
196
+ issuer: this.issuer.toString(),
197
+ authorization_endpoint: this.authorizationEndpoint,
198
+ token_endpoint: this.tokenEndpoint,
199
+ revocation_endpoint: this.revocationEndpoint,
200
+ response_types_supported: ["code"],
201
+ grant_types_supported: ["authorization_code", "refresh_token"],
202
+ code_challenge_methods_supported: ["S256"],
203
+ token_endpoint_auth_methods_supported: ["none"],
204
+ scopes_supported: this.scopesSupported
205
+ });
206
+ }
207
+ if (request.method === "GET" && path.startsWith("/client-metadata/")) {
208
+ const metadata = Object.values(this.clientMetadata).find((candidate) => new URL(candidate.client_id).pathname === url.pathname);
209
+ return metadata ? json(200, metadata) : json(404, { error: "not_found" });
210
+ }
211
+ if (request.method === "GET" && path === "/authorize") {
212
+ return this.authorize(url);
213
+ }
214
+ if (request.method === "POST" && path === "/token") {
215
+ return this.token(await request.text());
216
+ }
217
+ if (request.method === "POST" && path === "/revoke") {
218
+ return this.revoke(await request.text());
219
+ }
220
+ if (request.method === "GET" && path === "/callback") {
221
+ const parameters = Object.fromEntries(url.searchParams);
222
+ this.callbacks.push({ url: url.toString(), parameters });
223
+ return new Response(
224
+ `<!doctype html><html><body><main><h1>OAuth callback received</h1><pre>${escapeHtml(JSON.stringify(parameters, null, 2))}</pre></main></body></html>`,
225
+ { headers: { "content-type": "text/html; charset=utf-8" } }
226
+ );
227
+ }
228
+ return json(404, { error: "not_found" });
229
+ }
230
+ nowSeconds() {
231
+ return Math.floor(this.clock() / 1e3);
232
+ }
233
+ resolveClient(clientId) {
234
+ if (clientId === this.clientId) {
235
+ return { clientId, redirectUris: this.redirectUris };
236
+ }
237
+ const metadata = Object.values(this.clientMetadata).find((candidate) => candidate.client_id === clientId);
238
+ if (!metadata) return void 0;
239
+ assertAuthorizationCodeClientMetadata(metadata, clientId);
240
+ return { clientId, redirectUris: metadata.redirect_uris };
241
+ }
242
+ authorize(url) {
243
+ const clientId = url.searchParams.get("client_id") ?? "";
244
+ const redirectUri = url.searchParams.get("redirect_uri") ?? "";
245
+ const client = this.resolveClient(clientId);
246
+ if (!client) return oauthError(400, "invalid_client", "Unknown OAuth client");
247
+ if (!client.redirectUris.includes(redirectUri)) {
248
+ return oauthError(400, "invalid_request", "redirect_uri is not registered");
249
+ }
250
+ if (url.searchParams.get("response_type") !== "code") {
251
+ return this.authorizationRedirect(url, redirectUri, "unsupported_response_type");
252
+ }
253
+ const codeChallenge = url.searchParams.get("code_challenge");
254
+ if (!codeChallenge || url.searchParams.get("code_challenge_method") !== "S256") {
255
+ return this.authorizationRedirect(url, redirectUri, "invalid_request", "PKCE S256 is required");
256
+ }
257
+ const requestedScopes = uniqueScopes(url.searchParams.get("scope"));
258
+ if (requestedScopes.some((scope) => !this.scopesSupported.includes(scope))) {
259
+ return this.authorizationRedirect(url, redirectUri, "invalid_scope");
260
+ }
261
+ const decision = url.searchParams.get("decision");
262
+ if (!decision) {
263
+ const hidden = [...url.searchParams].filter(([name]) => name !== "decision").map(([name, value]) => `<input type="hidden" name="${escapeHtml(name)}" value="${escapeHtml(value)}">`).join("");
264
+ return new Response(
265
+ `<!doctype html><html><body><main><h1>Authorize MCP client</h1><p>${escapeHtml(clientId)}</p><form method="get" action="${escapeHtml(this.authorizationEndpoint)}">${hidden}<button name="decision" value="approve">Approve</button><button name="decision" value="deny">Deny</button></form></main></body></html>`,
266
+ { headers: { "content-type": "text/html; charset=utf-8", "cache-control": "no-store" } }
267
+ );
268
+ }
269
+ if (decision === "deny") {
270
+ return this.authorizationRedirect(url, redirectUri, "access_denied");
271
+ }
272
+ if (decision !== "approve") {
273
+ return oauthError(400, "invalid_request", "Unknown authorization decision");
274
+ }
275
+ const code = `mcpfn_code_${randomUUID()}`;
276
+ this.codes.set(code, {
277
+ clientId,
278
+ redirectUri,
279
+ codeChallenge,
280
+ scopes: requestedScopes,
281
+ resource: url.searchParams.get("resource") ?? void 0,
282
+ expiresAt: this.nowSeconds() + this.authorizationCodeLifetimeSeconds,
283
+ used: false
284
+ });
285
+ const callback = new URL(redirectUri);
286
+ callback.searchParams.set("code", code);
287
+ const state = url.searchParams.get("state");
288
+ if (state) callback.searchParams.set("state", state);
289
+ return Response.redirect(callback, 302);
290
+ }
291
+ authorizationRedirect(requestUrl, redirectUri, error, description) {
292
+ const callback = new URL(redirectUri);
293
+ callback.searchParams.set("error", error);
294
+ if (description) callback.searchParams.set("error_description", description);
295
+ const state = requestUrl.searchParams.get("state");
296
+ if (state) callback.searchParams.set("state", state);
297
+ return Response.redirect(callback, 302);
298
+ }
299
+ token(source) {
300
+ const form = new URLSearchParams(source);
301
+ const grantType = formValue(form, "grant_type");
302
+ if (grantType === "authorization_code") return this.exchangeAuthorizationCode(form);
303
+ if (grantType === "refresh_token") return this.exchangeRefreshToken(form);
304
+ return oauthError(400, "unsupported_grant_type", "The grant type is not supported");
305
+ }
306
+ exchangeAuthorizationCode(form) {
307
+ const code = formValue(form, "code");
308
+ const record = code ? this.codes.get(code) : void 0;
309
+ if (!record || record.used || record.expiresAt <= this.nowSeconds()) {
310
+ return oauthError(400, "invalid_grant", "Authorization code is invalid or expired");
311
+ }
312
+ if (formValue(form, "client_id") !== record.clientId || formValue(form, "redirect_uri") !== record.redirectUri) {
313
+ return oauthError(400, "invalid_grant", "Authorization code binding does not match");
314
+ }
315
+ const verifier = formValue(form, "code_verifier");
316
+ const challenge = verifier ? createHash("sha256").update(verifier).digest("base64url") : void 0;
317
+ if (challenge !== record.codeChallenge) {
318
+ return oauthError(400, "invalid_grant", "PKCE verification failed");
319
+ }
320
+ record.used = true;
321
+ return json(200, this.issueTokenSet(record));
322
+ }
323
+ exchangeRefreshToken(form) {
324
+ const refreshToken = formValue(form, "refresh_token");
325
+ const record = refreshToken ? this.refreshTokens.get(refreshToken) : void 0;
326
+ if (!record || record.revoked || record.expiresAt <= this.nowSeconds()) {
327
+ return oauthError(400, "invalid_grant", "Refresh token is invalid or expired");
328
+ }
329
+ if (formValue(form, "client_id") !== record.clientId) {
330
+ return oauthError(400, "invalid_grant", "Refresh token client binding does not match");
331
+ }
332
+ const requestedScopes = form.has("scope") ? uniqueScopes(form.get("scope")) : record.scopes;
333
+ if (requestedScopes.some((scope) => !record.scopes.includes(scope))) {
334
+ return oauthError(400, "invalid_scope", "Refresh cannot increase granted scopes");
335
+ }
336
+ record.revoked = true;
337
+ return json(200, this.issueTokenSet({ ...record, scopes: requestedScopes }));
338
+ }
339
+ issueTokenSet(input) {
340
+ const accessToken = `mcpfn_access_${randomUUID()}`;
341
+ const refreshToken = `mcpfn_refresh_${randomUUID()}`;
342
+ this.accessTokens.set(accessToken, {
343
+ ...input,
344
+ expiresAt: this.nowSeconds() + this.accessTokenLifetimeSeconds,
345
+ revoked: false
346
+ });
347
+ this.refreshTokens.set(refreshToken, {
348
+ ...input,
349
+ expiresAt: this.nowSeconds() + this.refreshTokenLifetimeSeconds,
350
+ revoked: false
351
+ });
352
+ return {
353
+ access_token: accessToken,
354
+ token_type: "Bearer",
355
+ expires_in: this.accessTokenLifetimeSeconds,
356
+ refresh_token: refreshToken,
357
+ scope: input.scopes.join(" "),
358
+ ...input.resource ? { resource: input.resource } : {}
359
+ };
360
+ }
361
+ revoke(source) {
362
+ const token = new URLSearchParams(source).get("token");
363
+ if (token) this.revokeToken(token);
364
+ return new Response(null, { status: 200 });
365
+ }
366
+ };
367
+ async function toRequest(req, origin) {
368
+ const chunks = [];
369
+ for await (const chunk of req) chunks.push(Buffer.from(chunk));
370
+ const headers = new Headers();
371
+ for (const [name, value] of Object.entries(req.headers)) {
372
+ if (Array.isArray(value)) {
373
+ for (const entry of value) headers.append(name, entry);
374
+ } else if (value !== void 0) {
375
+ headers.set(name, value);
376
+ }
377
+ }
378
+ const body = chunks.length ? Buffer.concat(chunks) : void 0;
379
+ const init = {
380
+ method: req.method,
381
+ headers,
382
+ ...body ? { body, duplex: "half" } : {}
383
+ };
384
+ return new Request(new URL(req.url ?? "/", origin), init);
385
+ }
386
+ async function writeResponse(res, response) {
387
+ res.statusCode = response.status;
388
+ response.headers.forEach((value, name) => res.setHeader(name, value));
389
+ res.end(Buffer.from(await response.arrayBuffer()));
390
+ }
391
+ async function startMockOAuthAuthorizationServer(options = {}) {
392
+ let oauth;
393
+ let origin = "";
394
+ const server = createServer(async (req, res) => {
395
+ try {
396
+ if (!oauth) throw new Error("Mock OAuth server is not initialized");
397
+ await writeResponse(res, await oauth.handle(await toRequest(req, origin)));
398
+ } catch {
399
+ res.statusCode = 500;
400
+ res.setHeader("content-type", "application/json");
401
+ res.end(JSON.stringify({
402
+ error: "server_error",
403
+ error_description: "Internal Server Error"
404
+ }));
405
+ }
406
+ });
407
+ await new Promise((resolve, reject) => {
408
+ server.once("error", reject);
409
+ server.listen(options.port ?? 0, options.hostname ?? "127.0.0.1", () => {
410
+ server.off("error", reject);
411
+ resolve();
412
+ });
413
+ });
414
+ const address = server.address();
415
+ origin = `http://${address.address.includes(":") ? `[${address.address}]` : address.address}:${address.port}`;
416
+ oauth = new McpFnMockOAuthAuthorizationServer({ ...options, issuer: origin });
417
+ return {
418
+ origin,
419
+ oauth,
420
+ close: () => new Promise((resolve, reject) => {
421
+ server.close((error) => error ? reject(error) : resolve());
422
+ })
423
+ };
424
+ }
425
+
426
+ // src/playwright.ts
427
+ function createOAuthAuthorizationRequestUrl(options) {
428
+ const pkce = options.pkce ?? createPkcePair();
429
+ const state = options.state ?? `mcpfn_state_${randomUUID2()}`;
430
+ const url = new URL(options.authorizationEndpoint);
431
+ for (const [name, value] of Object.entries(options.extraParameters ?? {})) {
432
+ url.searchParams.set(name, value);
433
+ }
434
+ url.searchParams.set("response_type", "code");
435
+ url.searchParams.set("client_id", options.clientId);
436
+ url.searchParams.set("redirect_uri", options.redirectUri);
437
+ url.searchParams.set("code_challenge", pkce.challenge);
438
+ url.searchParams.set("code_challenge_method", pkce.method);
439
+ url.searchParams.set("state", state);
440
+ if (options.scopes?.length) url.searchParams.set("scope", options.scopes.join(" "));
441
+ if (options.resource) url.searchParams.set("resource", options.resource);
442
+ return { url: url.toString(), pkce, state };
443
+ }
444
+ function parseOAuthCallback(url) {
445
+ const callback = new URL(url);
446
+ return {
447
+ url: callback.toString(),
448
+ parameters: Object.fromEntries(callback.searchParams)
449
+ };
450
+ }
451
+ async function authorizeWithBrowser(page, options) {
452
+ const request = createOAuthAuthorizationRequestUrl(options);
453
+ await page.goto(request.url);
454
+ await options.beforeDecision?.(page);
455
+ const decision = options.decision ?? "approve";
456
+ const buttonName = decision === "approve" ? options.approveButtonName ?? /^approve$/i : options.denyButtonName ?? /^deny$/i;
457
+ await page.getByRole("button", { name: buttonName }).click();
458
+ const redirect = new URL(options.redirectUri);
459
+ await page.waitForURL((candidate) => candidate.origin === redirect.origin && candidate.pathname === redirect.pathname);
460
+ const callback = parseOAuthCallback(page.url());
461
+ if (callback.parameters.state !== request.state) {
462
+ throw new Error("OAuth callback state does not match the authorization request");
463
+ }
464
+ return {
465
+ authorizationUrl: request.url,
466
+ callback,
467
+ pkce: request.pkce,
468
+ state: request.state
469
+ };
470
+ }
471
+ async function exchangeAuthorizationCode(options) {
472
+ const response = await (options.fetch ?? globalThis.fetch)(options.tokenEndpoint, {
473
+ method: "POST",
474
+ headers: { "content-type": "application/x-www-form-urlencoded" },
475
+ body: new URLSearchParams({
476
+ grant_type: "authorization_code",
477
+ client_id: options.clientId,
478
+ redirect_uri: options.redirectUri,
479
+ code: options.code,
480
+ code_verifier: options.codeVerifier
481
+ })
482
+ });
483
+ const body = await response.json();
484
+ if (!response.ok) {
485
+ throw new Error(body.error_description ?? body.error ?? `Token exchange failed with ${response.status}`);
486
+ }
487
+ return body;
488
+ }
489
+ async function refreshOAuthAccessToken(options) {
490
+ const form = new URLSearchParams({
491
+ grant_type: "refresh_token",
492
+ client_id: options.clientId,
493
+ refresh_token: options.refreshToken
494
+ });
495
+ if (options.scopes?.length) form.set("scope", options.scopes.join(" "));
496
+ const response = await (options.fetch ?? globalThis.fetch)(options.tokenEndpoint, {
497
+ method: "POST",
498
+ headers: { "content-type": "application/x-www-form-urlencoded" },
499
+ body: form
500
+ });
501
+ const body = await response.json();
502
+ if (!response.ok) {
503
+ throw new Error(body.error_description ?? body.error ?? `Token refresh failed with ${response.status}`);
504
+ }
505
+ return body;
506
+ }
507
+ async function revokeOAuthToken(options) {
508
+ const response = await (options.fetch ?? globalThis.fetch)(options.revocationEndpoint, {
509
+ method: "POST",
510
+ headers: { "content-type": "application/x-www-form-urlencoded" },
511
+ body: new URLSearchParams({ token: options.token })
512
+ });
513
+ if (!response.ok) throw new Error(`Token revocation failed with ${response.status}`);
514
+ }
515
+ var test = base.extend({
516
+ mcpfnOAuth: async ({}, use) => {
517
+ const started = await startMockOAuthAuthorizationServer();
518
+ const fixture = {
519
+ started,
520
+ server: started.oauth,
521
+ authorize: (page, options = {}) => authorizeWithBrowser(page, {
522
+ ...options,
523
+ authorizationEndpoint: options.authorizationEndpoint ?? started.oauth.authorizationEndpoint,
524
+ clientId: options.clientId ?? started.oauth.clientId,
525
+ redirectUri: options.redirectUri ?? started.oauth.callbackUrl
526
+ })
527
+ };
528
+ try {
529
+ await use(fixture);
530
+ } finally {
531
+ await started.close();
532
+ }
533
+ }
534
+ });
535
+ export {
536
+ authorizeWithBrowser,
537
+ createOAuthAuthorizationRequestUrl,
538
+ exchangeAuthorizationCode,
539
+ expect,
540
+ parseOAuthCallback,
541
+ refreshOAuthAccessToken,
542
+ revokeOAuthToken,
543
+ test
544
+ };
545
+ //# sourceMappingURL=playwright.js.map