stack/apps/e2e/tests/js/oauth.test.ts
Mantra e2dc5f5ee0
[codex] fix OAuth redirect contract (#1393)
## Summary

- Route browser OAuth redirects through the configured `redirectMethod`
instead of hardcoded `window.location` calls.
- Keep OAuth redirect APIs pending after navigation starts, including
custom redirect methods.
- Add `cliAuthConfirm` handler URL metadata and custom-page prompt
coverage.
- Update SDK spec text for browser OAuth callback and `returnTo`
behavior.

## Root Cause

OAuth helpers previously combined URL construction with direct browser
navigation. That bypassed configured redirect methods and made it too
easy for public redirect APIs to resolve after navigation started.

## Impact

Browser SDK consumers get consistent redirect behavior across built-in
and custom navigation methods. `returnTo` is handled as the
post-callback destination while the OAuth callback URL remains fixed to
the configured handler route.

## Validation

- `pnpm test run packages/template/src/lib/auth.test.ts`
- `pnpm test run apps/e2e/tests/js/oauth.test.ts`
- `pnpm -C packages/template lint`
- `pnpm -C apps/e2e lint`
- `pnpm -C packages/template typecheck`
- `pnpm -C apps/e2e typecheck`

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
* Added CLI authorization confirmation page/flow for terminal-based
auth.
* Added optional returnTo parameter for OAuth to control post-auth
redirects.
* Exposed configurable redirect behavior so apps follow the chosen
redirect method.

* **Bug Fixes**
* OAuth callback now uses app navigation/queued redirects and shows a
fallback link instead of forcing location.assign.

* **Tests**
* Added unit and e2e tests covering OAuth URL generation, scope
handling, and CLI auth confirmation.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-04-28 16:33:59 -07:00

109 lines
3.0 KiB
TypeScript

import { it, localRedirectUrl } from "../helpers";
import { createApp } from "./js-helpers";
it("adds provider_scope from oauthScopesOnSignIn for authenticate flow", async ({ expect }) => {
const { clientApp } = await createApp(
{
config: {
oauthProviders: [
{
id: "github",
type: "standard",
clientId: "test_client_id",
clientSecret: "test_client_secret",
},
],
},
},
{
client: {
redirectMethod: "window",
oauthScopesOnSignIn: {
github: ["repo"],
},
},
}
);
// Patch window/document and call the real SDK API (signInWithOAuth)
const previousWindow = globalThis.window;
const previousDocument = globalThis.document;
let assignedUrl: string | null = null;
globalThis.document = { cookie: "", createElement: () => ({}) } as any;
globalThis.window = {
location: {
href: localRedirectUrl,
assign: (url: string) => {
assignedUrl = url;
throw new Error("INTENTIONAL_TEST_ABORT");
},
},
} as any;
try {
await expect(clientApp.signInWithOAuth("github")).rejects.toThrowError("INTENTIONAL_TEST_ABORT");
} finally {
globalThis.window = previousWindow;
globalThis.document = previousDocument;
}
// The SDK now receives the OAuth provider URL directly via JSON response
const oauthUrl = new URL(assignedUrl!);
const scope = decodeURIComponent(oauthUrl.searchParams.get("scope")!);
expect(scope).toBe("user:email repo");
}, { timeout: 40_000 });
it("does not resolve signInWithOAuth after a custom redirectMethod starts navigation", async ({ expect }) => {
const navigatedUrls: string[] = [];
const { clientApp } = await createApp(
{
config: {
oauthProviders: [
{
id: "github",
type: "standard",
clientId: "test_client_id",
clientSecret: "test_client_secret",
},
],
},
},
{
client: {
redirectMethod: {
useNavigate: () => (url) => {
navigatedUrls.push(url);
},
navigate: (url) => {
navigatedUrls.push(url);
},
},
},
}
);
const previousWindow = globalThis.window;
const previousDocument = globalThis.document;
globalThis.document = { cookie: "", createElement: () => ({}) } as any;
globalThis.window = {
location: {
href: localRedirectUrl,
},
} as any;
try {
const redirectResult = clientApp.signInWithOAuth("github").then(() => "resolved");
const result = await Promise.race([
redirectResult,
new Promise<string>((resolve) => setTimeout(() => resolve("pending"), 5000)),
]);
expect(navigatedUrls).toHaveLength(1);
expect(new URL(navigatedUrls[0]).pathname).toBe("/login/oauth/authorize");
expect(result).toBe("pending");
} finally {
globalThis.window = previousWindow;
globalThis.document = previousDocument;
}
}, { timeout: 40_000 });