Files
product-sourcing-capture/tests/server.test.js
T
mosen e9939ef9dc Initial release with production deploy config.
Add GitAtlas deploy assets, DATA_FILE support for isolated server data, and port documentation for sourcing.simosen.cn.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-15 00:52:31 +08:00

1356 lines
50 KiB
JavaScript

import assert from "node:assert/strict";
import { mkdir, rm, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import { dirname, join } from "node:path";
import { test } from "node:test";
import { fileURLToPath } from "node:url";
import { hashToken } from "../src/auth.js";
import { createServer } from "../src/server.js";
import { JsonStore } from "../src/store.js";
const ROOT_DIR = dirname(dirname(fileURLToPath(import.meta.url)));
async function startServer(options = {}) {
const dir = join(tmpdir(), `sourcing-api-${Date.now()}-${Math.random().toString(36).slice(2)}`);
await mkdir(dir, { recursive: true });
const store = new JsonStore(join(dir, "items.json"));
const server = createServer({ store, publicDir: join(dir, "public"), ...options });
await new Promise((resolve) => server.listen(0, resolve));
const baseUrl = `http://127.0.0.1:${server.address().port}`;
return {
baseUrl,
async close() {
await new Promise((resolve) => server.close(resolve));
await rm(dir, { recursive: true, force: true });
},
};
}
async function jsonRequest(baseUrl, path, { method = "GET", token, body } = {}) {
const headers = {};
if (body !== undefined) headers["Content-Type"] = "application/json";
if (token) headers.Authorization = `Bearer ${token}`;
const response = await fetch(`${baseUrl}${path}`, {
method,
headers,
body: body === undefined ? undefined : JSON.stringify(body),
});
const contentType = response.headers.get("content-type") || "";
const payload = contentType.includes("application/json") ? await response.json() : await response.text();
return { response, payload };
}
test("local API accepts extension captures and exports CSV", async () => {
const dir = join(tmpdir(), `sourcing-api-${Date.now()}`);
await mkdir(dir, { recursive: true });
const store = new JsonStore(join(dir, "items.json"));
const server = createServer({ store, publicDir: join(dir, "public") });
await new Promise((resolve) => server.listen(0, resolve));
const baseUrl = `http://127.0.0.1:${server.address().port}`;
try {
const captureResponse = await fetch(`${baseUrl}/api/captures`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
items: [
{
platform: "1688",
title: "藏式编绳手链 直播民族风",
url: "https://detail.1688.com/offer/100.html",
image: "https://img.example/100.jpg",
priceText: "¥6.80",
supplier: "义乌源头饰品厂",
metrics: { visibleText: "现货 混批 1000+人付款" },
},
],
}),
});
assert.equal(captureResponse.status, 201);
const capturePayload = await captureResponse.json();
assert.equal(capturePayload.items.length, 1);
assert.equal(capturePayload.items[0].decision, "拿样");
const listResponse = await fetch(`${baseUrl}/api/items?platform=1688`);
assert.equal(listResponse.status, 200);
const listPayload = await listResponse.json();
assert.equal(listPayload.items.length, 1);
const id = listPayload.items[0].id;
const updateResponse = await fetch(`${baseUrl}/api/items/${id}`, {
method: "PATCH",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ status: "ordered_sample", notes: "先拿样 3 件" }),
});
assert.equal(updateResponse.status, 200);
const updated = await updateResponse.json();
assert.equal(updated.item.status, "ordered_sample");
const csvResponse = await fetch(`${baseUrl}/api/export.csv`);
assert.equal(csvResponse.status, 200);
assert.match(await csvResponse.text(), /藏式编绳手链/);
} finally {
await new Promise((resolve) => server.close(resolve));
await rm(dir, { recursive: true, force: true });
}
});
test("health API exposes recommended extension version for update prompts", async () => {
const app = await startServer();
try {
const health = await jsonRequest(app.baseUrl, "/api/health");
assert.equal(health.response.status, 200);
assert.equal(health.payload.ok, true);
assert.equal(health.payload.extension.name, "选品采购采集器");
assert.match(health.payload.extension.recommendedVersion, /^\d+\.\d+\.\d+$/);
assert.equal(health.payload.extension.installUrl, "/downloads/product-sourcing-capture-extension.zip");
assert.match(health.payload.extension.fileName, /^product-sourcing-capture-extension-\d+\.\d+\.\d+\.zip$/);
assert.equal(health.payload.extension.reloadRequiredForUnpacked, true);
assert.ok(health.payload.extension.updateNote.includes("storage"));
} finally {
await app.close();
}
});
test("server packages browser extension as a downloadable zip", async () => {
const app = await startServer();
try {
const headResponse = await fetch(`${app.baseUrl}/downloads/product-sourcing-capture-extension.zip`, { method: "HEAD" });
assert.equal(headResponse.status, 200);
assert.equal(headResponse.headers.get("content-type"), "application/zip");
assert.match(
headResponse.headers.get("content-disposition") || "",
/attachment; filename="product-sourcing-capture-extension-\d+\.\d+\.\d+\.zip"/,
);
const response = await fetch(`${app.baseUrl}/downloads/product-sourcing-capture-extension.zip`);
assert.equal(response.status, 200);
assert.equal(response.headers.get("content-type"), "application/zip");
assert.match(
response.headers.get("content-disposition") || "",
/attachment; filename="product-sourcing-capture-extension-\d+\.\d+\.\d+\.zip"/,
);
const zip = Buffer.from(await response.arrayBuffer());
assert.ok(zip.length > 1000);
assert.equal(zip.slice(0, 4).toString("binary"), "PK\u0003\u0004");
assert.ok(zip.includes(Buffer.from("manifest.json")));
assert.ok(zip.includes(Buffer.from("popup.html")));
assert.ok(zip.includes(Buffer.from("content.js")));
} finally {
await app.close();
}
});
test("download routes do not fall back to the dashboard HTML", async () => {
const app = await startServer();
try {
const response = await fetch(`${app.baseUrl}/downloads/missing-extension.zip`);
assert.equal(response.status, 404);
assert.match(response.headers.get("content-type") || "", /application\/json/);
assert.deepEqual(await response.json(), { error: "Download not found" });
} finally {
await app.close();
}
});
test("dashboard exposes browser extension package download in plugin install areas", async () => {
const dir = join(tmpdir(), `sourcing-api-ui-${Date.now()}-${Math.random().toString(36).slice(2)}`);
const server = createServer({ store: new JsonStore(join(dir, "items.json")), publicDir: join(ROOT_DIR, "public") });
await new Promise((resolve) => server.listen(0, resolve));
const baseUrl = `http://127.0.0.1:${server.address().port}`;
try {
const homepage = await fetch(`${baseUrl}/`);
const html = await homepage.text();
assert.equal(homepage.status, 200);
assert.match(html, /\/downloads\/product-sourcing-capture-extension\.zip/);
assert.match(html, /下载插件安装包/);
const appJs = await fetch(`${baseUrl}/app.js`);
const js = await appJs.text();
assert.equal(appJs.status, 200);
assert.match(js, /\/downloads\/product-sourcing-capture-extension\.zip/);
assert.match(js, /下载插件安装包/);
} finally {
await new Promise((resolve) => server.close(resolve));
await rm(dir, { recursive: true, force: true });
}
});
test("homepage exposes absolute social sharing metadata and brand assets", async () => {
const dir = join(tmpdir(), `sourcing-api-brand-${Date.now()}-${Math.random().toString(36).slice(2)}`);
const publicDir = join(dir, "public");
await mkdir(join(publicDir, "assets"), { recursive: true });
await writeFile(
join(publicDir, "index.html"),
[
"<!doctype html>",
'<html><head><title>选品采购台|竞品采集、货源匹配与采购决策</title>',
'<link rel="icon" href="/favicon.ico" />',
'<meta name="description" content="采集淘宝、小红书等平台商品线索,匹配 1688 货源,完成筛选、利润测算、拿样与采购决策。" />',
'<meta property="og:title" content="选品采购台|竞品采集、货源匹配与采购决策" />',
'<meta property="og:image" content="__SHARE_ORIGIN__/assets/share-card.png" />',
'<meta property="og:url" content="__SHARE_ORIGIN__/" />',
"</head><body></body></html>",
].join(""),
"utf8",
);
await writeFile(join(publicDir, "favicon.ico"), "icon", "utf8");
await writeFile(join(publicDir, "assets", "share-card.png"), "share", "utf8");
const server = createServer({ store: new JsonStore(join(dir, "items.json")), publicDir });
await new Promise((resolve) => server.listen(0, resolve));
const baseUrl = `http://127.0.0.1:${server.address().port}`;
try {
const homepage = await fetch(`${baseUrl}/`);
const html = await homepage.text();
assert.equal(homepage.status, 200);
assert.match(html, /选品采购台|竞品采集、货源匹配与采购决策/);
assert.match(html, /rel="icon" href="\/favicon\.ico"/);
assert.match(html, /property="og:image"/);
assert.ok(html.includes(`content="${baseUrl}/assets/share-card.png"`));
assert.ok(html.includes(`content="${baseUrl}/"`));
assert.doesNotMatch(html, /__SHARE_ORIGIN__/);
assert.equal((await fetch(`${baseUrl}/favicon.ico`)).status, 200);
assert.equal((await fetch(`${baseUrl}/assets/share-card.png`)).status, 200);
} finally {
await new Promise((resolve) => server.close(resolve));
await rm(dir, { recursive: true, force: true });
}
});
test("SaaS auth isolates captures and rules by user", async () => {
const app = await startServer();
try {
const aliceRegister = await jsonRequest(app.baseUrl, "/api/auth/register", {
method: "POST",
body: { email: "alice@example.com", password: "secret123", name: "Alice 买手" },
});
assert.equal(aliceRegister.response.status, 201);
assert.ok(aliceRegister.payload.token);
assert.equal(aliceRegister.payload.user.email, "alice@example.com");
assert.equal(aliceRegister.payload.user.passwordHash, undefined);
const bobRegister = await jsonRequest(app.baseUrl, "/api/auth/register", {
method: "POST",
body: { email: "bob@example.com", password: "secret123", name: "Bob 买手" },
});
assert.equal(bobRegister.response.status, 201);
const unauthenticated = await jsonRequest(app.baseUrl, "/api/items");
assert.equal(unauthenticated.response.status, 401);
const aliceToken = aliceRegister.payload.token;
const bobToken = bobRegister.payload.token;
const capture = await jsonRequest(app.baseUrl, "/api/captures", {
method: "POST",
token: aliceToken,
body: {
items: [
{
platform: "1688",
title: "Alice 藏式编绳手链",
url: "https://detail.1688.com/offer/alice-auth.html",
priceText: "¥6.80",
},
],
},
});
assert.equal(capture.response.status, 201);
assert.equal(capture.payload.items[0].title, "Alice 藏式编绳手链");
const aliceItems = await jsonRequest(app.baseUrl, "/api/items", { token: aliceToken });
assert.equal(aliceItems.response.status, 200);
assert.deepEqual(
aliceItems.payload.items.map((item) => item.title),
["Alice 藏式编绳手链"],
);
const bobItems = await jsonRequest(app.baseUrl, "/api/items", { token: bobToken });
assert.equal(bobItems.response.status, 200);
assert.deepEqual(bobItems.payload.items, []);
const aliceRules = await jsonRequest(app.baseUrl, "/api/rules", { token: aliceToken });
assert.equal(aliceRules.response.status, 200);
assert.ok(aliceRules.payload.rules.tracks.some((track) => track.name === "藏式/民族风"));
const updateRules = await jsonRequest(app.baseUrl, "/api/rules", {
method: "PUT",
token: aliceToken,
body: {
rules: {
riskKeywords: ["过敏包治"],
profitModel: {
shippingFee: 5,
packagingFee: 2,
platformFeeRate: 0.06,
promoFee: 4,
maxRunningPrice: 99,
},
},
},
});
assert.equal(updateRules.response.status, 200);
assert.deepEqual(updateRules.payload.rules.riskKeywords, ["过敏包治"]);
assert.equal(updateRules.payload.rules.profitModel.shippingFee, 5);
const bobRules = await jsonRequest(app.baseUrl, "/api/rules", { token: bobToken });
assert.equal(bobRules.response.status, 200);
assert.notDeepEqual(bobRules.payload.rules.riskKeywords, ["过敏包治"]);
const login = await jsonRequest(app.baseUrl, "/api/auth/login", {
method: "POST",
body: { email: "alice@example.com", password: "secret123" },
});
assert.equal(login.response.status, 200);
assert.ok(login.payload.token);
assert.equal(login.payload.user.email, "alice@example.com");
const badLogin = await jsonRequest(app.baseUrl, "/api/auth/login", {
method: "POST",
body: { email: "alice@example.com", password: "wrong-password" },
});
assert.equal(badLogin.response.status, 401);
} finally {
await app.close();
}
});
test("admin can manage users and disabled users cannot sign in", async () => {
const app = await startServer();
try {
const adminRegister = await jsonRequest(app.baseUrl, "/api/auth/register", {
method: "POST",
body: { email: "admin@example.com", password: "secret123", name: "平台管理员" },
});
assert.equal(adminRegister.response.status, 201);
assert.equal(adminRegister.payload.user.role, "admin");
assert.equal(adminRegister.payload.user.status, "active");
const buyerRegister = await jsonRequest(app.baseUrl, "/api/auth/register", {
method: "POST",
body: { email: "buyer@example.com", password: "secret123", name: "买手用户" },
});
assert.equal(buyerRegister.response.status, 201);
assert.equal(buyerRegister.payload.user.role, "buyer");
const buyerAdminRead = await jsonRequest(app.baseUrl, "/api/admin/users", { token: buyerRegister.payload.token });
assert.equal(buyerAdminRead.response.status, 403);
const adminUsers = await jsonRequest(app.baseUrl, "/api/admin/users", { token: adminRegister.payload.token });
assert.equal(adminUsers.response.status, 200);
assert.equal(adminUsers.payload.users.length, 2);
assert.deepEqual(
adminUsers.payload.users.map((user) => [user.email, user.role, user.status, user.itemCount, user.pluginTokenCount]),
[
["admin@example.com", "admin", "active", 0, 0],
["buyer@example.com", "buyer", "active", 0, 0],
],
);
const disableBuyer = await jsonRequest(app.baseUrl, `/api/admin/users/${buyerRegister.payload.user.id}/status`, {
method: "PATCH",
token: adminRegister.payload.token,
body: { status: "disabled" },
});
assert.equal(disableBuyer.response.status, 200);
assert.equal(disableBuyer.payload.user.status, "disabled");
const disabledLogin = await jsonRequest(app.baseUrl, "/api/auth/login", {
method: "POST",
body: { email: "buyer@example.com", password: "secret123" },
});
assert.equal(disabledLogin.response.status, 403);
const disabledSessionRead = await jsonRequest(app.baseUrl, "/api/items", { token: buyerRegister.payload.token });
assert.equal(disabledSessionRead.response.status, 403);
const disableSelf = await jsonRequest(app.baseUrl, `/api/admin/users/${adminRegister.payload.user.id}/status`, {
method: "PATCH",
token: adminRegister.payload.token,
body: { status: "disabled" },
});
assert.equal(disableSelf.response.status, 400);
} finally {
await app.close();
}
});
test("plugin tokens can capture to one user but cannot read private library", async () => {
const app = await startServer();
try {
const register = await jsonRequest(app.baseUrl, "/api/auth/register", {
method: "POST",
body: { email: "plugin-owner@example.com", password: "secret123", name: "插件用户" },
});
const sessionToken = register.payload.token;
const createdToken = await jsonRequest(app.baseUrl, "/api/plugin-tokens", {
method: "POST",
token: sessionToken,
body: { name: "Chrome 云采集插件" },
});
assert.equal(createdToken.response.status, 201);
assert.ok(createdToken.payload.token.startsWith("pst_"));
assert.equal(createdToken.payload.pluginToken.name, "Chrome 云采集插件");
assert.equal(createdToken.payload.pluginToken.tokenHash, undefined);
const createdCode = await jsonRequest(app.baseUrl, "/api/admin/credit-codes", {
method: "POST",
token: sessionToken,
body: { credits: 3 },
});
const redeemed = await jsonRequest(app.baseUrl, "/api/credits/redeem", {
method: "POST",
token: sessionToken,
body: { code: createdCode.payload.code },
});
assert.equal(redeemed.response.status, 200);
const pluginCapture = await jsonRequest(app.baseUrl, "/api/captures", {
method: "POST",
token: createdToken.payload.token,
body: {
items: [
{
platform: "xiaohongshu",
title: "插件云端采集珍珠耳饰",
url: "https://www.xiaohongshu.com/explore/plugin-token",
priceText: "¥59",
},
],
},
});
assert.equal(pluginCapture.response.status, 201);
assert.equal(pluginCapture.payload.items[0].title, "插件云端采集珍珠耳饰");
const pluginRead = await jsonRequest(app.baseUrl, "/api/items", { token: createdToken.payload.token });
assert.equal(pluginRead.response.status, 403);
const ownerRead = await jsonRequest(app.baseUrl, "/api/items", { token: sessionToken });
assert.equal(ownerRead.response.status, 200);
assert.deepEqual(
ownerRead.payload.items.map((item) => item.title),
["插件云端采集珍珠耳饰"],
);
} finally {
await app.close();
}
});
test("plugin tokens track usage and can be disabled or deleted by owner", async () => {
const app = await startServer();
try {
const register = await jsonRequest(app.baseUrl, "/api/auth/register", {
method: "POST",
body: { email: "token-owner@example.com", password: "secret123", name: "Token 用户" },
});
const sessionToken = register.payload.token;
const createdToken = await jsonRequest(app.baseUrl, "/api/plugin-tokens", {
method: "POST",
token: sessionToken,
body: { name: "直播间采集插件" },
});
assert.equal(createdToken.response.status, 201);
assert.equal(createdToken.payload.pluginToken.status, "active");
assert.equal(createdToken.payload.pluginToken.usageCount, 0);
assert.equal(createdToken.payload.pluginToken.lastUsedAt, null);
const createdCode = await jsonRequest(app.baseUrl, "/api/admin/credit-codes", {
method: "POST",
token: sessionToken,
body: { credits: 2 },
});
const redeemed = await jsonRequest(app.baseUrl, "/api/credits/redeem", {
method: "POST",
token: sessionToken,
body: { code: createdCode.payload.code },
});
assert.equal(redeemed.response.status, 200);
const pluginCapture = await jsonRequest(app.baseUrl, "/api/captures", {
method: "POST",
token: createdToken.payload.token,
body: {
items: [
{
platform: "1688",
title: "Token 统计藏式手链",
url: "https://detail.1688.com/offer/token-usage.html",
priceText: "¥6.80",
},
],
},
});
assert.equal(pluginCapture.response.status, 201);
const tokenListAfterUse = await jsonRequest(app.baseUrl, "/api/plugin-tokens", { token: sessionToken });
assert.equal(tokenListAfterUse.response.status, 200);
assert.equal(tokenListAfterUse.payload.pluginTokens[0].usageCount, 1);
assert.ok(tokenListAfterUse.payload.pluginTokens[0].lastUsedAt);
const disabledToken = await jsonRequest(app.baseUrl, `/api/plugin-tokens/${createdToken.payload.pluginToken.id}`, {
method: "PATCH",
token: sessionToken,
body: { status: "disabled" },
});
assert.equal(disabledToken.response.status, 200);
assert.equal(disabledToken.payload.pluginToken.status, "disabled");
const disabledCapture = await jsonRequest(app.baseUrl, "/api/captures", {
method: "POST",
token: createdToken.payload.token,
body: {
items: [
{
platform: "1688",
title: "禁用 token 不应采集",
url: "https://detail.1688.com/offer/token-disabled.html",
priceText: "¥7.80",
},
],
},
});
assert.equal(disabledCapture.response.status, 403);
const deleteToken = await jsonRequest(app.baseUrl, `/api/plugin-tokens/${createdToken.payload.pluginToken.id}`, {
method: "DELETE",
token: sessionToken,
});
assert.equal(deleteToken.response.status, 200);
assert.equal(deleteToken.payload.deleted, 1);
const tokenListAfterDelete = await jsonRequest(app.baseUrl, "/api/plugin-tokens", { token: sessionToken });
assert.equal(tokenListAfterDelete.response.status, 200);
assert.deepEqual(tokenListAfterDelete.payload.pluginTokens, []);
} finally {
await app.close();
}
});
test("plugin token owner can copy token again from token list", async () => {
const app = await startServer();
try {
const ownerRegister = await jsonRequest(app.baseUrl, "/api/auth/register", {
method: "POST",
body: { email: "copy-token-owner@example.com", password: "secret123", name: "可复制 Token 用户" },
});
const otherRegister = await jsonRequest(app.baseUrl, "/api/auth/register", {
method: "POST",
body: { email: "copy-token-other@example.com", password: "secret123", name: "其他用户" },
});
const createdToken = await jsonRequest(app.baseUrl, "/api/plugin-tokens", {
method: "POST",
token: ownerRegister.payload.token,
body: { name: "可再次复制的插件" },
});
assert.equal(createdToken.response.status, 201);
assert.ok(createdToken.payload.token.startsWith("pst_"));
const ownerList = await jsonRequest(app.baseUrl, "/api/plugin-tokens", { token: ownerRegister.payload.token });
assert.equal(ownerList.response.status, 200);
assert.equal(ownerList.payload.pluginTokens.length, 1);
assert.equal(ownerList.payload.pluginTokens[0].token, createdToken.payload.token);
assert.equal(ownerList.payload.pluginTokens[0].tokenHash, undefined);
const otherList = await jsonRequest(app.baseUrl, "/api/plugin-tokens", { token: otherRegister.payload.token });
assert.equal(otherList.response.status, 200);
assert.deepEqual(otherList.payload.pluginTokens, []);
} finally {
await app.close();
}
});
test("admin can issue credit codes and buyers redeem them into account credits", async () => {
const app = await startServer();
try {
const adminRegister = await jsonRequest(app.baseUrl, "/api/auth/register", {
method: "POST",
body: { email: "credit-admin@example.com", password: "secret123", name: "积分管理员" },
});
const buyerRegister = await jsonRequest(app.baseUrl, "/api/auth/register", {
method: "POST",
body: { email: "credit-buyer@example.com", password: "secret123", name: "积分买手" },
});
assert.equal(adminRegister.payload.user.credits, 0);
assert.equal(buyerRegister.payload.user.credits, 0);
const buyerCannotIssue = await jsonRequest(app.baseUrl, "/api/admin/credit-codes", {
method: "POST",
token: buyerRegister.payload.token,
body: { credits: 30 },
});
assert.equal(buyerCannotIssue.response.status, 403);
const createdCode = await jsonRequest(app.baseUrl, "/api/admin/credit-codes", {
method: "POST",
token: adminRegister.payload.token,
body: { credits: 30, note: "首批采集额度" },
});
assert.equal(createdCode.response.status, 201);
assert.equal(createdCode.payload.creditCode.credits, 30);
assert.equal(createdCode.payload.creditCode.status, "active");
assert.match(createdCode.payload.code, /^CDX-[A-Z0-9-]+$/);
const codeList = await jsonRequest(app.baseUrl, "/api/admin/credit-codes", { token: adminRegister.payload.token });
assert.equal(codeList.response.status, 200);
assert.equal(codeList.payload.creditCodes.length, 1);
assert.equal(codeList.payload.creditCodes[0].code, createdCode.payload.code);
assert.equal(codeList.payload.creditCodes[0].redeemedBy, null);
const redeemed = await jsonRequest(app.baseUrl, "/api/credits/redeem", {
method: "POST",
token: buyerRegister.payload.token,
body: { code: createdCode.payload.code.toLowerCase() },
});
assert.equal(redeemed.response.status, 200);
assert.equal(redeemed.payload.user.credits, 30);
assert.equal(redeemed.payload.creditCode.status, "redeemed");
const me = await jsonRequest(app.baseUrl, "/api/me", { token: buyerRegister.payload.token });
assert.equal(me.response.status, 200);
assert.equal(me.payload.user.credits, 30);
const redeemAgain = await jsonRequest(app.baseUrl, "/api/credits/redeem", {
method: "POST",
token: buyerRegister.payload.token,
body: { code: createdCode.payload.code },
});
assert.equal(redeemAgain.response.status, 409);
} finally {
await app.close();
}
});
test("admin manages AI settings without exposing API keys to buyers", async () => {
const app = await startServer();
try {
const adminRegister = await jsonRequest(app.baseUrl, "/api/auth/register", {
method: "POST",
body: { email: "ai-admin@example.com", password: "secret123", name: "AI 管理员" },
});
const buyerRegister = await jsonRequest(app.baseUrl, "/api/auth/register", {
method: "POST",
body: { email: "ai-buyer@example.com", password: "secret123", name: "AI 买手" },
});
const buyerRead = await jsonRequest(app.baseUrl, "/api/admin/ai-settings", { token: buyerRegister.payload.token });
assert.equal(buyerRead.response.status, 403);
const saved = await jsonRequest(app.baseUrl, "/api/admin/ai-settings", {
method: "PUT",
token: adminRegister.payload.token,
body: {
enabled: true,
provider: "deepseek",
baseUrl: "https://api.deepseek.com/v1",
apiKey: "sk-secret-value",
model: "deepseek-chat",
temperature: 0.2,
maxOutputTokens: 900,
timeoutMs: 15000,
tokenUnitCost: 2,
prompts: {
itemAnalysis: "请分析这个商品",
},
},
});
assert.equal(saved.response.status, 200);
assert.equal(saved.payload.aiSettings.enabled, true);
assert.equal(saved.payload.aiSettings.provider, "deepseek");
assert.equal(saved.payload.aiSettings.model, "deepseek-chat");
assert.equal(saved.payload.aiSettings.apiKeyMasked, "sk-...alue");
assert.equal(saved.payload.aiSettings.apiKey, undefined);
const readBack = await jsonRequest(app.baseUrl, "/api/admin/ai-settings", { token: adminRegister.payload.token });
assert.equal(readBack.response.status, 200);
assert.equal(readBack.payload.aiSettings.apiKeyMasked, "sk-...alue");
assert.equal(readBack.payload.aiSettings.apiKey, undefined);
assert.equal(readBack.payload.aiSettings.prompts.itemAnalysis, "请分析这个商品");
} finally {
await app.close();
}
});
test("admin can issue AI token codes and buyers redeem them into AI token balance", async () => {
const app = await startServer();
try {
const adminRegister = await jsonRequest(app.baseUrl, "/api/auth/register", {
method: "POST",
body: { email: "ai-token-admin@example.com", password: "secret123", name: "AI Token 管理员" },
});
const buyerRegister = await jsonRequest(app.baseUrl, "/api/auth/register", {
method: "POST",
body: { email: "ai-token-buyer@example.com", password: "secret123", name: "AI Token 买手" },
});
assert.equal(buyerRegister.payload.user.aiTokens, 0);
const createdCode = await jsonRequest(app.baseUrl, "/api/admin/credit-codes", {
method: "POST",
token: adminRegister.payload.token,
body: { credits: 5, aiTokens: 12000, note: "AI 分析包" },
});
assert.equal(createdCode.response.status, 201);
assert.equal(createdCode.payload.creditCode.credits, 5);
assert.equal(createdCode.payload.creditCode.aiTokens, 12000);
const redeemed = await jsonRequest(app.baseUrl, "/api/credits/redeem", {
method: "POST",
token: buyerRegister.payload.token,
body: { code: createdCode.payload.code },
});
assert.equal(redeemed.response.status, 200);
assert.equal(redeemed.payload.user.credits, 5);
assert.equal(redeemed.payload.user.aiTokens, 12000);
const me = await jsonRequest(app.baseUrl, "/api/me", { token: buyerRegister.payload.token });
assert.equal(me.payload.user.aiTokens, 12000);
} finally {
await app.close();
}
});
test("AI item analysis uses admin model settings and deducts user AI tokens", async () => {
const calls = [];
const fakeAiClient = {
async chat({ settings, messages }) {
calls.push({ settings, messages });
return {
text: JSON.stringify({
summary: "适合直播测试的低客单商品",
purchaseAdvice: "建议先拿样 3 件",
sellingPoints: ["视觉记忆点强", "适合组合购"],
risks: ["确认材质和售后"],
supplierQuestions: ["是否现货?"],
}),
usage: { inputTokens: 320, outputTokens: 180, totalTokens: 500 },
raw: { id: "chatcmpl_fake" },
};
},
};
const app = await startServer({ aiClient: fakeAiClient });
try {
const adminRegister = await jsonRequest(app.baseUrl, "/api/auth/register", {
method: "POST",
body: { email: "ai-use-admin@example.com", password: "secret123", name: "AI 使用管理员" },
});
const buyerRegister = await jsonRequest(app.baseUrl, "/api/auth/register", {
method: "POST",
body: { email: "ai-use-buyer@example.com", password: "secret123", name: "AI 使用买手" },
});
await jsonRequest(app.baseUrl, "/api/admin/ai-settings", {
method: "PUT",
token: adminRegister.payload.token,
body: {
enabled: true,
provider: "openai-compatible",
baseUrl: "https://proxy.example.com/v1",
apiKey: "sk-proxy-secret",
model: "gpt-4o-mini",
tokenUnitCost: 1,
},
});
const createdCode = await jsonRequest(app.baseUrl, "/api/admin/credit-codes", {
method: "POST",
token: adminRegister.payload.token,
body: { aiTokens: 1000, note: "AI 测试额度" },
});
await jsonRequest(app.baseUrl, "/api/credits/redeem", {
method: "POST",
token: buyerRegister.payload.token,
body: { code: createdCode.payload.code },
});
const capture = await jsonRequest(app.baseUrl, "/api/captures", {
method: "POST",
token: buyerRegister.payload.token,
body: {
items: [
{
platform: "1688",
title: "藏式编绳手机链 低价跑量",
url: "https://detail.1688.com/offer/ai-analysis.html",
priceText: "¥5.80",
supplier: "义乌源头饰品厂",
},
],
},
});
const analysis = await jsonRequest(app.baseUrl, "/api/ai/analyze", {
method: "POST",
token: buyerRegister.payload.token,
body: { type: "item_analysis", itemId: capture.payload.items[0].id },
});
assert.equal(analysis.response.status, 200);
assert.equal(analysis.payload.analysis.summary, "适合直播测试的低客单商品");
assert.deepEqual(analysis.payload.analysis.sellingPoints, ["视觉记忆点强", "适合组合购"]);
assert.equal(analysis.payload.usage.totalTokens, 500);
assert.equal(analysis.payload.billing.deductedAiTokens, 500);
assert.equal(analysis.payload.billing.remainingAiTokens, 500);
assert.equal(calls.length, 1);
assert.equal(calls[0].settings.apiKey, "sk-proxy-secret");
assert.equal(calls[0].settings.model, "gpt-4o-mini");
assert.match(calls[0].messages.at(-1).content, /藏式编绳手机链/);
const me = await jsonRequest(app.baseUrl, "/api/me", { token: buyerRegister.payload.token });
assert.equal(me.payload.user.aiTokens, 500);
} finally {
await app.close();
}
});
test("AI analysis requires enabled settings and enough AI tokens", async () => {
const app = await startServer({
aiClient: {
async chat() {
throw new Error("AI client should not be called without enough tokens");
},
},
});
try {
const adminRegister = await jsonRequest(app.baseUrl, "/api/auth/register", {
method: "POST",
body: { email: "ai-limit-admin@example.com", password: "secret123", name: "AI 限额管理员" },
});
const buyerRegister = await jsonRequest(app.baseUrl, "/api/auth/register", {
method: "POST",
body: { email: "ai-limit-buyer@example.com", password: "secret123", name: "AI 限额买手" },
});
const disabled = await jsonRequest(app.baseUrl, "/api/ai/analyze", {
method: "POST",
token: buyerRegister.payload.token,
body: { type: "item_analysis", item: { title: "未启用 AI" } },
});
assert.equal(disabled.response.status, 409);
await jsonRequest(app.baseUrl, "/api/admin/ai-settings", {
method: "PUT",
token: adminRegister.payload.token,
body: {
enabled: true,
baseUrl: "https://proxy.example.com/v1",
apiKey: "sk-secret",
model: "gpt-4o-mini",
},
});
const noBalance = await jsonRequest(app.baseUrl, "/api/ai/analyze", {
method: "POST",
token: buyerRegister.payload.token,
body: { type: "item_analysis", item: { title: "无 AI Token" } },
});
assert.equal(noBalance.response.status, 402);
assert.equal(noBalance.payload.aiTokens.remaining, 0);
} finally {
await app.close();
}
});
test("plugin capture requires shared account credits and deducts per saved item", async () => {
const app = await startServer();
try {
const adminRegister = await jsonRequest(app.baseUrl, "/api/auth/register", {
method: "POST",
body: { email: "quota-admin@example.com", password: "secret123", name: "额度管理员" },
});
const buyerRegister = await jsonRequest(app.baseUrl, "/api/auth/register", {
method: "POST",
body: { email: "quota-buyer@example.com", password: "secret123", name: "额度买手" },
});
const buyerToken = buyerRegister.payload.token;
const firstPluginToken = await jsonRequest(app.baseUrl, "/api/plugin-tokens", {
method: "POST",
token: buyerToken,
body: { name: "淘宝采集插件" },
});
const secondPluginToken = await jsonRequest(app.baseUrl, "/api/plugin-tokens", {
method: "POST",
token: buyerToken,
body: { name: "1688 采集插件" },
});
assert.ok(firstPluginToken.payload.token.startsWith("pst_"));
assert.ok(secondPluginToken.payload.token.startsWith("pst_"));
const noCreditCapture = await jsonRequest(app.baseUrl, "/api/captures", {
method: "POST",
token: firstPluginToken.payload.token,
body: {
items: [
{
platform: "taobao",
title: "无积分不应采集",
url: "https://item.taobao.com/item.htm?id=no-credit",
priceText: "¥39",
},
],
},
});
assert.equal(noCreditCapture.response.status, 402);
const createdCode = await jsonRequest(app.baseUrl, "/api/admin/credit-codes", {
method: "POST",
token: adminRegister.payload.token,
body: { credits: 3 },
});
const redeemed = await jsonRequest(app.baseUrl, "/api/credits/redeem", {
method: "POST",
token: buyerToken,
body: { code: createdCode.payload.code },
});
assert.equal(redeemed.payload.user.credits, 3);
const firstCapture = await jsonRequest(app.baseUrl, "/api/captures", {
method: "POST",
token: firstPluginToken.payload.token,
body: {
items: [
{
platform: "taobao",
title: "积分采集淘宝耳饰",
url: "https://item.taobao.com/item.htm?id=credit-a",
priceText: "¥39",
},
{
platform: "xiaohongshu",
title: "积分采集小红书手链",
url: "https://www.xiaohongshu.com/explore/credit-b",
priceText: "¥49",
},
],
},
});
assert.equal(firstCapture.response.status, 201);
assert.equal(firstCapture.payload.credits.remaining, 1);
assert.equal(firstCapture.payload.credits.deducted, 2);
const secondCapture = await jsonRequest(app.baseUrl, "/api/captures", {
method: "POST",
token: secondPluginToken.payload.token,
body: {
items: [
{
platform: "1688",
title: "第二个 Token 共用积分",
url: "https://detail.1688.com/offer/shared-credit.html",
priceText: "¥6.80",
},
],
},
});
assert.equal(secondCapture.response.status, 201);
assert.equal(secondCapture.payload.credits.remaining, 0);
const meAfterCapture = await jsonRequest(app.baseUrl, "/api/me", { token: buyerToken });
assert.equal(meAfterCapture.payload.user.credits, 0);
const insufficientCapture = await jsonRequest(app.baseUrl, "/api/captures", {
method: "POST",
token: firstPluginToken.payload.token,
body: {
items: [
{
platform: "1688",
title: "积分耗尽不应采集",
url: "https://detail.1688.com/offer/no-more-credit.html",
priceText: "¥8.80",
},
],
},
});
assert.equal(insufficientCapture.response.status, 402);
} finally {
await app.close();
}
});
test("plugin capture charges duplicate urls in one batch as one saved lead", async () => {
const app = await startServer();
try {
const adminRegister = await jsonRequest(app.baseUrl, "/api/auth/register", {
method: "POST",
body: { email: "dedupe-admin@example.com", password: "secret123", name: "去重管理员" },
});
const buyerRegister = await jsonRequest(app.baseUrl, "/api/auth/register", {
method: "POST",
body: { email: "dedupe-buyer@example.com", password: "secret123", name: "去重买手" },
});
const buyerToken = buyerRegister.payload.token;
const pluginToken = await jsonRequest(app.baseUrl, "/api/plugin-tokens", {
method: "POST",
token: buyerToken,
body: { name: "重复链接采集插件" },
});
const createdCode = await jsonRequest(app.baseUrl, "/api/admin/credit-codes", {
method: "POST",
token: adminRegister.payload.token,
body: { credits: 1 },
});
await jsonRequest(app.baseUrl, "/api/credits/redeem", {
method: "POST",
token: buyerToken,
body: { code: createdCode.payload.code },
});
const duplicateCapture = await jsonRequest(app.baseUrl, "/api/captures", {
method: "POST",
token: pluginToken.payload.token,
body: {
items: [
{
platform: "taobao",
title: "重复采集的藏式手链 A",
url: "https://item.taobao.com/item.htm?id=duplicate-credit",
priceText: "¥49",
},
{
platform: "taobao",
title: "重复采集的藏式手链 B",
url: "https://item.taobao.com/item.htm?id=duplicate-credit",
priceText: "¥45",
},
],
},
});
assert.equal(duplicateCapture.response.status, 201);
assert.equal(duplicateCapture.payload.items.length, 1);
assert.equal(duplicateCapture.payload.items[0].title, "重复采集的藏式手链 B");
assert.equal(duplicateCapture.payload.credits.deducted, 1);
assert.equal(duplicateCapture.payload.credits.remaining, 0);
const items = await jsonRequest(app.baseUrl, "/api/items", { token: buyerToken });
assert.equal(items.payload.items.length, 1);
} finally {
await app.close();
}
});
test("plugin capture keeps credits unchanged when saving fails", async () => {
const pluginTokenValue = "pst_failure_test_token";
const fakeStore = {
user: {
id: "usr_failure",
email: "failure-buyer@example.com",
name: "失败买手",
role: "buyer",
status: "active",
credits: 1,
},
pluginToken: {
id: "ptk_failure",
userId: "usr_failure",
tokenHash: hashToken(pluginTokenValue),
status: "active",
usageCount: 0,
},
async userCount() {
return 1;
},
async findPluginTokenByHash(tokenHash) {
if (tokenHash !== this.pluginToken.tokenHash) return null;
return { user: this.user, pluginToken: this.pluginToken };
},
async deductCredits(userId, amount) {
this.user.credits -= amount;
return { deducted: amount, remaining: this.user.credits };
},
async insertMany() {
const error = new Error("Simulated save failure");
error.status = 503;
throw error;
},
async insertManyWithCreditDeduction() {
const error = new Error("Simulated save failure");
error.status = 503;
throw error;
},
};
const dir = join(tmpdir(), `sourcing-api-failed-save-${Date.now()}-${Math.random().toString(36).slice(2)}`);
await mkdir(dir, { recursive: true });
const server = createServer({ store: fakeStore, publicDir: join(dir, "public") });
await new Promise((resolve) => server.listen(0, resolve));
const baseUrl = `http://127.0.0.1:${server.address().port}`;
try {
const failedCapture = await jsonRequest(baseUrl, "/api/captures", {
method: "POST",
token: pluginTokenValue,
body: {
items: [
{
platform: "1688",
title: "保存失败不应扣积分",
url: "https://detail.1688.com/offer/failure-credit.html",
priceText: "¥6.80",
},
],
},
});
assert.equal(failedCapture.response.status, 503);
assert.equal(fakeStore.user.credits, 1);
assert.equal(fakeStore.pluginToken.usageCount, 0);
} finally {
await new Promise((resolve) => server.close(resolve));
await rm(dir, { recursive: true, force: true });
}
});
test("legacy users without role or status are normalized for admin access", async () => {
const dir = join(tmpdir(), `sourcing-api-legacy-users-${Date.now()}-${Math.random().toString(36).slice(2)}`);
await mkdir(dir, { recursive: true });
const filePath = join(dir, "items.json");
const store = new JsonStore(filePath);
const server = createServer({ store, publicDir: join(dir, "public") });
const admin = await store.createUser({
email: "legacy-admin@example.com",
name: "旧管理员",
passwordHash: "legacy-admin-hash",
});
const buyer = await store.createUser({
email: "legacy-buyer@example.com",
name: "旧买手",
passwordHash: "legacy-buyer-hash",
});
await writeFile(
filePath,
JSON.stringify(
{
users: [
{ id: admin.id, email: admin.email, name: admin.name, passwordHash: admin.passwordHash, createdAt: admin.createdAt },
{ id: buyer.id, email: buyer.email, name: buyer.name, passwordHash: buyer.passwordHash, createdAt: buyer.createdAt },
],
items: [{ id: "legacy-item", userId: buyer.id, title: "旧数据饰品", platform: "1688", createdAt: "2026-01-01T00:00:00.000Z" }],
pluginTokens: [{ id: "legacy-token", userId: buyer.id, name: "旧插件", tokenHash: "hash", createdAt: "2026-01-01T00:00:00.000Z" }],
sessions: [],
rules: {},
},
null,
2,
),
"utf8",
);
await new Promise((resolve) => server.listen(0, resolve));
const baseUrl = `http://127.0.0.1:${server.address().port}`;
try {
const adminToken = "sess_legacy_admin_access";
const adminSession = await store.createSession({ userId: admin.id, tokenHash: hashToken(adminToken) });
assert.ok(adminSession.id);
const adminUsers = await jsonRequest(baseUrl, "/api/admin/users", { token: adminToken });
assert.equal(adminUsers.response.status, 200);
assert.deepEqual(
adminUsers.payload.users.map((user) => [user.email, user.role, user.status, user.itemCount, user.pluginTokenCount]),
[
["legacy-admin@example.com", "admin", "active", 0, 0],
["legacy-buyer@example.com", "buyer", "active", 1, 1],
],
);
} finally {
await new Promise((resolve) => server.close(resolve));
await rm(dir, { recursive: true, force: true });
}
});
test("first SaaS user claims existing local captures during migration", async () => {
const app = await startServer();
try {
const localCapture = await jsonRequest(app.baseUrl, "/api/captures", {
method: "POST",
body: {
items: [
{
platform: "1688",
title: "注册前本地采集藏式手链",
url: "https://detail.1688.com/offer/local-before-register.html",
priceText: "¥5.80",
},
],
},
});
assert.equal(localCapture.response.status, 201);
const register = await jsonRequest(app.baseUrl, "/api/auth/register", {
method: "POST",
body: { email: "first-user@example.com", password: "secret123", name: "首个用户" },
});
assert.equal(register.response.status, 201);
const ownedItems = await jsonRequest(app.baseUrl, "/api/items", { token: register.payload.token });
assert.equal(ownedItems.response.status, 200);
assert.deepEqual(
ownedItems.payload.items.map((item) => item.title),
["注册前本地采集藏式手链"],
);
} finally {
await app.close();
}
});
test("local API deletes selected captures in one bulk request", async () => {
const dir = join(tmpdir(), `sourcing-api-bulk-${Date.now()}`);
await mkdir(dir, { recursive: true });
const store = new JsonStore(join(dir, "items.json"));
const server = createServer({ store, publicDir: join(dir, "public") });
await new Promise((resolve) => server.listen(0, resolve));
const baseUrl = `http://127.0.0.1:${server.address().port}`;
try {
const captureResponse = await fetch(`${baseUrl}/api/captures`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
items: [
{
platform: "1688",
title: "误采集 A",
url: "https://detail.1688.com/offer/api-bulk-a.html",
priceText: "¥3.80",
},
{
platform: "1688",
title: "误采集 B",
url: "https://detail.1688.com/offer/api-bulk-b.html",
priceText: "¥4.80",
},
{
platform: "1688",
title: "保留 C",
url: "https://detail.1688.com/offer/api-bulk-c.html",
priceText: "¥5.80",
},
],
}),
});
const { items } = await captureResponse.json();
const deleteResponse = await fetch(`${baseUrl}/api/items/bulk-delete`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ ids: [items[0].id, items[1].id, "missing-id"] }),
});
assert.equal(deleteResponse.status, 200);
const deleted = await deleteResponse.json();
assert.equal(deleted.deleted, 2);
assert.deepEqual(deleted.missing, ["missing-id"]);
const listResponse = await fetch(`${baseUrl}/api/items`);
const listPayload = await listResponse.json();
assert.equal(listPayload.items.length, 1);
assert.equal(listPayload.items[0].title, "保留 C");
} finally {
await new Promise((resolve) => server.close(resolve));
await rm(dir, { recursive: true, force: true });
}
});
test("local API exposes dashboard, groups, sample queue, and bulk updates", async () => {
const dir = join(tmpdir(), `sourcing-api-workbench-${Date.now()}`);
await mkdir(dir, { recursive: true });
const store = new JsonStore(join(dir, "items.json"));
const server = createServer({ store, publicDir: join(dir, "public") });
await new Promise((resolve) => server.listen(0, resolve));
const baseUrl = `http://127.0.0.1:${server.address().port}`;
try {
const captureResponse = await fetch(`${baseUrl}/api/captures`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
items: [
{
platform: "taobao",
title: "藏式绿松石编绳手链 民族风",
url: "https://item.taobao.com/item.htm?id=api-style-a",
priceText: "¥39.90",
},
{
platform: "1688",
title: "民族风绿松石色编织手链 藏式",
url: "https://detail.1688.com/offer/api-style-b.html",
priceText: "¥6.80",
supplier: "义乌源头厂",
},
],
}),
});
const { items } = await captureResponse.json();
const bulkResponse = await fetch(`${baseUrl}/api/items/bulk-update`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
ids: [items[1].id],
patch: { status: "asking_supplier" },
addTags: ["低价跑量"],
}),
});
assert.equal(bulkResponse.status, 200);
assert.equal((await bulkResponse.json()).updated, 1);
const dashboardResponse = await fetch(`${baseUrl}/api/dashboard`);
assert.equal(dashboardResponse.status, 200);
const dashboard = await dashboardResponse.json();
assert.equal(dashboard.dashboard.totals.items, 2);
const groupsResponse = await fetch(`${baseUrl}/api/style-groups`);
assert.equal(groupsResponse.status, 200);
const groups = await groupsResponse.json();
assert.equal(groups.groups.length, 1);
assert.equal(groups.groups[0].items.length, 2);
const sampleResponse = await fetch(`${baseUrl}/api/sample-queue`);
assert.equal(sampleResponse.status, 200);
const sampleQueue = await sampleResponse.json();
assert.equal(sampleQueue.items.length, 1);
assert.equal(sampleQueue.items[0].status, "asking_supplier");
} finally {
await new Promise((resolve) => server.close(resolve));
await rm(dir, { recursive: true, force: true });
}
});