Files
oh-my-opencode/src/auth/antigravity/oauth.test.ts
T
2026-01-08 23:06:24 +09:00

192 lines
5.7 KiB
TypeScript

import { describe, it, expect, beforeEach, afterEach, mock } from "bun:test"
import { buildAuthURL, exchangeCode } from "./oauth"
import { ANTIGRAVITY_CLIENT_ID, GOOGLE_TOKEN_URL } from "./constants"
describe("OAuth PKCE Removal", () => {
describe("buildAuthURL", () => {
it("should NOT include code_challenge parameter", async () => {
// #given
const projectId = "test-project"
// #when
const result = await buildAuthURL(projectId)
const url = new URL(result.url)
// #then
expect(url.searchParams.has("code_challenge")).toBe(false)
})
it("should NOT include code_challenge_method parameter", async () => {
// #given
const projectId = "test-project"
// #when
const result = await buildAuthURL(projectId)
const url = new URL(result.url)
// #then
expect(url.searchParams.has("code_challenge_method")).toBe(false)
})
it("should include state parameter for CSRF protection", async () => {
// #given
const projectId = "test-project"
// #when
const result = await buildAuthURL(projectId)
const url = new URL(result.url)
const state = url.searchParams.get("state")
// #then
expect(state).toBeTruthy()
})
it("should have state as simple random string (not JSON/base64)", async () => {
// #given
const projectId = "test-project"
// #when
const result = await buildAuthURL(projectId)
const url = new URL(result.url)
const state = url.searchParams.get("state")!
// #then - positive assertions for simple random string
expect(state.length).toBeGreaterThanOrEqual(16)
expect(state.length).toBeLessThanOrEqual(64)
// Should be URL-safe (alphanumeric, no special chars like { } " :)
expect(state).toMatch(/^[a-zA-Z0-9_-]+$/)
// Should NOT contain JSON indicators
expect(state).not.toContain("{")
expect(state).not.toContain("}")
expect(state).not.toContain('"')
})
it("should include access_type=offline", async () => {
// #given
const projectId = "test-project"
// #when
const result = await buildAuthURL(projectId)
const url = new URL(result.url)
// #then
expect(url.searchParams.get("access_type")).toBe("offline")
})
it("should include prompt=consent", async () => {
// #given
const projectId = "test-project"
// #when
const result = await buildAuthURL(projectId)
const url = new URL(result.url)
// #then
expect(url.searchParams.get("prompt")).toBe("consent")
})
it("should NOT return verifier property (PKCE removed)", async () => {
// #given
const projectId = "test-project"
// #when
const result = await buildAuthURL(projectId)
// #then
expect(result).not.toHaveProperty("verifier")
expect(result).toHaveProperty("url")
expect(result).toHaveProperty("state")
})
it("should return state that matches URL state param", async () => {
// #given
const projectId = "test-project"
// #when
const result = await buildAuthURL(projectId)
const url = new URL(result.url)
// #then
expect(result.state).toBe(url.searchParams.get("state")!)
})
})
describe("exchangeCode", () => {
let originalFetch: typeof fetch
beforeEach(() => {
originalFetch = globalThis.fetch
})
afterEach(() => {
globalThis.fetch = originalFetch
})
it("should NOT send code_verifier in token exchange", async () => {
// #given
let capturedBody: string | null = null
globalThis.fetch = mock(async (url: string, init?: RequestInit) => {
if (url === GOOGLE_TOKEN_URL) {
capturedBody = init?.body as string
return new Response(JSON.stringify({
access_token: "test-access",
refresh_token: "test-refresh",
expires_in: 3600,
token_type: "Bearer"
}))
}
return new Response("", { status: 404 })
}) as unknown as typeof fetch
// #when
await exchangeCode("test-code", "http://localhost:51121/oauth-callback")
// #then
expect(capturedBody).toBeTruthy()
const params = new URLSearchParams(capturedBody!)
expect(params.has("code_verifier")).toBe(false)
})
it("should send required OAuth parameters", async () => {
// #given
let capturedBody: string | null = null
globalThis.fetch = mock(async (url: string, init?: RequestInit) => {
if (url === GOOGLE_TOKEN_URL) {
capturedBody = init?.body as string
return new Response(JSON.stringify({
access_token: "test-access",
refresh_token: "test-refresh",
expires_in: 3600,
token_type: "Bearer"
}))
}
return new Response("", { status: 404 })
}) as unknown as typeof fetch
// #when
await exchangeCode("test-code", "http://localhost:51121/oauth-callback")
// #then
const params = new URLSearchParams(capturedBody!)
expect(params.get("grant_type")).toBe("authorization_code")
expect(params.get("code")).toBe("test-code")
expect(params.get("client_id")).toBe(ANTIGRAVITY_CLIENT_ID)
expect(params.get("redirect_uri")).toBe("http://localhost:51121/oauth-callback")
})
})
describe("State/CSRF Validation", () => {
it("should generate unique state for each call", async () => {
// #given
const projectId = "test-project"
// #when
const result1 = await buildAuthURL(projectId)
const result2 = await buildAuthURL(projectId)
// #then
expect(result1.state).not.toBe(result2.state)
})
})
})