import { afterEach, describe, expect, it, vi } from "vitest" ;
import {
createProviderUsageFetch,
makeResponse,
toRequestUrl,
} from "../test-utils/provider-usage-fetch.js" ;
import { fetchClaudeUsage } from "./provider-usage.fetch.claude.js" ;
const MISSING_SCOPE_MESSAGE = "missing scope requirement user:profile" ;
function makeMissingScopeResponse() {
return makeResponse(403 , {
error: { message: MISSING_SCOPE_MESSAGE },
});
}
function expectMissingScopeError(result: Awaited<ReturnType<typeof fetchClaudeUsage>>) {
expect(result.error).toBe(`HTTP 403 : ${MISSING_SCOPE_MESSAGE}`);
expect(result.windows).toHaveLength(0 );
}
function createScopeFallbackFetch(handler: (url: string) => Promise<Response> | Response) {
return createProviderUsageFetch(async (url) => {
if (url.includes("/api/oauth/usage" )) {
return makeMissingScopeResponse();
}
return handler(url);
});
}
type ScopeFallbackFetch = ReturnType<typeof createScopeFallbackFetch>;
async function expectMissingScopeWithoutFallback(mockFetch: ScopeFallbackFetch) {
// Use explicit non-session values so this stays deterministic even when worker env contains
// real Claude session variables from other suites.
vi.stubEnv("CLAUDE_AI_SESSION_KEY" , "missing-session-key" );
vi.stubEnv("CLAUDE_WEB_SESSION_KEY" , "missing-session-key" );
vi.stubEnv("CLAUDE_WEB_COOKIE" , "foo=bar" );
const result = await fetchClaudeUsage("token" , 5000 , mockFetch);
expectMissingScopeError(result);
const calledUrls = mockFetch.mock.calls.map(([input]) => toRequestUrl(input));
expect(calledUrls.length).toBeGreaterThan(0 );
expect(calledUrls.every((url) => url.includes("/api/oauth/usage" ))).toBe(true );
}
function makeOrgAResponse() {
return makeResponse(200 , [{ uuid: "org-a" }]);
}
describe("fetchClaudeUsage" , () => {
afterEach(() => {
vi.unstubAllEnvs();
});
it("parses oauth usage windows" , async () => {
const fiveHourReset = "2026-01-08T00:00:00Z" ;
const weekReset = "2026-01-12T00:00:00Z" ;
const mockFetch = createProviderUsageFetch(async (_url, init) => {
const headers = (init?.headers as Record<string, string> | undefined) ?? {};
expect(headers.Authorization).toBe("Bearer token" );
expect(headers["anthropic-beta" ]).toBe("oauth-2025-04-20" );
return makeResponse(200 , {
five_hour: { utilization: 18 , resets_at: fiveHourReset },
seven_day: { utilization: 54 , resets_at: weekReset },
seven_day_sonnet: { utilization: 67 },
});
});
const result = await fetchClaudeUsage("token" , 5000 , mockFetch);
expect(result.windows).toEqual([
{ label: "5h" , usedPercent: 18 , resetAt: new Date(fiveHourReset).getTime() },
{ label: "Week" , usedPercent: 54 , resetAt: new Date(weekReset).getTime() },
{ label: "Sonnet" , usedPercent: 67 },
]);
});
it("clamps oauth usage windows and prefers sonnet over opus when both exist" , async () => {
const mockFetch = createProviderUsageFetch(async () =>
makeResponse(200 , {
five_hour: { utilization: -5 },
seven_day: { utilization: 140 },
seven_day_sonnet: { utilization: 40 },
seven_day_opus: { utilization: 90 },
}),
);
const result = await fetchClaudeUsage("token" , 5000 , mockFetch);
expect(result.windows).toEqual([
{ label: "5h" , usedPercent: 0 , resetAt: undefined },
{ label: "Week" , usedPercent: 100 , resetAt: undefined },
{ label: "Sonnet" , usedPercent: 40 },
]);
});
it("returns HTTP errors with provider message suffix" , async () => {
const mockFetch = createProviderUsageFetch(async () =>
makeResponse(403 , {
error: { message: "scope not granted" },
}),
);
const result = await fetchClaudeUsage("token" , 5000 , mockFetch);
expect(result.error).toBe("HTTP 403: scope not granted" );
expect(result.windows).toHaveLength(0 );
});
it("omits blank error message suffixes on oauth failures" , async () => {
const mockFetch = createProviderUsageFetch(async () =>
makeResponse(403 , {
error: { message: " " },
}),
);
const result = await fetchClaudeUsage("token" , 5000 , mockFetch);
expect(result.error).toBe("HTTP 403" );
expect(result.windows).toHaveLength(0 );
});
it("keeps HTTP status errors when oauth error bodies are not JSON" , async () => {
const mockFetch = createProviderUsageFetch(async () => makeResponse(502 , "bad gateway" ));
const result = await fetchClaudeUsage("token" , 5000 , mockFetch);
expect(result.error).toBe("HTTP 502" );
expect(result.windows).toHaveLength(0 );
});
it("falls back to claude web usage when oauth scope is missing" , async () => {
vi.stubEnv("CLAUDE_AI_SESSION_KEY" , "sk-ant-session-key" );
const mockFetch = createProviderUsageFetch(async (url, init) => {
if (url.includes("/api/oauth/usage" )) {
return makeMissingScopeResponse();
}
const headers = (init?.headers as Record<string, string> | undefined) ?? {};
expect(headers.Cookie).toBe("sessionKey=sk-ant-session-key" );
if (url.endsWith("/api/organizations" )) {
return makeResponse(200 , [{ uuid: "org-123" }]);
}
if (url.endsWith("/api/organizations/org-123/usage" )) {
return makeResponse(200 , {
five_hour: { utilization: 12 },
});
}
return makeResponse(404 , "not found" );
});
const result = await fetchClaudeUsage("token" , 5000 , mockFetch);
expect(result.error).toBeUndefined();
expect(result.windows).toEqual([{ label: "5h" , usedPercent: 12 , resetAt: undefined }]);
});
it("parses sessionKey from Cookie-prefixed CLAUDE_WEB_COOKIE headers" , async () => {
vi.stubEnv("CLAUDE_WEB_COOKIE" , "Cookie: foo=bar; sessionKey=sk-ant-cookie-header" );
const mockFetch = createScopeFallbackFetch(async (url) => {
if (url.endsWith("/api/organizations" )) {
return makeResponse(200 , [{ uuid: "org-header" }]);
}
if (url.endsWith("/api/organizations/org-header/usage" )) {
return makeResponse(200 , { five_hour: { utilization: 9 } });
}
return makeResponse(404 , "not found" );
});
const result = await fetchClaudeUsage("token" , 5000 , mockFetch);
expect(result.error).toBeUndefined();
expect(result.windows).toEqual([{ label: "5h" , usedPercent: 9 , resetAt: undefined }]);
expect(mockFetch).toHaveBeenCalledTimes(3 );
});
it("parses sessionKey from CLAUDE_WEB_COOKIE for web fallback" , async () => {
vi.stubEnv("CLAUDE_WEB_COOKIE" , "sessionKey=sk-ant-cookie-session" );
const mockFetch = createScopeFallbackFetch(async (url) => {
if (url.endsWith("/api/organizations" )) {
return makeResponse(200 , [{ uuid: "org-cookie" }]);
}
if (url.endsWith("/api/organizations/org-cookie/usage" )) {
return makeResponse(200 , { seven_day_opus: { utilization: 44 } });
}
return makeResponse(404 , "not found" );
});
const result = await fetchClaudeUsage("token" , 5000 , mockFetch);
expect(result.error).toBeUndefined();
expect(result.windows).toEqual([{ label: "Opus" , usedPercent: 44 }]);
expect(mockFetch).toHaveBeenCalledTimes(3 );
});
it("keeps oauth error when fallback session key is unavailable" , async () => {
const mockFetch = createScopeFallbackFetch(async (url) => {
if (url.endsWith("/api/organizations" )) {
return makeResponse(200 , [{ uuid: "org-missing-session" }]);
}
return makeResponse(404 , "not found" );
});
await expectMissingScopeWithoutFallback(mockFetch);
});
it.each([
{
name: "org list request fails" ,
orgResponse: () => makeResponse(500 , "boom" ),
usageResponse: () => makeResponse(200 , {}),
},
{
name: "org list has no id" ,
orgResponse: () => makeResponse(200 , [{}]),
usageResponse: () => makeResponse(200 , {}),
},
{
name: "usage request fails" ,
orgResponse: makeOrgAResponse,
usageResponse: () => makeResponse(503 , "down" ),
},
{
name: "usage request has no windows" ,
orgResponse: makeOrgAResponse,
usageResponse: () => makeResponse(200 , {}),
},
])(
"returns oauth error when web fallback is unavailable: $name" ,
async ({ orgResponse, usageResponse }) => {
vi.stubEnv("CLAUDE_AI_SESSION_KEY" , "sk-ant-fallback" );
const mockFetch = createScopeFallbackFetch(async (url) => {
if (url.endsWith("/api/organizations" )) {
return orgResponse();
}
if (url.endsWith("/api/organizations/org-a/usage" )) {
return usageResponse();
}
return makeResponse(404 , "not found" );
});
const result = await fetchClaudeUsage("token" , 5000 , mockFetch);
expectMissingScopeError(result);
},
);
});
Messung V0.5 in Prozent C=100 H=96 G=97
¤ Dauer der Verarbeitung: 0.10 Sekunden
(vorverarbeitet am 2026-06-10)
¤
*© Formatika GbR, Deutschland