Skip to content
Merged

Large diffs are not rendered by default.

451 changes: 451 additions & 0 deletions src/__tests__/claude-subscription-store.test.ts

Large diffs are not rendered by default.

292 changes: 292 additions & 0 deletions src/__tests__/claude-subscription.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,292 @@
// src/__tests__/claude-subscription.test.ts
// Curadoria via assinatura Claude — provider PURO (sem DB, sem rede real).
// Porta o mecanismo do Odysseus (Bearer OAuth + oauth-2025-04-20): parse de
// credencial colada (3 formatos + heurística ms/s), headers OAuth (Bearer, NUNCA
// x-api-key), spoof de identidade Claude Code antes do system do usuário, lógica
// de refresh (skew 300s, bare-token nunca), gate de effort por versão de modelo,
// e refresh via token endpoint (fetch fake). Cobre ACs 1-4 do spec.
import { test } from "node:test";
import assert from "node:assert/strict";
import {
CLIENT_ID,
TOKEN_URL,
MESSAGES_URL,
ANTHROPIC_VERSION,
OAUTH_BETA,
DEFAULT_TOKEN_TTL_DAYS,
CLAUDE_CODE_SYSTEM,
CLAUDE_CODE_GUIDANCE,
parsePastedCredential,
oauthHeaders,
buildSubscriptionSystemBlocks,
needsRefresh,
supportsEffort,
refreshSubscription,
} from "../claude-subscription.js";

const DAY_MS = 86_400_000;

// --- Constantes (centralizadas, fixas) --------------------------------------

test("constantes são as fixas do mecanismo Odysseus (nunca de input)", () => {
assert.equal(CLIENT_ID, "9d1c250a-e61b-44d9-88ed-5944d1962f5e");
assert.equal(TOKEN_URL, "https://platform.claude.com/v1/oauth/token");
assert.equal(MESSAGES_URL, "https://api.anthropic.com/v1/messages");
assert.equal(ANTHROPIC_VERSION, "2023-06-01");
assert.equal(OAUTH_BETA, "oauth-2025-04-20");
assert.equal(DEFAULT_TOKEN_TTL_DAYS, 365);
assert.equal(CLAUDE_CODE_SYSTEM, "You are Claude Code, Anthropic's official CLI for Claude.");
assert.ok(CLAUDE_CODE_GUIDANCE.length > 0);
});

// --- AC1: parsePastedCredential (3 formatos + heurística + bare TTL) --------

test("parse (a) bare token sk-ant-oat01 → refresh null, expiry = now + 365d", () => {
const now = new Date("2026-06-19T00:00:00.000Z");
const out = parsePastedCredential("sk-ant-oat01-abc123", now);
assert.equal(out.access_token, "sk-ant-oat01-abc123");
assert.equal(out.refresh_token, null);
assert.equal(
out.expires_at.getTime(),
now.getTime() + DEFAULT_TOKEN_TTL_DAYS * DAY_MS,
);
});

test("parse bare token tolera espaço em volta", () => {
const now = new Date("2026-06-19T00:00:00.000Z");
const out = parsePastedCredential(" sk-ant-oat01-xyz ", now);
assert.equal(out.access_token, "sk-ant-oat01-xyz");
assert.equal(out.refresh_token, null);
});

test("parse (b) JSON claudeAiOauth {accessToken,refreshToken,expiresAt}", () => {
const expMs = Date.UTC(2026, 8, 1); // epoch ms (> 1e12)
const text = JSON.stringify({
claudeAiOauth: {
accessToken: "sk-ant-oat01-J",
refreshToken: "rt-J",
expiresAt: expMs,
},
});
const out = parsePastedCredential(text);
assert.equal(out.access_token, "sk-ant-oat01-J");
assert.equal(out.refresh_token, "rt-J");
assert.equal(out.expires_at.getTime(), expMs);
});

test("parse (c) flat {access_token, refresh_token, expires_at}", () => {
const expMs = Date.UTC(2026, 8, 1);
const text = JSON.stringify({
access_token: "sk-ant-oat01-F",
refresh_token: "rt-F",
expires_at: expMs,
});
const out = parsePastedCredential(text);
assert.equal(out.access_token, "sk-ant-oat01-F");
assert.equal(out.refresh_token, "rt-F");
assert.equal(out.expires_at.getTime(), expMs);
});

