Files
asin-check/src/stalker-sellability.test.ts

208 lines
5.8 KiB
TypeScript

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,
},
]);
});