Something went wrong. Try again.
Our Personal Data Server from scratch!
Something went wrong. Try again.
7.1 kB · 239 lines
TypeScript
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240import { beforeEach, describe, expect, it, vi } from "vitest";import { checkForOAuthCallback, clearOAuthCallbackParams, generateCodeChallenge, generateCodeVerifier, generateState, saveOAuthState,} from "../lib/oauth.ts";
describe("OAuth utilities", () => { beforeEach(() => { sessionStorage.clear(); vi.restoreAllMocks(); });
describe("generateState", () => { it("generates a 64-character hex string", () => { const state = generateState(); expect(state).toMatch(/^[0-9a-f]{64}$/); });
it("generates unique values", () => { const states = new Set( Array.from({ length: 100 }, () => generateState()), ); expect(states.size).toBe(100); }); });
describe("generateCodeVerifier", () => { it("generates a 64-character hex string", () => { const verifier = generateCodeVerifier(); expect(verifier).toMatch(/^[0-9a-f]{64}$/); });
it("generates unique values", () => { const verifiers = new Set( Array.from({ length: 100 }, () => generateCodeVerifier()), ); expect(verifiers.size).toBe(100); }); });
describe("generateCodeChallenge", () => { it("generates a base64url-encoded SHA-256 hash", async () => { const verifier = "test-verifier-12345"; const challenge = await generateCodeChallenge(verifier);
expect(challenge).toMatch(/^[A-Za-z0-9_-]+$/); expect(challenge).not.toContain("+"); expect(challenge).not.toContain("/"); expect(challenge).not.toContain("="); });
it("produces consistent output for same input", async () => { const verifier = "consistent-test-verifier"; const challenge1 = await generateCodeChallenge(verifier); const challenge2 = await generateCodeChallenge(verifier);
expect(challenge1).toBe(challenge2); });
it("produces different output for different inputs", async () => { const challenge1 = await generateCodeChallenge("verifier-1"); const challenge2 = await generateCodeChallenge("verifier-2");
expect(challenge1).not.toBe(challenge2); });
it("produces correct S256 challenge", async () => { const challenge = await generateCodeChallenge( "dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk", ); expect(challenge).toBe("E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM"); }); });
describe("saveOAuthState", () => { it("stores state and verifier in sessionStorage", () => { saveOAuthState({ state: "test-state", codeVerifier: "test-verifier" });
expect(sessionStorage.getItem("tranquil_pds_oauth_state")).toBe( "test-state", ); expect(sessionStorage.getItem("tranquil_pds_oauth_verifier")).toBe( "test-verifier", ); }); });
describe("checkForOAuthCallback", () => { it("returns null when no code/state in URL", () => { Object.defineProperty(globalThis.location, "search", { value: "", writable: true, configurable: true, }); Object.defineProperty(globalThis.location, "pathname", { value: "/app/", writable: true, configurable: true, });
expect(checkForOAuthCallback()).toBeNull(); });
it("returns code and state when present in URL", () => { Object.defineProperty(globalThis.location, "search", { value: "?code=auth-code-123&state=state-456", writable: true, configurable: true, }); Object.defineProperty(globalThis.location, "pathname", { value: "/app/", writable: true, configurable: true, });
const result = checkForOAuthCallback(); expect(result).toEqual({ code: "auth-code-123", state: "state-456" }); });
it("returns null on migrate path even with code/state", () => { Object.defineProperty(globalThis.location, "search", { value: "?code=auth-code-123&state=state-456", writable: true, configurable: true, }); Object.defineProperty(globalThis.location, "pathname", { value: "/app/migrate", writable: true, configurable: true, });
expect(checkForOAuthCallback()).toBeNull(); });
it("returns null when only code is present", () => { Object.defineProperty(globalThis.location, "search", { value: "?code=auth-code-123", writable: true, configurable: true, }); Object.defineProperty(globalThis.location, "pathname", { value: "/app/", writable: true, configurable: true, });
expect(checkForOAuthCallback()).toBeNull(); });
it("returns null when only state is present", () => { Object.defineProperty(globalThis.location, "search", { value: "?state=state-456", writable: true, configurable: true, }); Object.defineProperty(globalThis.location, "pathname", { value: "/app/", writable: true, configurable: true, });
expect(checkForOAuthCallback()).toBeNull(); }); });
describe("clearOAuthCallbackParams", () => { it("removes query params from URL", () => { const replaceStateSpy = vi.spyOn(globalThis.history, "replaceState");
Object.defineProperty(globalThis.location, "href", { value: "http://localhost:3000/app/?code=123&state=456", writable: true, configurable: true, });
clearOAuthCallbackParams();
expect(replaceStateSpy).toHaveBeenCalled(); const callArgs = replaceStateSpy.mock.calls[0]; expect(callArgs[0]).toEqual({}); expect(callArgs[1]).toBe(""); const urlString = callArgs[2] as string; expect(urlString).toBe("http://localhost:3000/app/"); expect(urlString).not.toContain("?"); expect(urlString).not.toContain("code="); expect(urlString).not.toContain("state="); }); });});
describe("DPoP proof generation", () => { it("base64url encoding produces valid output", async () => { const testData = new Uint8Array([72, 101, 108, 108, 111]); const _buffer = testData.buffer;
const binary = Array.from(testData, (byte) => String.fromCharCode(byte)) .join(""); const base64url = btoa(binary) .replace(/\+/g, "-") .replace(/\//g, "_") .replace(/=+$/, "");
expect(base64url).toBe("SGVsbG8"); expect(base64url).not.toContain("+"); expect(base64url).not.toContain("/"); expect(base64url).not.toContain("="); });
it("JWK thumbprint uses correct key ordering for EC keys", () => { const jwk = { kty: "EC", crv: "P-256", x: "test-x", y: "test-y", };
const canonical = JSON.stringify({ crv: jwk.crv, kty: jwk.kty, x: jwk.x, y: jwk.y, });
expect(canonical).toBe( '{"crv":"P-256","kty":"EC","x":"test-x","y":"test-y"}', );
const keys = Object.keys(JSON.parse(canonical)); expect(keys).toEqual(["crv", "kty", "x", "y"]);
for (let i = 1; i < keys.length; i++) { expect(keys[i - 1] < keys[i]).toBe(true); } });});