test("parse flat aceita expiresAt camelCase também", () => {
const expMs = Date.UTC(2027, 0, 1);
const out = parsePastedCredential(
JSON.stringify({ access_token: "sk-ant-oat01-C", expiresAt: expMs }),
);
assert.equal(out.access_token, "sk-ant-oat01-C");
assert.equal(out.refresh_token, null);
assert.equal(out.expires_at.getTime(), expMs);
});

test("AC4: heurística ms-vs-s — segundos (<= 1e12) viram ms via *1000", () => {
const expSecs = Math.floor(Date.UTC(2026, 8, 1) / 1000); // epoch SECONDS
const out = parsePastedCredential(
JSON.stringify({ access_token: "sk-ant-oat01-S", expiresAt: expSecs }),
);
// raw <= 1e12 → tratado como segundos → *1000
assert.equal(out.expires_at.getTime(), expSecs * 1000);
});

test("AC4: heurística ms-vs-s — ms (> 1e12) ficam como estão", () => {
const expMs = Date.UTC(2026, 8, 1); // > 1e12
const out = parsePastedCredential(
JSON.stringify({ access_token: "sk-ant-oat01-M", expiresAt: expMs }),
);
assert.equal(out.expires_at.getTime(), expMs);
});

test("parse JSON sem expiry aplica TTL default (como bare token)", () => {
const now = new Date("2026-06-19T00:00:00.000Z");
const out = parsePastedCredential(
JSON.stringify({ access_token: "sk-ant-oat01-N", refresh_token: "rt-N" }),
now,
);
assert.equal(out.expires_at.getTime(), now.getTime() + DEFAULT_TOKEN_TTL_DAYS * DAY_MS);
});

test("parse lixo (string vazia) → erro claro", () => {
assert.throws(() => parsePastedCredential(" "), /credencial|token|vazi/i);
});

test("parse JSON inválido → erro claro", () => {
assert.throws(() => parsePastedCredential("{not json"), /credencial|JSON|token/i);
});

test("parse JSON sem access token → erro claro", () => {
assert.throws(
() => parsePastedCredential(JSON.stringify({ refreshToken: "rt-only" })),
/access|token/i,
);
});

// --- AC2: oauthHeaders (Bearer + oauth-beta, NUNCA x-api-key) ---------------

test("AC2: oauthHeaders produz Bearer + anthropic-beta + version, sem x-api-key", () => {
const h = oauthHeaders("sk-ant-oat01-TOK");
assert.equal(h["Authorization"], "Bearer sk-ant-oat01-TOK");
assert.equal(h["anthropic-beta"], OAUTH_BETA);
assert.equal(h["anthropic-version"], ANTHROPIC_VERSION);
// NUNCA x-api-key (case-insensitive check)
const keys = Object.keys(h).map((k) => k.toLowerCase());
assert.ok(!keys.includes("x-api-key"), "headers nunca devem ter x-api-key");
});

// --- AC3: buildSubscriptionSystemBlocks (spoof primeiro, user depois) -------

test("AC3: spoof — identidade primeiro, guidance, depois system do usuário", () => {
const blocks = buildSubscriptionSystemBlocks("Você é o curador de memória.");
assert.equal(blocks.length, 3);
assert.equal(blocks[0].type, "text");
assert.equal(blocks[0].text, CLAUDE_CODE_SYSTEM);
assert.equal(blocks[1].text, CLAUDE_CODE_GUIDANCE);
assert.equal(blocks[2].text, "Você é o curador de memória.");
});

test("AC3: sem system do usuário → apenas os 2 blocos de spoof", () => {
const blocks = buildSubscriptionSystemBlocks();
assert.equal(blocks.length, 2);
assert.equal(blocks[0].text, CLAUDE_CODE_SYSTEM);
assert.equal(blocks[1].text, CLAUDE_CODE_GUIDANCE);
});

test("AC3: system do usuário vazio não vira bloco", () => {
const blocks = buildSubscriptionSystemBlocks("");
assert.equal(blocks.length, 2);
});

// --- AC4: needsRefresh (skew 300s, bare-token nunca) ------------------------

test("AC4: needsRefresh true quando expira dentro de 300s e há refresh_token", () => {
const now = new Date("2026-06-19T00:00:00.000Z");
const soon = new Date(now.getTime() + 200_000); // 200s < 300s skew
assert.equal(needsRefresh(soon, true, now), true);
});

test("AC4: needsRefresh false quando expira além de 300s", () => {
const now = new Date("2026-06-19T00:00:00.000Z");
const later = new Date(now.getTime() + 3600_000); // 1h
assert.equal(needsRefresh(later, true, now), false);
});

