- Replaced direct SQLite database calls with Drizzle ORM methods in `top-monthly-sold-by-category.ts`, `writer.ts`, and `upc-file-analysis.ts`. - Updated test cases in `top-monthly-sold-by-category.test.ts` to mock the new database interactions. - Removed unnecessary database initialization and cleanup code. - Improved code readability and maintainability by using ORM features for inserting and updating records.
183 lines
5.1 KiB
TypeScript
183 lines
5.1 KiB
TypeScript
import { test, expect, beforeEach, mock } from "bun:test";
|
|
|
|
let nextId = 0;
|
|
|
|
function chainable(resolveWith: any[] = []): any {
|
|
const p: any = Promise.resolve(resolveWith);
|
|
p.limit = (_n: any) => chainable(resolveWith);
|
|
p.where = (_cond: any) => chainable(resolveWith);
|
|
p.from = (_table: any) => chainable(resolveWith);
|
|
return p;
|
|
}
|
|
|
|
const makeMockDb = (): any => ({
|
|
insert: (_table: any) => ({
|
|
values: (_vals: any) => ({
|
|
returning: (_sel: any) => Promise.resolve([{ id: ++nextId }]),
|
|
onConflictDoUpdate: (_conf: any) => Promise.resolve([]),
|
|
}),
|
|
}),
|
|
update: (_table: any) => ({
|
|
set: (_vals: any) => ({
|
|
where: (_cond: any) => Promise.resolve([]),
|
|
}),
|
|
}),
|
|
select: (_sel?: any) => ({
|
|
from: (_table: any) => ({
|
|
where: (_cond: any) => chainable(),
|
|
limit: (_n: any) => chainable(),
|
|
}),
|
|
}),
|
|
selectDistinct: (_sel: any) => ({
|
|
from: (_table: any) => chainable(),
|
|
}),
|
|
execute: (_query: any) => Promise.resolve([]),
|
|
transaction: async (fn: (tx: any) => Promise<any>) => fn(makeMockDb()),
|
|
});
|
|
|
|
mock.module("./db/index.ts", () => ({ db: makeMockDb(), client: {} }));
|
|
|
|
const fetchSellabilityBatchMock = mock(async (asins: string[]) => {
|
|
return new Map(
|
|
asins.map((asin) => [
|
|
asin,
|
|
{
|
|
canSell: true,
|
|
sellabilityStatus: "available" as const,
|
|
sellabilityReason: "ok",
|
|
},
|
|
]),
|
|
);
|
|
});
|
|
|
|
const fetchSpApiPricingAndFeesMock = mock(async () => ({
|
|
fbaFee: 4,
|
|
fbmFee: 2,
|
|
referralFeePercent: 15,
|
|
estimatedSalePrice: 25,
|
|
canSell: true,
|
|
sellabilityStatus: "available" as const,
|
|
sellabilityReason: "ok",
|
|
}));
|
|
|
|
const analyzeProductsMock = mock(async (products: any[]) => {
|
|
return products.map((p, idx) => ({
|
|
asin: p.record.asin,
|
|
verdict: idx === 0 ? "FBA" : "FBM",
|
|
confidence: 90,
|
|
reasoning: "mocked",
|
|
}));
|
|
});
|
|
|
|
mock.module("./sp-api.ts", () => ({
|
|
fetchSellabilityBatch: fetchSellabilityBatchMock,
|
|
fetchSpApiPricingAndFees: fetchSpApiPricingAndFeesMock,
|
|
}));
|
|
|
|
mock.module("./llm.ts", () => ({
|
|
analyzeProducts: analyzeProductsMock,
|
|
}));
|
|
|
|
const modulePromise = import("./bestsellers-by-category.ts");
|
|
|
|
let processCategory: (runId: number, category: any, perCategoryTop: number) => Promise<any>;
|
|
let insertCategoryRunSummary: (summary: any, runTimestamp: string) => Promise<number>;
|
|
let originalFetch: typeof globalThis.fetch;
|
|
|
|
const mod = await modulePromise;
|
|
processCategory = mod.processCategory;
|
|
insertCategoryRunSummary = mod.insertCategoryRunSummary;
|
|
originalFetch = globalThis.fetch;
|
|
|
|
beforeEach(() => {
|
|
nextId = 0;
|
|
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 === "/bestsellers") {
|
|
return new Response(
|
|
JSON.stringify({
|
|
bestSellersList: ["B000000001", "B000000002"],
|
|
tokensLeft: 10,
|
|
refillRate: 1,
|
|
}),
|
|
{ status: 200 },
|
|
);
|
|
}
|
|
|
|
if (url.pathname === "/product") {
|
|
return new Response(
|
|
JSON.stringify({
|
|
products: [
|
|
{
|
|
asin: "B000000001",
|
|
title: "Product One",
|
|
stats: { current: [null, null, null, 1000, null, null, null, null, null, null, null, 2, null, null, null, null, null, null, 2599], avg: [2400, null, null, 1200] },
|
|
csv: [[1, 2599]],
|
|
categoryTree: [{ name: "Category 1" }],
|
|
},
|
|
{
|
|
asin: "B000000002",
|
|
title: "Product Two",
|
|
stats: { current: [null, null, null, 2000, null, null, null, null, null, null, null, 3, null, null, null, null, null, null, 1999], avg: [1800, null, null, 2200] },
|
|
csv: [[1, 1999]],
|
|
categoryTree: [{ name: "Category 1" }],
|
|
},
|
|
],
|
|
tokensLeft: 10,
|
|
refillRate: 1,
|
|
}),
|
|
{ status: 200 },
|
|
);
|
|
}
|
|
|
|
return new Response("not found", { status: 404 });
|
|
}) as unknown as typeof globalThis.fetch;
|
|
});
|
|
|
|
test("processCategory function test", async () => {
|
|
const mockCategory = {
|
|
id: 1,
|
|
label: "Category 1",
|
|
parentId: 0,
|
|
childCount: 0,
|
|
};
|
|
|
|
const runId = await insertCategoryRunSummary(
|
|
{
|
|
categoryId: mockCategory.id,
|
|
categoryLabel: mockCategory.label,
|
|
topAsinsChecked: 0,
|
|
availableAsins: 0,
|
|
fba: 0,
|
|
fbm: 0,
|
|
skip: 0,
|
|
status: "running",
|
|
error: "",
|
|
results: [],
|
|
},
|
|
new Date().toISOString(),
|
|
);
|
|
|
|
const summary = await processCategory(runId, mockCategory, 2);
|
|
|
|
expect(summary.status).toBe("ok");
|
|
expect(summary.topAsinsChecked).toBe(2);
|
|
expect(summary.availableAsins).toBe(2);
|
|
expect(summary.fba).toBe(1);
|
|
expect(summary.fbm).toBe(1);
|
|
expect(summary.results?.length).toBe(2);
|
|
expect(summary.results?.[0]?.product.record.asin).toBe("B000000001");
|
|
expect(summary.results?.[0]?.verdict.verdict).toBe("FBA");
|
|
expect(summary.results?.[1]?.product.record.asin).toBe("B000000002");
|
|
expect(summary.results?.[1]?.verdict.verdict).toBe("FBM");
|
|
|
|
globalThis.fetch = originalFetch;
|
|
});
|