'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)); } });