Skip to content

Commit f60625a

Browse files
authored
test: cover PKCE callback token exchange (#69)
* test: cover PKCE callback token exchange * docs: move agent instructions
1 parent 882129c commit f60625a

3 files changed

Lines changed: 66 additions & 5 deletions

File tree

.github/instructions/oauth-testing.instructions.md renamed to docs/instructions/oauth-testing.instructions.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,5 +8,5 @@ applyTo: "test/**,src/**"
88
- Keep `pnpm test` and GitHub Actions deterministic: no live provider calls, browser automation, or OAuth secrets; model provider behavior with mocks.
99
- Exercise callback behavior through `createCallbackEndpoint(...).handler(req)`; avoid replacing callback assertions with direct mock Payload `create` or `update` calls.
1010
- Preserve provider matrix coverage for Google, Zitadel, Apple, and Microsoft Entra ID; share contract helpers but keep provider quirks in fixtures.
11-
- Use mocked external-provider integration for authorize + callback happy paths, create/update branches, provider response failures, token request bodies, and PKCE authorize behavior; `pnpm test` runs this layer with the rest of the suite.
11+
- Use mocked external-provider integration for authorize + callback happy paths, create/update branches, provider response failures, token request bodies, PKCE authorize behavior, and PKCE callback `code_verifier` exchange; `pnpm test` runs this layer with the rest of the suite.
1212
- Keep roundtrip tests for callback-issued JWTs authenticating through the auth strategy, and idempotency tests for plugin collection wiring.

.github/instructions/troubleshooting-nextjs-rsc-oauth-authorize.instructions.md renamed to docs/instructions/troubleshooting-nextjs-rsc-oauth-authorize.instructions.md

File renamed without changes.

test/mocked-provider-integration.spec.ts

Lines changed: 65 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -57,10 +57,17 @@ const findEndpoint = (
5757
const authCodeFor = (provider: OAuthProviderTestCase) =>
5858
`${provider.strategyName}-auth-code`;
5959

60+
const mergeHeaders = (base: HeadersInit, extra: HeadersInit = {}) => {
61+
const headers = new Headers(base);
62+
new Headers(extra).forEach((value, key) => headers.set(key, value));
63+
return headers;
64+
};
65+
6066
const createCallbackRequest = (
6167
provider: OAuthProviderTestCase,
6268
usersCollection: CollectionConfig,
6369
context: OAuthTestContext = createMockOAuthTestContext({ foundUsers: [] }),
70+
headers: HeadersInit = {},
6471
): PayloadRequest => {
6572
const payload = createMockPayload(context);
6673
payload.collections.users.config = usersCollection;
@@ -69,9 +76,10 @@ const createCallbackRequest = (
6976
const body = `code=${encodeURIComponent(authCodeFor(provider))}`;
7077
return {
7178
payload,
72-
headers: new Headers({
73-
"content-type": "application/x-www-form-urlencoded",
74-
}),
79+
headers: mergeHeaders(
80+
{ "content-type": "application/x-www-form-urlencoded" },
81+
headers,
82+
),
7583
searchParams: new URLSearchParams(),
7684
query: {},
7785
method: "POST",
@@ -84,7 +92,7 @@ const createCallbackRequest = (
8492
const code = authCodeFor(provider);
8593
return {
8694
payload,
87-
headers: new Headers(),
95+
headers: mergeHeaders({}, headers),
8896
searchParams: new URLSearchParams({ code }),
8997
query: { code },
9098
method: "GET",
@@ -337,6 +345,59 @@ describe("Mocked external provider integration", () => {
337345
);
338346
});
339347

348+
if (!provider.createGetToken) {
349+
it("passes PKCE verifier cookie into the default token exchange", async () => {
350+
const usersCollection = buildPluginCollection(provider, {
351+
pkceEnabled: true,
352+
getPkceCodes: () => ({
353+
verifier: `${provider.strategyName}-verifier`,
354+
challenge: `${provider.strategyName}-challenge`,
355+
challengeMethod: "S256",
356+
}),
357+
});
358+
const authorizeEndpoint = findEndpoint(
359+
usersCollection.endpoints,
360+
provider.authorizePath,
361+
"get",
362+
);
363+
const callbackEndpoint = findEndpoint(
364+
usersCollection.endpoints,
365+
provider.callbackPath,
366+
provider.callbackMethod.toLowerCase(),
367+
);
368+
369+
const authorizeResponse = (await authorizeEndpoint.handler({
370+
payload: createMockPayload(createMockOAuthTestContext()),
371+
headers: new Headers(),
372+
searchParams: new URLSearchParams(),
373+
query: {},
374+
method: "GET",
375+
context: {},
376+
user: null,
377+
} as unknown as PayloadRequest)) as Response;
378+
const pkceCookie = authorizeResponse.headers
379+
.get("Set-Cookie")!
380+
.split(";")[0];
381+
const callbackRequest = createCallbackRequest(
382+
provider,
383+
usersCollection,
384+
createMockOAuthTestContext({ foundUsers: [] }),
385+
{ cookie: pkceCookie },
386+
);
387+
388+
const callbackResponse = (await callbackEndpoint.handler(
389+
callbackRequest,
390+
)) as Response;
391+
392+
expect(callbackResponse.status).toBe(302);
393+
const tokenBody = tokenRequestBodyFor(provider);
394+
expect(tokenBody.get("code")).toBe(authCodeFor(provider));
395+
expect(tokenBody.get("code_verifier")).toBe(
396+
`${provider.strategyName}-verifier`,
397+
);
398+
});
399+
}
400+
340401
it("redirects to failure when mocked token exchange fails", async () => {
341402
const usersCollection = buildPluginCollection(provider);
342403
const callbackEndpoint = findEndpoint(

0 commit comments

Comments
 (0)