test("AC4: needsRefresh respeita exatamente o skew de 300s (<=)", () => {
const now = new Date("2026-06-19T00:00:00.000Z");
const exactly = new Date(now.getTime() + 300_000);
assert.equal(needsRefresh(exactly, true, now), true); // <= now+300s
const oneMore = new Date(now.getTime() + 300_001);
assert.equal(needsRefresh(oneMore, true, now), false);
});

test("AC4: bare-token (sem refresh_token) NUNCA precisa refresh, mesmo expirado", () => {
const now = new Date("2026-06-19T00:00:00.000Z");
const past = new Date(now.getTime() - 10_000);
assert.equal(needsRefresh(past, false, now), false);
});

test("AC4: needsRefresh com expiresAt null e sem refresh_token → false (não tenta)", () => {
// Sem expiry conhecido e sem refresh, não há o que refrescar.
assert.equal(needsRefresh(null, false), false);
});

// --- supportsEffort (Opus >=4.5, Sonnet >=4.6, Fable/Mythos) ----------------

test("supportsEffort: Opus 4.5+ sim, Opus 4.4 não", () => {
assert.equal(supportsEffort("claude-opus-4-8"), true);
assert.equal(supportsEffort("claude-opus-4-5"), true);
assert.equal(supportsEffort("claude-opus-4-4"), false);
assert.equal(supportsEffort("claude-opus-4-20250514"), false); // 4.0 datado
});

test("supportsEffort: Sonnet 4.6+ sim, 4.5 não", () => {
assert.equal(supportsEffort("claude-sonnet-4-6"), true);
assert.equal(supportsEffort("claude-sonnet-4-5"), false);
});

test("supportsEffort: Fable/Mythos por nome", () => {
assert.equal(supportsEffort("claude-fable-1"), true);
assert.equal(supportsEffort("some-mythos-model"), true);
});

test("supportsEffort: Haiku / desconhecido → false; input vazio → false", () => {
assert.equal(supportsEffort("claude-haiku-4-5"), false);
assert.equal(supportsEffort("gpt-4o"), false);
assert.equal(supportsEffort(""), false);
});

test("supportsEffort: 'octopus-4-8' não é Opus (boundary)", () => {
assert.equal(supportsEffort("octopus-4-8"), false);
});

// --- AC4: refreshSubscription (payload + expires_in delta + preserva rt) ----

test("AC4: refreshSubscription monta grant_type=refresh_token + client_id e usa expires_in", async () => {
const now = new Date("2026-06-19T00:00:00.000Z");
let capturedUrl = "";
let capturedBody: any = null;
const fetchImpl = (async (url: string, init: any) => {
capturedUrl = url;
capturedBody = JSON.parse(init.body);
return {
ok: true,
status: 200,
json: async () => ({
access_token: "sk-ant-oat01-NEW",
refresh_token: "rt-NEW",
expires_in: 3600,
}),
};
}) as unknown as typeof fetch;

const out = await refreshSubscription("rt-OLD", fetchImpl, now);
assert.equal(capturedUrl, TOKEN_URL);
assert.equal(capturedBody.grant_type, "refresh_token");
assert.equal(capturedBody.refresh_token, "rt-OLD");
assert.equal(capturedBody.client_id, CLIENT_ID);

assert.equal(out.access_token, "sk-ant-oat01-NEW");
assert.equal(out.refresh_token, "rt-NEW");
// novo expiry = now + expires_in (delta em segundos)
assert.equal(out.expires_at.getTime(), now.getTime() + 3600 * 1000);
});

test("AC4: refreshSubscription preserva refresh_token antigo quando a resposta não traz um", async () => {
const now = new Date("2026-06-19T00:00:00.000Z");
const fetchImpl = (async () => ({
ok: true,
status: 200,
json: async () => ({ access_token: "sk-ant-oat01-NEW2", expires_in: 1800 }),
})) as unknown as typeof fetch;

const out = await refreshSubscription("rt-PRESERVE", fetchImpl, now);
assert.equal(out.access_token, "sk-ant-oat01-NEW2");
// sem refresh_token na resposta → null (caller preserva o antigo)
assert.equal(out.refresh_token, null);
assert.equal(out.expires_at.getTime(), now.getTime() + 1800 * 1000);
});

test("refreshSubscription propaga falha HTTP (>=400) como erro", async () => {
const fetchImpl = (async () => ({
ok: false,
status: 401,
json: async () => ({ error: "invalid_grant" }),
})) as unknown as typeof fetch;
await assert.rejects(() => refreshSubscription("rt-bad", fetchImpl), /401|refresh|reconect/i);
});
Loading
Loading