import { readFileSync } from "node:fs"; import { beforeEach, describe, expect, test } from "bun:test"; import { EsiClient, EsiError, EsiHttpError, EsiMarketEstimates, EsiSchemaError, ESI_ASSET_MAX_PAGES, EsiRateLimitError, EsiStabilityError, ESI_ASSET_MAX_RESPONSE_BYTES, ESI_ASSET_MAX_ROWS_PER_PAGE, ESI_ASSET_SNAPSHOT_MAX_ROWS, ESI_CACHE_MAX_BYTES, ESI_ASSET_NAME_MAX_RESPONSE_BYTES, ESI_ASSETS_SCOPE, ESI_COMPATIBILITY_DATE, ESI_STRUCTURES_SCOPE, ESI_READ_OPERATIONS, ESI_WALLET_MAX_RESPONSE_BYTES, ESI_WALLET_SCOPE, } from "../src/lib/server/esi"; const asset = (itemId = "9007199254740993", quantity = 2) => ({ item_id: Number(itemId) > Number.MAX_SAFE_INTEGER ? itemId : Number(itemId), type_id: 34, quantity, location_id: 60003760, location_type: "station", location_flag: "Hangar", is_singleton: false, }); function response(body: unknown, headers: Record = {}, status = 200): Response { return new Response(typeof body === "string" ? body : JSON.stringify(body), { status, headers }); } function authority(userId: string, characterId: number, scopes: readonly string[] = [ESI_ASSETS_SCOPE], grantRevision = 1, authorizationEpoch = 1, credentialVersion = 1) { return { userId, principal: `${userId}:principal`, characterId, scopes, grantRevision, authorizationEpoch, credentialVersion } as const; } function queueFetch(responses: Response[]) { const calls: { url: string; init: RequestInit }[] = []; const fetch = async (input: RequestInfo | URL, init?: RequestInit) => { calls.push({ url: String(input), init: init ?? {} }); const next = responses.shift(); if (!next) throw new Error(`unexpected request ${String(input)}`); return next; }; return { fetch: fetch as typeof globalThis.fetch, calls }; } describe("central ESI client", () => { beforeEach(() => { expect(ESI_COMPATIBILITY_DATE).toBe("2026-08-18"); }); test("sends pinned and auth headers, fetches pages, and revalidates page one", async () => { const second = [asset("2", 4)]; const queued = queueFetch([ response('[{"item_id":9007199254740993,"type_id":34,"quantity":2,"location_id":60003760,"location_type":"station","location_flag":"Hangar","is_singleton":false}]', { ETag: '"one"', "X-Pages": "2", "Cache-Control": "max-age=0" }), response(second, { ETag: '"two"', "X-Pages": "2", "Cache-Control": "max-age=0" }), new Response(null, { status: 304, headers: { ETag: '"one"', "X-Pages": "2", "Cache-Control": "max-age=60" } }), ]); const client = new EsiClient({ userAgent: "eden-test", fetch: queued.fetch, now: () => 1_000 }); const result = await client.getAssets(42, "secret-token", authority("user-a", 42)); expect(result.assets.map((item) => item.itemId)).toEqual(["9007199254740993", "2"]); expect(result.assets[0]?.characterId).toBe(42); expect(queued.calls).toHaveLength(3); expect(queued.calls[0]?.url).toBe("https://esi.evetech.net/characters/42/assets?page=1"); const requestHeaders = new Headers(queued.calls[0]?.init.headers); expect(requestHeaders.get("authorization")).toBe("Bearer secret-token"); expect(requestHeaders.get("user-agent")).toBe("eden-test"); expect(requestHeaders.get("x-compatibility-date")).toBe("2026-08-18"); expect(new Headers(queued.calls[2]?.init.headers).get("if-none-match")).toBe('"one"'); }); test("keeps auth contexts private and reuses a 304 body", async () => { let now = 0; const queued = queueFetch([ response([asset("1")], { ETag: '"a"', "X-Pages": "1", "Cache-Control": "max-age=1" }), new Response(null, { status: 304, headers: { ETag: '"a"', "X-Pages": "1", "Cache-Control": "max-age=1" } }), new Response(null, { status: 304, headers: { ETag: '"a"', "X-Pages": "1", "Cache-Control": "max-age=1" } }), new Response(null, { status: 304, headers: { ETag: '"a"', "X-Pages": "1", "Cache-Control": "max-age=1" } }), response([asset("1")], { ETag: '"b"', "X-Pages": "1", "Cache-Control": "max-age=1" }), new Response(null, { status: 304, headers: { ETag: '"b"', "X-Pages": "1", "Cache-Control": "max-age=1" } }), ]); const client = new EsiClient({ userAgent: "test", fetch: queued.fetch, now: () => now }); await client.getAssets(7, "a", authority("one", 7)); now = 2_000; await client.getAssets(7, "a", authority("one", 7)); await client.getAssets(7, "b", authority("two", 7)); expect(queued.calls).toHaveLength(6); expect(new Headers(queued.calls[1]?.init.headers).get("if-none-match")).toBe('"a"'); expect(new Headers(queued.calls[2]?.init.headers).get("if-none-match")).toBe('"a"'); expect(new Headers(queued.calls[4]?.init.headers).get("if-none-match")).toBeNull(); expect(JSON.stringify(queued.calls)).not.toContain("secret"); }); test("reads private structures through the fixed lossless endpoint and isolates its scope", async () => { const queued = queueFetch([response('{"structure_id":9007199254740993,"name":"Private Keep","solar_system_id":30000142,"owner_id":99000001,"type_id":35832,"position":{"x":1,"y":2,"z":3}}', { "Cache-Control": "max-age=60" })]); const client = new EsiClient({ userAgent: "test", fetch: queued.fetch, now: () => 1_000 }); const result = await client.getStructure(42, "9007199254740993", "secret-token", authority("u", 42, [ESI_STRUCTURES_SCOPE])); expect(result.structureId).toBe("9007199254740993"); expect(result.solarSystemId).toBe("30000142"); expect(queued.calls[0]?.url).toBe("https://esi.evetech.net/universe/structures/9007199254740993"); expect(new Headers(queued.calls[0]?.init.headers).get("authorization")).toBe("Bearer secret-token"); await expect(client.getStructure(42, "9007199254740994", "secret-token", authority("u", 42, [ESI_ASSETS_SCOPE]))).rejects.toThrow("missing required scope"); expect(ESI_READ_OPERATIONS.characterStructure.requiredScope).toBe(ESI_STRUCTURES_SCOPE); }); test("retries transient responses with Retry-After but does not retry 401/403", async () => { const delays: number[] = []; const retryable = queueFetch([ response({ error: "rate" }, { "Retry-After": "2", "X-Ratelimit-Group": "char-asset", "X-Ratelimit-Remaining": "0" }, 429), response([asset("1")], { "X-Pages": "1", "Cache-Control": "max-age=60", ETag: '"x"' }), new Response(null, { status: 304, headers: { "X-Pages": "1", ETag: '"x"', "Cache-Control": "max-age=60" } }), ]); const client = new EsiClient({ userAgent: "test", fetch: retryable.fetch, sleep: async (milliseconds) => { delays.push(milliseconds); }, random: () => 0, now: () => 0, }); await client.getAssets(8, "token", authority("u", 8)); expect(delays[0]).toBe(2_000); const denied = queueFetch([response({}, {}, 401)]); const deniedClient = new EsiClient({ userAgent: "test", fetch: denied.fetch, now: () => 0 }); await expect(deniedClient.getAssets(8, "token", authority("u", 8))).rejects.toBeInstanceOf(EsiHttpError); expect(denied.calls).toHaveLength(1); }); test("does not publish changing or incomplete snapshots", async () => { const changing = queueFetch([ response([asset("1")], { "X-Pages": "2", ETag: '"one"' }), response([asset("2")], { "X-Pages": "2", ETag: '"two"' }), response([asset("3")], { "X-Pages": "2", ETag: '"three"' }), ]); const client = new EsiClient({ userAgent: "test", fetch: changing.fetch, now: () => 0 }); await expect(client.getAssets(1, "token", authority("u", 1))).rejects.toBeInstanceOf(EsiStabilityError); expect(changing.calls).toHaveLength(3); const invalid = queueFetch([response([{ item_id: 1 }], { "X-Pages": "1" })]); const invalidClient = new EsiClient({ userAgent: "test", fetch: invalid.fetch, now: () => 0 }); await expect(invalidClient.getAssets(1, "token", authority("u", 1))).rejects.toThrow("invalid response"); }); }); describe("typed ESI helper reads", () => { test("canonicalizes bounded POST IDs, preserves int64 literals, and isolates authority revisions", async () => { let now = 0; const queued = queueFetch([ response(readFileSync("tests/fixtures/esi/asset-names-int64.json", "utf8"), { ETag: '"names"', "Cache-Control": "max-age=0" }), new Response(null, { status: 304, headers: { ETag: '"names"', "Cache-Control": "max-age=60" } }), response('[{"item_id":9007199254740993,"name":"Fresh"}]', { ETag: '"fresh"', "Cache-Control": "max-age=60" }), ]); const client = new EsiClient({ userAgent: "test", fetch: queued.fetch, now: () => now }); const authority = { userId: "user", principal: "user:principal", characterId: 42, scopes: [ESI_ASSETS_SCOPE, "b", "a"], grantRevision: 1, authorizationEpoch: 1, credentialVersion: 1 }; const first = await client.getAssetNames(42, ["9007199254740993", "2"], "token", authority); expect(first).toMatchObject({ requestedIds: ["2", "9007199254740993"], missing: ["2"], complete: false }); expect(queued.calls[0]?.url).toBe("https://esi.evetech.net/characters/42/assets/names"); expect(queued.calls[0]?.init.method).toBe("POST"); expect(queued.calls[0]?.init.body).toBe("[2,9007199254740993]"); now = 2_000; const reused = await client.getAssetNames(42, ["2", "9007199254740993"], "token", authority); expect(reused.names).toEqual([{ itemId: "9007199254740993", name: "Freighter" }]); expect(new Headers(queued.calls[1]?.init.headers).get("if-none-match")).toBe('"names"'); await client.getAssetNames(42, ["2", "9007199254740993"], "token", { ...authority, grantRevision: 2 }); expect(queued.calls).toHaveLength(3); }); test("validates helper schemas, bounds, and preserves explicit missing locations", async () => { const locations = queueFetch([ response(readFileSync("tests/fixtures/esi/asset-locations-finite.json", "utf8"), { "Cache-Control": "max-age=60" }), ]); const client = new EsiClient({ userAgent: "test", fetch: locations.fetch, now: () => 10 }); const result = await client.getAssetLocations(7, ["9007199254740993", "8"], "token", authority("u", 7)); expect(result).toMatchObject({ missing: ["8"], complete: false, locations: [{ itemId: "9007199254740993", position: { x: 1, y: -2, z: 0 } }] }); expect(locations.calls[0]?.url).toBe("https://esi.evetech.net/characters/7/assets/locations"); expect(locations.calls[0]?.init.method).toBe("POST"); const invalid = queueFetch([response('[{"item_id":8,"position":{"x":1e10000,"y":0,"z":0}}]')]); const invalidClient = new EsiClient({ userAgent: "test", fetch: invalid.fetch, now: () => 0 }); await expect(invalidClient.getAssetLocations(7, ["8"], "token", authority("u", 7))).rejects.toBeInstanceOf(EsiSchemaError); await expect(invalidClient.getAssetLocations(7, [], "token", authority("u", 7))).rejects.toMatchObject({ code: "invalid_asset_ids" }); await expect(invalidClient.getAssetLocations(7, ["1", "1"], "token", authority("u", 7))).rejects.toMatchObject({ code: "invalid_asset_ids" }); await expect(invalidClient.getAssetLocations(7, ["9223372036854775808"], "token", authority("u", 7))).rejects.toMatchObject({ code: "invalid_asset_ids" }); }); test("retries safe POST reads, honors no-store and exposes wallet exact cents", async () => { const delays: number[] = []; let now = 0; const queued = queueFetch([ response({}, {}, 503), response('[{"item_id":1,"name":"Box"}]', { ETag: '"box"', "Cache-Control": "no-store" }), response('[{"item_id":1,"name":"Box"}]', { ETag: '"box2"', "Cache-Control": "no-store" }), response(readFileSync("tests/fixtures/esi/wallet-balance-signed.json", "utf8"), { ETag: '"wallet"', "Cache-Control": "max-age=1" }), new Response(null, { status: 304, headers: { ETag: '"wallet"', "Cache-Control": "max-age=60" } }), ]); const client = new EsiClient({ userAgent: "test", fetch: queued.fetch, now: () => now, sleep: async (ms) => { delays.push(ms); }, random: () => 0 }); await client.getAssetNames(7, ["1"], "token", authority("u", 7)); await client.getAssetNames(7, ["1"], "token", authority("u", 7)); expect(delays).toContain(250); expect(queued.calls[0]?.init.method).toBe("POST"); expect(new Headers(queued.calls[2]?.init.headers).get("if-none-match")).toBeNull(); const wallet = await client.getWalletBalance(7, "token", authority("u", 7, [ESI_WALLET_SCOPE])); expect(wallet.amount).toBe("-900719925474099301"); expect(queued.calls[3]?.url).toBe("https://esi.evetech.net/characters/7/wallet"); expect(queued.calls[3]?.init.method).toBe("GET"); now = 500; await client.getWalletBalance(7, "token", authority("u", 7, [ESI_WALLET_SCOPE])); expect(queued.calls).toHaveLength(4); now = 2_000; const refreshed = await client.getWalletBalance(7, "token", authority("u", 7, [ESI_WALLET_SCOPE])); expect(refreshed.amount).toBe(wallet.amount); expect(new Headers(queued.calls[4]?.init.headers).get("if-none-match")).toBe('"wallet"'); }); test("uses the asset endpoint page cap and retains singleton source facts", async () => { const queued = queueFetch([response("[]", { "X-Pages": String(ESI_ASSET_MAX_PAGES + 1) })]); const client = new EsiClient({ userAgent: "test", fetch: queued.fetch, now: () => 0 }); await expect(client.getAssets(7, "token", authority("u", 7))).rejects.toMatchObject({ code: "pagination_error" }); const singletonQueue = queueFetch([ response('[{"item_id":1,"type_id":34,"quantity":10,"location_id":60003760,"location_type":"station","location_flag":"Hangar","is_singleton":true}]', { "X-Pages": "1" }), new Response(null, { status: 304, headers: { "X-Pages": "1" } }), ]); const singletonClient = new EsiClient({ userAgent: "test", fetch: singletonQueue.fetch, now: () => 0 }); const snapshot = await singletonClient.getAssets(7, "token", authority("u", 7)); expect(snapshot.assets[0]?.isSingleton).toBe(true); }); }); describe("T06 transport bounds, authority, and metadata", () => { test("requires complete scoped authority and asserts pinned route metadata", async () => { const metadata = JSON.parse(readFileSync("tests/fixtures/esi/transport-metadata.json", "utf8")) as any; expect(metadata.compatibilityDate).toBe(ESI_COMPATIBILITY_DATE); expect(metadata.evidence.assetHelpers).toEqual({ source: "docs/research/eve-openapi-evidence.json", verification: "pinned-authoritative" }); expect(metadata.evidence.walletBalance).toMatchObject({ source: "https://esi.evetech.net/meta/openapi.json?compatibility_date=2026-08-18", verification: "current-retrieval-with-pinned-compatibility-date-not-historical-capture", retrievedAt: "2026-09-19T19:30:12Z", responseDate: "2026-08-18", openapiVersion: "2026-08-18", sha256: "9a9a0f4af77216300e960cfd2576c9baac085a8170022e40c928c28622d80fc6", operationId: "GetCharactersCharacterIdWallet", route: "/characters/{character_id}/wallet", method: "GET", requiredScope: ESI_WALLET_SCOPE, pagination: "none", response: { status: 200, mediaType: "application/json", shape: "scalar number/double" }, cache: { ttlSeconds: 120, policy: "ttl-based", validators: ["Cache-Control", "ETag", "Last-Modified"] }, rateLimit: { group: "char-wallet", tokens: 150, window: "15m" }, officialDocs: { versioning: "https://developers.eveonline.com/docs/services/esi/overview/", caching: "https://developers.eveonline.com/docs/services/esi/overview/#caching", rateLimiting: "https://developers.eveonline.com/docs/services/esi/rate-limiting/", }, }); for (const [operation, expected] of Object.entries(metadata.operations) as [keyof typeof ESI_READ_OPERATIONS, any][]) { const descriptor = ESI_READ_OPERATIONS[operation]; expect({ route: `/${descriptor.path}`, method: descriptor.method, requiredScope: descriptor.requiredScope }).toEqual(expected); } const client = new EsiClient({ userAgent: "test", fetch: queueFetch([response("[{\"item_id\":1,\"name\":\"x\"}]")]).fetch, now: () => 0 }); await expect(client.getAssetNames(7, ["1"], "token", "legacy" as never)).rejects.toMatchObject({ code: "authorization_context" }); await expect(client.getAssetNames(7, ["1"], "token", { ...authority("u", 7), grantRevision: undefined } as never)).rejects.toMatchObject({ code: "authorization_context" }); await expect(client.getAssetNames(7, ["1"], "token", authority("u", 7, ["other.scope"]))).rejects.toMatchObject({ code: "authorization_context" }); await expect(client.getWalletBalance(7, "token", authority("u", 7))).rejects.toMatchObject({ code: "authorization_context" }); }); test("isolates every private cache authority dimension and uses correct request metadata", async () => { const queued = queueFetch(Array.from({ length: 8 }, (_, index) => response(`[{\"item_id\":1,\"name\":\"${index}\"}]`, { "Cache-Control": "max-age=60" }))); const client = new EsiClient({ userAgent: "test", fetch: queued.fetch, now: () => 0 }); const contexts = [ authority("u", 7), { ...authority("u", 7), principal: "u:other-principal" }, { ...authority("u", 7), credentialVersion: 2 }, authority("other", 7), authority("u", 8), authority("u", 7, [ESI_ASSETS_SCOPE, "extra"]), authority("u", 7, [ESI_ASSETS_SCOPE], 2, 1), authority("u", 7, [ESI_ASSETS_SCOPE], 1, 2), ]; for (const context of contexts) await client.getAssetNames(context.characterId, ["1"], "token", context as any); expect(queued.calls).toHaveLength(8); expect(queued.calls.every((call) => call.init.method === "POST")).toBe(true); expect(queued.calls.every((call) => new Headers(call.init.headers).get("content-type") === "application/json")).toBe(true); expect(new Headers(queued.calls[0]?.init.headers).get("authorization")).toBe("Bearer token"); expect(new Headers(queued.calls[1]?.init.headers).get("if-none-match")).toBeNull(); }); test("counts received rows before duplicate handling", async () => { const duplicatePage = JSON.stringify(Array.from({ length: 5_000 }, () => asset("1"))); const responses = Array.from({ length: 11 }, () => response(duplicatePage, { "X-Pages": "11", "Cache-Control": "max-age=60" })); const queued = queueFetch(responses); const client = new EsiClient({ userAgent: "test", fetch: queued.fetch, now: () => 0 }); await expect(client.getAssets(7, "token", authority("duplicate-heavy", 7))).rejects.toMatchObject({ code: "response_too_large" }); expect(queued.calls).toHaveLength(11); }); test("shares one deadline across all asset pages and final revalidation", async () => { let now = 0; const calls: (RequestInfo | URL)[] = []; const client = new EsiClient({ userAgent: "test", now: () => now, fetch: (async (input) => { calls.push(input); now = 16_000; return response([asset("1")], { "X-Pages": "2" }); }) as typeof globalThis.fetch, }); await expect(client.getAssets(7, "token", authority("u", 7))).rejects.toMatchObject({ code: "deadline" }); expect(calls).toHaveLength(1); }); test("rejects oversized content/streams and oversized asset pages before caching", async () => { const contentLengthQueue = queueFetch([response("[]", { "Content-Length": String(ESI_ASSET_NAME_MAX_RESPONSE_BYTES + 1) })]); const contentLengthClient = new EsiClient({ userAgent: "test", fetch: contentLengthQueue.fetch, now: () => 0 }); await expect(contentLengthClient.getAssetNames(7, ["1"], "token", authority("u", 7))).rejects.toMatchObject({ code: "response_too_large" }); const stream = new ReadableStream({ start(controller) { controller.enqueue(new TextEncoder().encode("x".repeat(ESI_WALLET_MAX_RESPONSE_BYTES + 1))); controller.close(); }, }); const streamQueue = queueFetch([new Response(stream)]); const streamClient = new EsiClient({ userAgent: "test", fetch: streamQueue.fetch, now: () => 0 }); await expect(streamClient.getWalletBalance(7, "token", authority("u", 7, [ESI_WALLET_SCOPE]))).rejects.toMatchObject({ code: "response_too_large" }); const oversizedPage = JSON.stringify(Array.from({ length: ESI_ASSET_MAX_ROWS_PER_PAGE + 1 }, () => ({}))); const pageQueue = queueFetch([response(oversizedPage, { "X-Pages": "1" })]); const pageClient = new EsiClient({ userAgent: "test", fetch: pageQueue.fetch, now: () => 0 }); await expect(pageClient.getAssets(7, "token", authority("u", 7))).rejects.toMatchObject({ code: "response_too_large" }); expect(pageClient.getCacheStats().cacheEntries).toBe(0); }); test("rejects malformed helper arrays, duplicate/unexpected IDs, malformed wallet, and negative zero", async () => { const queued = queueFetch([ response("[{\"item_id\":1,\"name\":\"a\"},{\"item_id\":1,\"name\":\"b\"}]"), response("[{\"item_id\":2,\"name\":\"a\"}]"), response("{}"), response("not-a-number"), ]); const client = new EsiClient({ userAgent: "test", fetch: queued.fetch, now: () => 0 }); await expect(client.getAssetNames(7, ["1"], "token", authority("u", 7))).rejects.toBeInstanceOf(EsiSchemaError); await expect(client.getAssetNames(7, ["1"], "token", authority("u", 7))).rejects.toBeInstanceOf(EsiSchemaError); await expect(client.getAssetLocations(7, ["1"], "token", authority("u", 7))).rejects.toBeInstanceOf(EsiSchemaError); await expect(client.getWalletBalance(7, "token", authority("u", 7, [ESI_WALLET_SCOPE]))).rejects.toBeInstanceOf(EsiSchemaError); await expect(client.getAssetNames(7, ["-0"], "token", authority("u", 7))).rejects.toMatchObject({ code: "invalid_asset_ids" }); }); test("records cooldowns on successful responses, exposes typed queries, and bounds maps", async () => { let now = 0; const cooldownAuth = authority("u", 7, [ESI_WALLET_SCOPE]); const cooldownQueue = queueFetch([ response("1", { "Cache-Control": "max-age=0", "X-Ratelimit-Remaining": "0", "X-Ratelimit-Group": "wallet", "Retry-After": "10" }), response("1", { "Cache-Control": "max-age=60" }), ]); const sleeps: number[] = []; const client = new EsiClient({ userAgent: "test", fetch: cooldownQueue.fetch, now: () => now, sleep: async (ms) => { sleeps.push(ms); } }); await client.getWalletBalance(7, "token", cooldownAuth); expect(client.getCooldown(cooldownAuth, 7)).toMatchObject({ delayMs: 10_000, group: "wallet", kind: "new" }); now = 1_000; await client.getWalletBalance(7, "token", cooldownAuth); expect(sleeps).toContain(9_000); const thrownQueue = queueFetch([response("rate", { "Retry-After": "20", "X-Ratelimit-Remaining": "0" }, 429)]); const thrown = new EsiClient({ userAgent: "test", fetch: thrownQueue.fetch, now: () => 0, sleep: async () => {} }); await expect(thrown.getWalletBalance(7, "token", cooldownAuth)).rejects.toMatchObject({ code: "rate_limited", cooldown: { delayMs: 20_000 } }); const boundedQueue = queueFetch(Array.from({ length: 520 }, (_, index) => response(`[{\"item_id\":${index + 1},\"name\":\"x\"}]`, { "Cache-Control": "max-age=60" }))); const bounded = new EsiClient({ userAgent: "test", fetch: boundedQueue.fetch, now: () => 0 }); for (let index = 0; index < 520; index += 1) await bounded.getAssetNames(7, [String(index + 1)], "token", authority("u", 7)); expect(bounded.getCacheStats().cacheEntries).toBeLessThanOrEqual(512); expect(bounded.getCacheStats(7 * 60 * 60 * 1000).cacheEntries).toBe(0); const weightedBody = JSON.stringify(Array.from({ length: 1_000 }, (_, index) => ({ item_id: index + 1, name: "x".repeat(512) }))); const weightedIds = Array.from({ length: 1_000 }, (_, index) => String(index + 1)); const weightedQueue = queueFetch(Array.from({ length: 80 }, () => response(weightedBody, { "Cache-Control": "max-age=60" }))); const weighted = new EsiClient({ userAgent: "test", fetch: weightedQueue.fetch, now: () => 0 }); for (let index = 0; index < 80; index += 1) await weighted.getAssetNames(7, weightedIds, "token", authority(`weighted-${index}`, 7)); const weightedStats = weighted.getCacheStats(); expect(weightedStats.cacheBytes).toBeLessThanOrEqual(ESI_CACHE_MAX_BYTES); expect(weightedStats.cacheEntries).toBeLessThan(80); const snapshotRows = Array.from({ length: 1_000 }, (_, index) => asset(String(index + 1))); const snapshotBody = JSON.stringify(snapshotRows); const snapshotResponses: Response[] = []; for (let index = 0; index < 51; index += 1) { snapshotResponses.push(response(snapshotBody, { "X-Pages": "1", "Cache-Control": "max-age=60" })); snapshotResponses.push(new Response(null, { status: 304, headers: { "X-Pages": "1", "Cache-Control": "max-age=60" } })); } const snapshots = new EsiClient({ userAgent: "test", fetch: queueFetch(snapshotResponses).fetch, now: () => 0 }); for (let index = 0; index < 51; index += 1) await snapshots.getAssets(7, "token", authority(`snapshot-${index}`, 7)); const snapshotStats = snapshots.getCacheStats(); expect(snapshotStats.assetSnapshotRows).toBeLessThanOrEqual(ESI_ASSET_SNAPSHOT_MAX_ROWS); expect(snapshotStats.assetSnapshots).toBeLessThan(51); }); }); describe("ESI market estimates", () => { test("keeps the inherited non-negative market-price policy", async () => { const queued = queueFetch([ response([{ type_id: 34, average_price: -1 }], { ETag: '"negative"', "Cache-Control": "max-age=60" }), ]); const client = new EsiClient({ userAgent: "test", fetch: queued.fetch, now: () => 1234 }); await expect(new EsiMarketEstimates(client).estimates([34])).rejects.toMatchObject({ code: "invalid_price" }); }); test("uses average prices only and returns cents with retrieval provenance", async () => { const queued = queueFetch([ response([ { type_id: 34, average_price: 12.345, adjusted_price: 999999 }, { type_id: 35, adjusted_price: 100 }, ], { ETag: '"prices"', "Cache-Control": "max-age=60" }), ]); const client = new EsiClient({ userAgent: "test", fetch: queued.fetch, now: () => 1234 }); const snapshot = await new EsiMarketEstimates(client).estimates([34, 35]); expect(snapshot.prices).toEqual([{ typeId: 34, unitPrice: "1235" }]); expect(snapshot.source).toContain("2026-08-18"); expect(snapshot.asOf).toBe(1234); expect(queued.calls[0]?.url).toBe("https://esi.evetech.net/markets/prices"); }); }); describe("ESI cache and limiter edge cases", () => { test("uses Date, Age, and Expires for freshness", async () => { let now = 1_000_000; const date = new Date(now - 10_000).toUTCString(); const expires = new Date(now + 50_000).toUTCString(); const queued = queueFetch([ response([{ type_id: 34, average_price: 1 }], { Date: date, Age: "2", Expires: expires, ETag: '"p"' }), new Response(null, { status: 304, headers: { Date: new Date(now + 60_000).toUTCString(), ETag: '"p"' } }), ]); const client = new EsiClient({ userAgent: "test", fetch: queued.fetch, now: () => now, sleep: async () => {} }); await new EsiMarketEstimates(client).estimates([34]); now += 40_000; await new EsiMarketEstimates(client).estimates([34]); expect(queued.calls).toHaveLength(1); now += 20_000; await new EsiMarketEstimates(client).estimates([34]); expect(queued.calls).toHaveLength(2); }); test("persists new and legacy limiter cooldowns across calls", async () => { let now = 100; const delays: number[] = []; const newer = queueFetch([ response([{ type_id: 34, average_price: 1 }], { "Cache-Control": "max-age=0", "X-Ratelimit-Group": "meta", "X-Ratelimit-Remaining": "0", "Retry-After": "2" }), response([{ type_id: 34, average_price: 1 }], { "Cache-Control": "max-age=60" }), ]); const newClient = new EsiClient({ userAgent: "test", fetch: newer.fetch, now: () => now, sleep: async (ms) => { delays.push(ms); } }); await new EsiMarketEstimates(newClient).estimates([34]); now = 200; await new EsiMarketEstimates(newClient).estimates([34]); expect(delays).toContain(1_900); now = 100; const legacyDelays: number[] = []; const legacy = queueFetch([ response([{ type_id: 34, average_price: 1 }], { "Cache-Control": "max-age=0", "X-ESI-Error-Limit-Remain": "0", "X-ESI-Error-Limit-Reset": "3" }), response([{ type_id: 34, average_price: 1 }], { "Cache-Control": "max-age=60" }), ]); const legacyClient = new EsiClient({ userAgent: "test", fetch: legacy.fetch, now: () => now, sleep: async (ms) => { legacyDelays.push(ms); } }); await new EsiMarketEstimates(legacyClient).estimates([34]); now = 200; await new EsiMarketEstimates(legacyClient).estimates([34]); expect(legacyDelays).toContain(2_900); }); test("does not retry an unbounded server delay beyond the request budget", async () => { const queued = queueFetch([response({}, { "Retry-After": "60" }, 429)]); const client = new EsiClient({ userAgent: "test", fetch: queued.fetch, now: () => 0, sleep: async () => {} }); await expect(new EsiMarketEstimates(client).estimates([34])).rejects.toBeInstanceOf(EsiRateLimitError); expect(queued.calls).toHaveLength(1); }); test("does not retain no-store bodies or validators", async () => { const body = [asset("1")]; const queued = queueFetch([ response(body, { "Cache-Control": "no-store", ETag: '"nope"', "X-Pages": "1" }), response(body, { "Cache-Control": "no-store", ETag: '"nope"', "X-Pages": "1" }), response(body, { "Cache-Control": "no-store", ETag: '"nope2"', "X-Pages": "1" }), response(body, { "Cache-Control": "no-store", ETag: '"nope2"', "X-Pages": "1" }), ]); const client = new EsiClient({ userAgent: "test", fetch: queued.fetch, now: () => 0 }); await client.getAssets(10, "token", authority("u", 10)); await client.getAssets(10, "token", authority("u", 10)); expect(queued.calls).toHaveLength(4); expect(new Headers(queued.calls[2]?.init.headers).get("if-none-match")).toBeNull(); }); test("cancels discarded retry and error response bodies", async () => { let retryCanceled = 0; const retryResponse = { status: 503, headers: new Headers(), body: { cancel: async () => { retryCanceled += 1; } }, } as unknown as Response; const retryQueue = queueFetch([ retryResponse, response([{ type_id: 34, average_price: 1 }]), ]); const retryClient = new EsiClient({ userAgent: "test", fetch: retryQueue.fetch, now: () => 0, sleep: async () => {} }); await new EsiMarketEstimates(retryClient).estimates([34]); expect(retryCanceled).toBe(1); let errorCanceled = 0; const errorResponse = { status: 401, headers: new Headers(), body: { cancel: async () => { errorCanceled += 1; } }, } as unknown as Response; const errorQueue = queueFetch([errorResponse]); const errorClient = new EsiClient({ userAgent: "test", fetch: errorQueue.fetch, now: () => 0 }); await expect(new EsiMarketEstimates(errorClient).estimates([34])).rejects.toBeInstanceOf(EsiHttpError); expect(errorCanceled).toBe(1); }); });