/** * Tests for banned account detection. * * Verifies: * 1. Non-CF 403 from quota fetch marks account as banned * 2. CF 403 (challenge page) does NOT mark as banned * 3. Banned accounts are skipped by acquire() * 4. Banned accounts auto-recover when quota fetch succeeds */ import { describe, it, expect, vi, beforeEach } from "vitest"; vi.mock("../../config.js", () => ({ getConfig: vi.fn(() => ({ server: {}, model: { default: "gpt-5.2-codex" }, api: { base_url: "https://chatgpt.com/backend-api" }, client: { app_version: "1.0.0" }, auth: { refresh_margin_seconds: 300 }, quota: { refresh_interval_minutes: 5, skip_exhausted: true, warning_thresholds: { primary: [80, 90], secondary: [80, 90] }, }, })), })); vi.mock("../../paths.js", () => ({ getConfigDir: vi.fn(() => "/tmp/test-config"), getDataDir: vi.fn(() => "/tmp/test-data"), })); vi.mock("fs", async (importOriginal) => { const actual = await importOriginal(); return { ...actual, readFileSync: vi.fn(() => "models: []\naliases: {}"), writeFileSync: vi.fn(), writeFile: vi.fn((_p: string, _d: string, _e: string, cb: (err: Error | null) => void) => cb(null)), existsSync: vi.fn(() => false), mkdirSync: vi.fn(), }; }); vi.mock("js-yaml", () => ({ default: { load: vi.fn(() => ({ models: [], aliases: {} })), dump: vi.fn(() => ""), }, })); import type { AccountEntry, AccountStatus } from "../types.js"; import { CodexApiError } from "../../proxy/codex-types.js"; // Inline a minimal AccountPool mock to test ban logic function makeEntry(overrides: Partial = {}): AccountEntry { return { id: overrides.id ?? "test-1", token: "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ0ZXN0IiwiZXhwIjo5OTk5OTk5OTk5fQ.fake", refreshToken: null, email: overrides.email ?? "test@example.com", accountId: "acc-1", userId: "user-1", planType: "free", proxyApiKey: "key-1", status: overrides.status ?? "active", usage: { request_count: 0, input_tokens: 0, output_tokens: 0, empty_response_count: 0, last_used: null, rate_limit_until: null, }, addedAt: new Date().toISOString(), cachedQuota: null, quotaFetchedAt: null, }; } describe("ban detection", () => { it("non-CF 403 is detected as ban error", () => { // Import the isBanError logic inline (it's a private function, test via behavior) const err = new CodexApiError(403, '{"detail": "Your account has been flagged"}'); expect(err.status).toBe(403); // Verify it's NOT a CF error const body = err.body.toLowerCase(); expect(body).not.toContain("cf_chl"); expect(body).not.toContain(" { const err = new CodexApiError(403, 'cf_chl_managed'); const body = err.body.toLowerCase(); expect(body).toContain("cf_chl"); }); it("banned accounts are skipped by acquire (status !== active)", () => { const entry = makeEntry({ status: "banned" }); // acquire() filters: a.status === "active" expect(entry.status).toBe("banned"); expect(entry.status === "active").toBe(false); }); it("AccountStatus type includes banned", () => { const status: AccountStatus = "banned"; expect(status).toBe("banned"); }); });