68 lines
2.6 KiB
TypeScript
68 lines
2.6 KiB
TypeScript
import { afterEach, expect, test } from "bun:test";
|
|
import { createContext, withContextHeaders } from "@wrnexus/core";
|
|
import type { OAuthProvider } from "@wrnexus/oauth";
|
|
import { createAuthEngine } from "../src/engine.ts";
|
|
import { createAuthHttpHandlers } from "../src/http/index.ts";
|
|
import { MemoryAuthStore } from "../src/stores/memory.ts";
|
|
|
|
const realFetch = globalThis.fetch;
|
|
afterEach(() => {
|
|
globalThis.fetch = realFetch;
|
|
});
|
|
|
|
test("configured OAuth owns PKCE state, callback, and session establishment", async () => {
|
|
const provider: OAuthProvider = {
|
|
name: "example",
|
|
authorizeUrl: "https://identity.example/authorize",
|
|
tokenUrl: "https://identity.example/token",
|
|
userInfoUrl: "https://identity.example/user",
|
|
scopes: ["openid", "email"],
|
|
clientId: "client",
|
|
clientSecret: "secret",
|
|
mapProfile: (raw) => ({
|
|
id: String(raw.id),
|
|
email: String(raw.email),
|
|
raw,
|
|
}),
|
|
};
|
|
const engine = createAuthEngine({
|
|
store: new MemoryAuthStore(),
|
|
secret: "oauth-http-secret-that-is-longer-than-thirty-two-characters",
|
|
});
|
|
const handlers = createAuthHttpHandlers({
|
|
engine,
|
|
baseUrl: "https://app.example",
|
|
oauth: { example: provider },
|
|
});
|
|
const startRequest = new Request(
|
|
"https://app.example/api/auth/oauth/example?returnTo=%2Fdashboard",
|
|
);
|
|
const startCtx = createContext(startRequest, new URL(startRequest.url));
|
|
startCtx.params = { provider: "example" };
|
|
const start = await handlers.startOAuth(startCtx);
|
|
expect(start.status).toBe(302);
|
|
const location = new URL(start.headers.get("location")!);
|
|
expect(location.searchParams.get("code_challenge_method")).toBe("S256");
|
|
const issued = withContextHeaders(startCtx, start).headers.get("set-cookie")!;
|
|
|
|
const replies = [
|
|
Response.json({ access_token: "access", token_type: "Bearer" }),
|
|
Response.json({ id: "provider-user", email: "oauth@example.test" }),
|
|
];
|
|
const mock: typeof fetch = Object.assign(async () => replies.shift()!, {
|
|
preconnect: () => undefined,
|
|
});
|
|
globalThis.fetch = mock;
|
|
|
|
const callbackRequest = new Request(
|
|
`https://app.example/api/auth/oauth/example/callback?code=code&state=${encodeURIComponent(location.searchParams.get("state")!)}`,
|
|
{ headers: { cookie: issued.split(";", 1)[0]! } },
|
|
);
|
|
const callbackCtx = createContext(callbackRequest, new URL(callbackRequest.url));
|
|
callbackCtx.params = { provider: "example" };
|
|
const callback = await handlers.completeOAuth(callbackCtx);
|
|
expect(callback.status).toBe(303);
|
|
expect(callback.headers.get("location")).toBe("https://app.example/dashboard");
|
|
expect(callbackCtx.user).toMatchObject({ id: expect.any(String) });
|
|
});
|