this repo has no description
at main 7.0 kB view raw
1import { beforeEach, describe, expect, it, vi } from "vitest"; 2import { 3 generateCodeChallenge, 4 generateCodeVerifier, 5 generateState, 6 saveOAuthState, 7 checkForOAuthCallback, 8 clearOAuthCallbackParams, 9} from "../lib/oauth"; 10 11describe("OAuth utilities", () => { 12 beforeEach(() => { 13 sessionStorage.clear(); 14 vi.restoreAllMocks(); 15 }); 16 17 describe("generateState", () => { 18 it("generates a 64-character hex string", () => { 19 const state = generateState(); 20 expect(state).toMatch(/^[0-9a-f]{64}$/); 21 }); 22 23 it("generates unique values", () => { 24 const states = new Set(Array.from({ length: 100 }, () => generateState())); 25 expect(states.size).toBe(100); 26 }); 27 }); 28 29 describe("generateCodeVerifier", () => { 30 it("generates a 64-character hex string", () => { 31 const verifier = generateCodeVerifier(); 32 expect(verifier).toMatch(/^[0-9a-f]{64}$/); 33 }); 34 35 it("generates unique values", () => { 36 const verifiers = new Set( 37 Array.from({ length: 100 }, () => generateCodeVerifier()), 38 ); 39 expect(verifiers.size).toBe(100); 40 }); 41 }); 42 43 describe("generateCodeChallenge", () => { 44 it("generates a base64url-encoded SHA-256 hash", async () => { 45 const verifier = "test-verifier-12345"; 46 const challenge = await generateCodeChallenge(verifier); 47 48 expect(challenge).toMatch(/^[A-Za-z0-9_-]+$/); 49 expect(challenge).not.toContain("+"); 50 expect(challenge).not.toContain("/"); 51 expect(challenge).not.toContain("="); 52 }); 53 54 it("produces consistent output for same input", async () => { 55 const verifier = "consistent-test-verifier"; 56 const challenge1 = await generateCodeChallenge(verifier); 57 const challenge2 = await generateCodeChallenge(verifier); 58 59 expect(challenge1).toBe(challenge2); 60 }); 61 62 it("produces different output for different inputs", async () => { 63 const challenge1 = await generateCodeChallenge("verifier-1"); 64 const challenge2 = await generateCodeChallenge("verifier-2"); 65 66 expect(challenge1).not.toBe(challenge2); 67 }); 68 69 it("produces correct S256 challenge", async () => { 70 const challenge = await generateCodeChallenge("dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk"); 71 expect(challenge).toBe("E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM"); 72 }); 73 }); 74 75 describe("saveOAuthState", () => { 76 it("stores state and verifier in sessionStorage", () => { 77 saveOAuthState({ state: "test-state", codeVerifier: "test-verifier" }); 78 79 expect(sessionStorage.getItem("tranquil_pds_oauth_state")).toBe( 80 "test-state", 81 ); 82 expect(sessionStorage.getItem("tranquil_pds_oauth_verifier")).toBe( 83 "test-verifier", 84 ); 85 }); 86 }); 87 88 describe("checkForOAuthCallback", () => { 89 it("returns null when no code/state in URL", () => { 90 Object.defineProperty(globalThis.location, "search", { 91 value: "", 92 writable: true, 93 configurable: true, 94 }); 95 Object.defineProperty(globalThis.location, "pathname", { 96 value: "/app/", 97 writable: true, 98 configurable: true, 99 }); 100 101 expect(checkForOAuthCallback()).toBeNull(); 102 }); 103 104 it("returns code and state when present in URL", () => { 105 Object.defineProperty(globalThis.location, "search", { 106 value: "?code=auth-code-123&state=state-456", 107 writable: true, 108 configurable: true, 109 }); 110 Object.defineProperty(globalThis.location, "pathname", { 111 value: "/app/", 112 writable: true, 113 configurable: true, 114 }); 115 116 const result = checkForOAuthCallback(); 117 expect(result).toEqual({ code: "auth-code-123", state: "state-456" }); 118 }); 119 120 it("returns null on migrate path even with code/state", () => { 121 Object.defineProperty(globalThis.location, "search", { 122 value: "?code=auth-code-123&state=state-456", 123 writable: true, 124 configurable: true, 125 }); 126 Object.defineProperty(globalThis.location, "pathname", { 127 value: "/app/migrate", 128 writable: true, 129 configurable: true, 130 }); 131 132 expect(checkForOAuthCallback()).toBeNull(); 133 }); 134 135 it("returns null when only code is present", () => { 136 Object.defineProperty(globalThis.location, "search", { 137 value: "?code=auth-code-123", 138 writable: true, 139 configurable: true, 140 }); 141 Object.defineProperty(globalThis.location, "pathname", { 142 value: "/app/", 143 writable: true, 144 configurable: true, 145 }); 146 147 expect(checkForOAuthCallback()).toBeNull(); 148 }); 149 150 it("returns null when only state is present", () => { 151 Object.defineProperty(globalThis.location, "search", { 152 value: "?state=state-456", 153 writable: true, 154 configurable: true, 155 }); 156 Object.defineProperty(globalThis.location, "pathname", { 157 value: "/app/", 158 writable: true, 159 configurable: true, 160 }); 161 162 expect(checkForOAuthCallback()).toBeNull(); 163 }); 164 }); 165 166 describe("clearOAuthCallbackParams", () => { 167 it("removes query params from URL", () => { 168 const replaceStateSpy = vi.spyOn(globalThis.history, "replaceState"); 169 170 Object.defineProperty(globalThis.location, "href", { 171 value: "http://localhost:3000/app/?code=123&state=456", 172 writable: true, 173 configurable: true, 174 }); 175 176 clearOAuthCallbackParams(); 177 178 expect(replaceStateSpy).toHaveBeenCalled(); 179 const callArgs = replaceStateSpy.mock.calls[0]; 180 expect(callArgs[0]).toEqual({}); 181 expect(callArgs[1]).toBe(""); 182 const urlString = callArgs[2] as string; 183 expect(urlString).toBe("http://localhost:3000/app/"); 184 expect(urlString).not.toContain("?"); 185 expect(urlString).not.toContain("code="); 186 expect(urlString).not.toContain("state="); 187 }); 188 }); 189}); 190 191describe("DPoP proof generation", () => { 192 it("base64url encoding produces valid output", async () => { 193 const testData = new Uint8Array([72, 101, 108, 108, 111]); 194 const buffer = testData.buffer; 195 196 const binary = Array.from(testData, (byte) => String.fromCharCode(byte)).join(""); 197 const base64url = btoa(binary) 198 .replace(/\+/g, "-") 199 .replace(/\//g, "_") 200 .replace(/=+$/, ""); 201 202 expect(base64url).toBe("SGVsbG8"); 203 expect(base64url).not.toContain("+"); 204 expect(base64url).not.toContain("/"); 205 expect(base64url).not.toContain("="); 206 }); 207 208 it("JWK thumbprint uses correct key ordering for EC keys", () => { 209 const jwk = { 210 kty: "EC", 211 crv: "P-256", 212 x: "test-x", 213 y: "test-y", 214 }; 215 216 const canonical = JSON.stringify({ 217 crv: jwk.crv, 218 kty: jwk.kty, 219 x: jwk.x, 220 y: jwk.y, 221 }); 222 223 expect(canonical).toBe('{"crv":"P-256","kty":"EC","x":"test-x","y":"test-y"}'); 224 225 const keys = Object.keys(JSON.parse(canonical)); 226 expect(keys).toEqual(["crv", "kty", "x", "y"]); 227 228 for (let i = 1; i < keys.length; i++) { 229 expect(keys[i - 1] < keys[i]).toBe(true); 230 } 231 }); 232});