- docker compose 部署:app / db / redis / tls(nginx) / bridge-tools,宿主端口错开避免与本机服务冲突 - bridge-tools 内联进仓库(compose build 路径改为 ./bridge-tools),仓库自包含可部署 - 部署说明 README、crm-new 对照与 bridge-tools 方案文档、备份与隧道脚本 - .gitignore 排除 .env、config.toml、uploads/、tls 私钥/证书、backups/、logs/
196 lines
7.8 KiB
JavaScript
196 lines
7.8 KiB
JavaScript
'use strict';
|
||
|
||
// 单测:mock RPC,覆盖 4 个工具 + HTTP 层(鉴权/健康检查/身份头)
|
||
const { test } = require('node:test');
|
||
const assert = require('node:assert');
|
||
const { loadConfig } = require('../src/config');
|
||
const { createTools, resolveSiteProfile } = require('../src/tools');
|
||
const { createServer } = require('../src/server');
|
||
|
||
function baseConfig(env = {}) {
|
||
return loadConfig({
|
||
TOOLS_SHARED_KEY: 'test-key',
|
||
RECOMMEND_URL: 'jsonrpc://rpc:9632',
|
||
ORDER_API_URL: 'jsonrpc://rpc:9612',
|
||
INVENTORY_API_URL: 'jsonrpc://rpc:9509',
|
||
MEMBER_RPC_URL: 'jsonrpc://rpc:9602',
|
||
DEFAULT_TENANT_ID: '2',
|
||
DEFAULT_LANGUAGE_ID: '1',
|
||
...env,
|
||
});
|
||
}
|
||
|
||
function mockRpc(handlers = {}) {
|
||
const calls = [];
|
||
return {
|
||
calls,
|
||
async call(addr, method, params, opts, timeout) {
|
||
calls.push({ addr, method, params, timeout });
|
||
const h = handlers[method];
|
||
if (h === undefined) throw new Error(`unexpected rpc method: ${method}`);
|
||
return typeof h === 'function' ? h(params) : h;
|
||
},
|
||
};
|
||
}
|
||
|
||
const verifiedCtx = {
|
||
verified: true,
|
||
email: 'customer@example.com',
|
||
inboxId: '1',
|
||
conversationUuid: 'conv-1',
|
||
};
|
||
|
||
test('站点 profile:inbox 1 映射到 www / tenant 2 / EUR', () => {
|
||
const profile = resolveSiteProfile(baseConfig(), { inboxId: '1' });
|
||
assert.equal(profile.site, 'www');
|
||
assert.equal(profile.tenantId, 2);
|
||
assert.equal(profile.currencyCode, 'EUR');
|
||
assert.equal(profile.searchUrl, 'https://www.yehwang.com/search.html?q=');
|
||
});
|
||
|
||
test('工具清单只暴露 4 个业务工具', () => {
|
||
const tools = createTools(baseConfig(), { rpc: mockRpc() });
|
||
assert.deepEqual(tools.map((t) => t.name).sort(), ['get_customer_info', 'search_inventory', 'search_orders', 'search_products']);
|
||
assert.equal(tools.find((t) => t.name === 'search_orders').requiresVerification, true);
|
||
assert.equal(tools.find((t) => t.name === 'get_customer_info').requiresVerification, true);
|
||
});
|
||
|
||
test('search_products:catalog 命中,返回价格/库存/URL,并注入 RPC 上下文', async () => {
|
||
const rpc = mockRpc({
|
||
'/product/list': {
|
||
result: {
|
||
data: [{ spuName: 'Gold Hoop Earrings', href: '/p/gold-hoop.html', spuId: 11, minPriceWithCurrency: '€9.99', skus: [{ quantity: 2 }], masterImage: '/img/1.jpg' }],
|
||
total: 1,
|
||
},
|
||
},
|
||
});
|
||
const tools = createTools(baseConfig(), { rpc });
|
||
const out = await tools.find((t) => t.name === 'search_products').handler({ query: 'gold hoop earrings' }, verifiedCtx);
|
||
assert.equal(out.count, 1);
|
||
assert.equal(out.source, 'catalog');
|
||
assert.equal(out.products[0].name, 'Gold Hoop Earrings');
|
||
assert.equal(out.products[0].price, '€9.99');
|
||
assert.equal(out.products[0].stock, 2);
|
||
assert.equal(out.products[0].product_id, 11);
|
||
assert.equal(out.products[0].url, 'https://www.yehwang.com/p/gold-hoop.html');
|
||
const sent = rpc.calls[0].params[0];
|
||
assert.equal(sent.reqContext.tenantId, 2);
|
||
assert.equal(sent.reqContext.languageId, 1);
|
||
assert.ok(sent.deviceInfo._class);
|
||
});
|
||
|
||
test('search_products:catalog 空时退回 site-map 类目链接', async () => {
|
||
const rpc = mockRpc({
|
||
'/product/list': { result: { data: [] } },
|
||
'/product_new/autocomplete': { result: [] },
|
||
});
|
||
const tools = createTools(baseConfig(), { rpc });
|
||
const out = await tools.find((t) => t.name === 'search_products').handler({ query: 'rings' }, verifiedCtx);
|
||
assert.equal(out.count > 0, true);
|
||
assert.equal(out.source, 'site-map');
|
||
assert.ok(out.products[0].url);
|
||
});
|
||
|
||
test('search_orders:未验证直接 403,不触发 RPC', async () => {
|
||
const rpc = mockRpc({});
|
||
const tools = createTools(baseConfig(), { rpc });
|
||
await assert.rejects(
|
||
() => tools.find((t) => t.name === 'search_orders').handler({}, { ...verifiedCtx, verified: false }),
|
||
(err) => err.status === 403 && err.code === 'verification_required'
|
||
);
|
||
assert.equal(rpc.calls.length, 0);
|
||
});
|
||
|
||
test('search_orders:已验证返回订单摘要(邮箱只取身份头)', async () => {
|
||
const rpc = mockRpc({
|
||
'/erp_order/getList': { result: [{ orderId: 5, orderNo: 'YW1001', statusText: 'Shipped', total: '€42.00', dateAdded: '2026-10-01', shippingMethod: 'DHL' }] },
|
||
});
|
||
const tools = createTools(baseConfig(), { rpc });
|
||
const out = await tools.find((t) => t.name === 'search_orders').handler({}, verifiedCtx);
|
||
assert.equal(out.count, 1);
|
||
assert.equal(out.orders[0].order_no, 'YW1001');
|
||
assert.equal(out.orders[0].status, 'Shipped');
|
||
assert.equal(rpc.calls[0].params[0].email, 'customer@example.com');
|
||
});
|
||
|
||
test('search_inventory:合并库存与 ETA', async () => {
|
||
const rpc = mockRpc({
|
||
'/eta/queryStockSummary': { result: { instantStock: 10, reservedQuantity: 2, inTransitQuantity: 3 } },
|
||
'/eta/queryProductEta': { result: { etaDate: '2026-11-01', availableQuantity: 5, status: 'in_transit' } },
|
||
});
|
||
const tools = createTools(baseConfig(), { rpc });
|
||
const out = await tools.find((t) => t.name === 'search_inventory').handler({ product_id: 11 }, verifiedCtx);
|
||
assert.equal(out.product_id, 11);
|
||
assert.equal(out.instant_stock, 10);
|
||
assert.equal(out.available, 8);
|
||
assert.equal(out.in_transit, 3);
|
||
assert.equal(out.eta_date, '2026-11-01');
|
||
});
|
||
|
||
test('get_customer_info:邮箱查 customerId 后返回资料', async () => {
|
||
const rpc = mockRpc({
|
||
'/customer/checkEmailExist': { result: { customerId: 7 } },
|
||
'/customer/getByCustomerId': {
|
||
result: { firstname: 'Ada', lastname: 'Lovelace', email: 'customer@example.com', company: 'ACME', countryName: 'Netherlands', dial: '31', telephone: '612345678', gradeName: 'VIP', groupName: 'G1', status: 1, point: 100 },
|
||
},
|
||
});
|
||
const tools = createTools(baseConfig(), { rpc });
|
||
const out = await tools.find((t) => t.name === 'get_customer_info').handler({}, verifiedCtx);
|
||
assert.equal(out.found, true);
|
||
assert.equal(out.customer_id, 7);
|
||
assert.equal(out.name, 'Ada Lovelace');
|
||
assert.equal(out.phone, '+31612345678');
|
||
assert.equal(out.member_grade, 'VIP');
|
||
});
|
||
|
||
test('HTTP:healthz 公开、无 key 401、有 key 可调工具、未验证 403', async () => {
|
||
const rpc = mockRpc({
|
||
'/erp_order/getList': { result: [{ orderId: 5, orderNo: 'YW1001', statusText: 'Shipped' }] },
|
||
});
|
||
const cfg = baseConfig();
|
||
const server = createServer({ config: cfg, tools: createTools(cfg, { rpc }) });
|
||
await new Promise((resolve) => server.listen(0, '127.0.0.1', resolve));
|
||
const base = `http://127.0.0.1:${server.address().port}`;
|
||
|
||
try {
|
||
const health = await fetch(`${base}/healthz`);
|
||
assert.equal(health.status, 200);
|
||
const healthBody = await health.json();
|
||
assert.equal(healthBody.service, 'bridge-tools');
|
||
assert.equal(healthBody.tools.length, 4);
|
||
|
||
const noKey = await fetch(`${base}/tools/manifest`);
|
||
assert.equal(noKey.status, 401);
|
||
|
||
const manifest = await fetch(`${base}/tools/manifest`, { headers: { 'x-tools-key': 'test-key' } });
|
||
assert.equal(manifest.status, 200);
|
||
const manifestBody = await manifest.json();
|
||
assert.equal(manifestBody.tools.length, 4);
|
||
|
||
const unverified = await fetch(`${base}/tools/search_orders`, {
|
||
method: 'POST',
|
||
headers: { 'x-tools-key': 'test-key', 'content-type': 'application/json' },
|
||
body: '{}',
|
||
});
|
||
assert.equal(unverified.status, 403);
|
||
|
||
const verified = await fetch(`${base}/tools/search_orders`, {
|
||
method: 'POST',
|
||
headers: {
|
||
'x-tools-key': 'test-key',
|
||
'content-type': 'application/json',
|
||
'x-libredesk-contact-verified': 'true',
|
||
'x-libredesk-contact-email': 'customer@example.com',
|
||
'x-libredesk-inbox-id': '1',
|
||
},
|
||
body: '{}',
|
||
});
|
||
assert.equal(verified.status, 200);
|
||
const body = await verified.json();
|
||
assert.equal(body.tool, 'search_orders');
|
||
assert.equal(body.orders[0].order_no, 'YW1001');
|
||
} finally {
|
||
await new Promise((resolve) => server.close(resolve));
|
||
}
|
||
});
|