goose/oidc-proxy/test/index.test.js
Michael Neale 7449a96664
Some checks failed
Canary / Prepare Version (push) Waiting to run
Canary / build-cli (push) Blocked by required conditions
Canary / Upload Install Script (push) Blocked by required conditions
Canary / bundle-desktop (push) Blocked by required conditions
Canary / bundle-desktop-intel (push) Blocked by required conditions
Canary / bundle-desktop-linux (push) Blocked by required conditions
Canary / bundle-desktop-windows (push) Blocked by required conditions
Canary / Release (push) Blocked by required conditions
Unused Dependencies / machete (push) Waiting to run
CI / changes (push) Waiting to run
CI / Check Rust Code Format (push) Blocked by required conditions
CI / Build and Test Rust Project (push) Blocked by required conditions
CI / Build Rust Project on Windows (push) Waiting to run
CI / Lint Rust Code (push) Blocked by required conditions
CI / Check OpenAPI Schema is Up-to-Date (push) Blocked by required conditions
CI / Test and Lint Electron Desktop App (push) Blocked by required conditions
Live Provider Tests / check-fork (push) Waiting to run
Live Provider Tests / changes (push) Blocked by required conditions
Live Provider Tests / Build Binary (push) Blocked by required conditions
Live Provider Tests / Smoke Tests (push) Blocked by required conditions
Live Provider Tests / Smoke Tests (Code Execution) (push) Blocked by required conditions
Live Provider Tests / Compaction Tests (push) Blocked by required conditions
Live Provider Tests / goose server HTTP integration tests (push) Blocked by required conditions
Publish Docker Image / docker (push) Waiting to run
Scorecard supply-chain security / Scorecard analysis (push) Waiting to run
Cargo Deny / deny (push) Has been cancelled
Deploy Documentation / deploy (push) Has been cancelled
Publish Ask AI Bot Docker Image / docker (push) Has been cancelled
Publish to npm / Generate ACP Schema (push) Has been cancelled
Publish to npm / Build goose CLI (darwin-arm64) (push) Has been cancelled
Publish to npm / Build goose CLI (darwin-x64) (push) Has been cancelled
Publish to npm / Build goose CLI (linux-arm64) (push) Has been cancelled
Publish to npm / Build goose CLI (linux-x64) (push) Has been cancelled
Publish to npm / Release to npm (push) Has been cancelled
docs: rework homepage and add aaif migration blog post (#8356)
Signed-off-by: Michael Neale <michael.neale@gmail.com>
2026-04-07 07:18:04 +00:00

306 lines
9.3 KiB
JavaScript

import {
env,
createExecutionContext,
waitOnExecutionContext,
fetchMock,
} from "cloudflare:test";
import { describe, it, expect, beforeAll, afterEach } from "vitest";
import worker from "../src/index.js";
let testKeyPair;
let testJwk;
const TEST_KID = "test-kid-001";
beforeAll(async () => {
testKeyPair = await crypto.subtle.generateKey(
{
name: "RSASSA-PKCS1-v1_5",
modulusLength: 2048,
publicExponent: new Uint8Array([1, 0, 1]),
hash: "SHA-256",
},
true,
["sign", "verify"],
);
const exported = await crypto.subtle.exportKey("jwk", testKeyPair.publicKey);
testJwk = { ...exported, kid: TEST_KID, alg: "RS256", use: "sig" };
});
afterEach(() => {
fetchMock.deactivate();
});
function base64UrlEncode(data) {
const str = typeof data === "string" ? data : JSON.stringify(data);
return btoa(str).replace(/=/g, "").replace(/\+/g, "-").replace(/\//g, "_");
}
async function createSignedJwt(payload, kid = TEST_KID) {
const header = { alg: "RS256", typ: "JWT", kid };
const headerB64 = base64UrlEncode(header);
const payloadB64 = base64UrlEncode(payload);
const data = new TextEncoder().encode(`${headerB64}.${payloadB64}`);
const signature = await crypto.subtle.sign(
"RSASSA-PKCS1-v1_5",
testKeyPair.privateKey,
data,
);
const sigB64 = btoa(String.fromCharCode(...new Uint8Array(signature)))
.replace(/=/g, "")
.replace(/\+/g, "-")
.replace(/\//g, "_");
return `${headerB64}.${payloadB64}.${sigB64}`;
}
let jtiCounter = 0;
function validPayload(overrides = {}) {
const now = Math.floor(Date.now() / 1000);
return {
iss: "https://token.actions.githubusercontent.com",
aud: "goose-oidc-proxy",
iat: now - 10,
exp: now + 300,
jti: `test-jti-${++jtiCounter}`,
repository: "aaif-goose/goose",
ref: "refs/heads/main",
sub: "repo:aaif-goose/goose:ref:refs/heads/main",
...overrides,
};
}
function mockAll(upstreamStatus = 200, upstreamBody = { ok: true }) {
fetchMock.activate();
fetchMock.disableNetConnect();
const oidc = fetchMock.get("https://token.actions.githubusercontent.com");
oidc
.intercept({ path: "/.well-known/openid-configuration", method: "GET" })
.reply(
200,
JSON.stringify({
jwks_uri:
"https://token.actions.githubusercontent.com/.well-known/jwks",
}),
)
.persist();
oidc
.intercept({ path: "/.well-known/jwks", method: "GET" })
.reply(200, JSON.stringify({ keys: [testJwk] }))
.persist();
const upstream = fetchMock.get("https://api.anthropic.com");
upstream
.intercept({ path: /.*/, method: "POST" })
.reply(upstreamStatus, JSON.stringify(upstreamBody));
}
function mockAllPersistent(upstreamStatus = 200, upstreamBody = { ok: true }) {
fetchMock.activate();
fetchMock.disableNetConnect();
const oidc = fetchMock.get("https://token.actions.githubusercontent.com");
oidc
.intercept({ path: "/.well-known/openid-configuration", method: "GET" })
.reply(
200,
JSON.stringify({
jwks_uri:
"https://token.actions.githubusercontent.com/.well-known/jwks",
}),
)
.persist();
oidc
.intercept({ path: "/.well-known/jwks", method: "GET" })
.reply(200, JSON.stringify({ keys: [testJwk] }))
.persist();
const upstream = fetchMock.get("https://api.anthropic.com");
upstream
.intercept({ path: /.*/, method: "POST" })
.reply(upstreamStatus, JSON.stringify(upstreamBody))
.persist();
}
// Mock TokenBucket Durable Object for unit tests
function mockTokenBucket(overrides = {}) {
const defaults = { allowed: true, remaining: 199 };
const response = { ...defaults, ...overrides };
return {
idFromName: () => "mock-id",
get: () => ({
fetch: async () => Response.json(response),
}),
};
}
function testEnv(overrides = {}) {
return {
OIDC_ISSUER: "https://token.actions.githubusercontent.com",
OIDC_AUDIENCE: "goose-oidc-proxy",
UPSTREAM_URL: "https://api.anthropic.com",
UPSTREAM_AUTH_HEADER: "x-api-key",
UPSTREAM_API_KEY: "sk-ant-real-key",
ALLOWED_REPOS: "aaif-goose/goose",
MAX_TOKEN_AGE_SECONDS: "1200",
MAX_REQUESTS_PER_TOKEN: "200",
RATE_LIMIT_PER_SECOND: "2",
TOKEN_BUCKET: mockTokenBucket(),
...overrides,
};
}
describe("rejects invalid requests", () => {
it("missing auth", async () => {
const request = new Request("https://proxy.example.com/v1/messages");
const ctx = createExecutionContext();
const response = await worker.fetch(request, testEnv(), ctx);
await waitOnExecutionContext(ctx);
expect(response.status).toBe(401);
expect((await response.json()).error).toBe("Missing authentication");
});
it("malformed token", async () => {
const request = new Request("https://proxy.example.com/v1/messages", {
headers: { "x-api-key": "not-a-jwt" },
});
const ctx = createExecutionContext();
const response = await worker.fetch(request, testEnv(), ctx);
await waitOnExecutionContext(ctx);
expect(response.status).toBe(401);
expect((await response.json()).error).toBe("Malformed JWT");
});
it("wrong claims (repo, audience, issuer)", async () => {
for (const [override, expectedError] of [
[{ repository: "evil/repo" }, "not allowed"],
[{ aud: "wrong" }, "Invalid audience"],
[{ iss: "https://evil.example.com" }, "Invalid issuer"],
]) {
const token = await createSignedJwt(validPayload(override));
const request = new Request("https://proxy.example.com/v1/messages", {
headers: { "x-api-key": token },
});
const ctx = createExecutionContext();
const response = await worker.fetch(request, testEnv(), ctx);
await waitOnExecutionContext(ctx);
expect(response.status).toBe(401);
expect((await response.json()).error).toContain(expectedError);
}
});
it("token too old", async () => {
const token = await createSignedJwt(
validPayload({ iat: Math.floor(Date.now() / 1000) - 1500 }),
);
const request = new Request("https://proxy.example.com/v1/messages", {
headers: { "x-api-key": token },
});
const ctx = createExecutionContext();
const response = await worker.fetch(request, testEnv(), ctx);
await waitOnExecutionContext(ctx);
expect(response.status).toBe(401);
expect((await response.json()).error).toBe("Token too old");
});
});
describe("proxies valid requests", () => {
it("forwards to upstream with injected API key", async () => {
const token = await createSignedJwt(validPayload());
mockAll(200, { id: "msg_123", type: "message" });
const request = new Request("https://proxy.example.com/v1/messages", {
method: "POST",
headers: {
"x-api-key": token,
"Content-Type": "application/json",
},
body: JSON.stringify({ model: "claude-sonnet-4-20250514", messages: [] }),
});
const ctx = createExecutionContext();
const response = await worker.fetch(request, testEnv(), ctx);
await waitOnExecutionContext(ctx);
expect(response.status).toBe(200);
expect((await response.json()).id).toBe("msg_123");
});
it("accepts recently-expired token within MAX_TOKEN_AGE_SECONDS", async () => {
const now = Math.floor(Date.now() / 1000);
const token = await createSignedJwt(
validPayload({ iat: now - 600, exp: now - 300 }),
);
mockAll(200, { ok: true });
const request = new Request("https://proxy.example.com/v1/messages", {
method: "POST",
headers: { "x-api-key": token, "Content-Type": "application/json" },
body: JSON.stringify({}),
});
const ctx = createExecutionContext();
const response = await worker.fetch(request, testEnv(), ctx);
await waitOnExecutionContext(ctx);
expect(response.status).toBe(200);
});
});
describe("token budget and rate limiting", () => {
it("rejects when budget exhausted", async () => {
const token = await createSignedJwt(validPayload());
mockAll();
const request = new Request("https://proxy.example.com/v1/messages", {
method: "POST",
headers: { "x-api-key": token, "Content-Type": "application/json" },
body: JSON.stringify({}),
});
const ctx = createExecutionContext();
const response = await worker.fetch(
request,
testEnv({
TOKEN_BUCKET: mockTokenBucket({
allowed: false,
error: "budget_exhausted",
}),
}),
ctx,
);
await waitOnExecutionContext(ctx);
expect(response.status).toBe(429);
expect((await response.json()).error).toBe("Token budget exhausted");
expect(response.headers.get("Retry-After")).toBeNull();
});
it("rejects with Retry-After when rate limited", async () => {
const token = await createSignedJwt(validPayload());
mockAll();
const request = new Request("https://proxy.example.com/v1/messages", {
method: "POST",
headers: { "x-api-key": token, "Content-Type": "application/json" },
body: JSON.stringify({}),
});
const ctx = createExecutionContext();
const response = await worker.fetch(
request,
testEnv({
TOKEN_BUCKET: mockTokenBucket({
allowed: false,
error: "rate_limited",
}),
}),
ctx,
);
await waitOnExecutionContext(ctx);
expect(response.status).toBe(429);
expect((await response.json()).error).toBe("Rate limit exceeded");
expect(response.headers.get("Retry-After")).toBe("1");
});
});