import { afterAll, beforeEach, expect, mock, test } from "bun:test"; import { mkdirSync, rmSync } from "node:fs"; import path from "node:path"; import * as XLSX from "xlsx"; import { closeDb, getDb } from "./database.ts"; const TEST_DIR = path.join(process.cwd(), "test_output", "stalker-sellability"); const originalFetch = globalThis.fetch; const originalKeepaKey = Bun.env.KEEPA_API_KEY; const fetchSellabilityBatchMock = mock(async (asins: string[]) => { return new Map( asins.map((asin) => [ asin, asin === "B111111111" ? { canSell: true, sellabilityStatus: "available" as const, sellabilityReason: "No listing restrictions reported", } : { canSell: false, sellabilityStatus: "restricted" as const, sellabilityReason: "approval required", }, ]), ); }); mock.module("./sp-api.ts", () => ({ fetchSellabilityBatch: fetchSellabilityBatchMock, })); const modulePromise = import("./stalker.ts"); beforeEach(() => { closeDb(); rmSync(TEST_DIR, { recursive: true, force: true }); mkdirSync(TEST_DIR, { recursive: true }); globalThis.fetch = originalFetch; Bun.env.KEEPA_API_KEY = "test-keepa-key"; fetchSellabilityBatchMock.mockClear(); }); afterAll(() => { globalThis.fetch = originalFetch; if (originalKeepaKey == null) { delete Bun.env.KEEPA_API_KEY; } else { Bun.env.KEEPA_API_KEY = originalKeepaKey; } closeDb(); rmSync(TEST_DIR, { recursive: true, force: true }); }); test("sellability checks matched seller inventory, not the source ASIN", async () => { const { runStalker } = await modulePromise; const inputPath = path.join(TEST_DIR, "input.xlsx"); const dbPath = path.join(TEST_DIR, "stalker.sqlite"); const workbook = XLSX.utils.book_new(); XLSX.utils.book_append_sheet( workbook, XLSX.utils.json_to_sheet([{ asin: "B000000001" }]), "Input", ); XLSX.writeFile(workbook, inputPath); globalThis.fetch = mock(async (input: string | URL | Request) => { const rawUrl = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url; const url = new URL(rawUrl); if (url.pathname === "/product") { if (url.searchParams.get("asin") === "B111111111") { return new Response( JSON.stringify({ products: [ { asin: "B111111111", title: "Sellable Storefront Product", brand: "Good Brand", categoryTree: [{ name: "Kitchen" }, { name: "Storage" }], monthlySold: 42, stats: { current: [null, null, null, 12345, null, null, null, null, null, null, null, 7], avg: [2500], }, csv: [[5000000, 1999, 5000100]], }, ], tokensLeft: 10, refillRate: 10, }), { status: 200 }, ); } return new Response( JSON.stringify({ products: [ { asin: "B000000001", title: "Source Product", offers: [{ sellerId: "AQUALIFIED", price: 1999 }], }, ], tokensLeft: 10, refillRate: 10, }), { status: 200 }, ); } if (url.pathname === "/seller") { const wantsStorefront = url.searchParams.get("storefront") === "1"; return new Response( JSON.stringify({ sellers: { AQUALIFIED: { sellerName: "New Seller", currentRatingCount: 12, asinList: wantsStorefront ? ["B111111111", "B222222222"] : [], }, }, tokensLeft: 10, refillRate: 10, }), { status: 200 }, ); } return new Response("not found", { status: 404 }); }) as unknown as typeof globalThis.fetch; const stats = await runStalker({ input: inputPath, dbPath, maxAsins: null, storefrontUpdateHours: 168, offerLimit: 20, sellerLimit: 30, inventoryLimit: 200, sellerCacheHours: 168, includeStock: false, dryRun: false, resume: true, maxSellerRequests: null, sellability: true, analyzeSellable: false, }); expect(fetchSellabilityBatchMock.mock.calls.length).toBe(1); expect(fetchSellabilityBatchMock.mock.calls[0]?.[0]).toEqual([ "B111111111", "B222222222", ]); expect(stats.inventorySellabilityCheckedAsins).toBe(2); expect(stats.inventorySellabilityAvailableAsins).toBe(1); expect(stats.inventorySellabilityExcludedAsins).toBe(1); expect(stats.persistedInventoryAsins).toBe(1); const db = getDb(dbPath); const scan = db.query("SELECT source_asin FROM stalker_asin_scans").get() as { source_asin: string; }; expect(scan.source_asin).toBe("B000000001"); const inventory = db .query( `SELECT asin, can_sell, sellability_status, product_title, brand, category_tree, current_price, avg_price_90d, sales_rank, monthly_sold, seller_count FROM stalker_seller_inventory ORDER BY asin`, ) .all() as Array<{ asin: string; can_sell: number | null; sellability_status: string | null; product_title: string | null; brand: string | null; category_tree: string | null; current_price: number | null; avg_price_90d: number | null; sales_rank: number | null; monthly_sold: number | null; seller_count: number | null; }>; expect(inventory).toEqual([ { asin: "B111111111", can_sell: 1, sellability_status: "available", product_title: "Sellable Storefront Product", brand: "Good Brand", category_tree: JSON.stringify(["Kitchen", "Storage"]), current_price: 19.99, avg_price_90d: 25, sales_rank: 12345, monthly_sold: 42, seller_count: 7, }, ]); });