/** * 独立进程的 HTTP 假后端,仅监听回环地址,按既有 /console/v1 信封响应。 * 只模拟测试需要的外部接口,不是可部署的业务后端,也不执行真实付款或短信投递。 * 控制口允许场景重置数据/制造挂起和失效;它不进入产品接口实现。 */ import { createServer } from "node:http"; const token = "e2e-session-token"; const account = { id: 1, username: "audit-user", email: "audit@example.invalid", credit: "100.00", }; const initialKeys = Array.from({ length: 28 }, (_, index) => ({ // 25个audit匹配项 + 3个other项,覆盖搜索结果10/10/5分页与切换过滤条件。 id: index + 1, name: index < 25 ? `audit-${String(index + 1).padStart(2, "0")}` : `other-${index + 1}`, public_key: `ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAITestPublicKey${index + 1}`, finger_print: `SHA256:fixture-${index + 1}`, })); let options = {}; let requests = []; let paid = false; let keys = initialKeys.map((key) => ({ ...key })); function reply(response, data = {}, extra = {}, httpStatus = 200) { // HTTP状态与业务信封可以独立覆盖,确保前端同时处理传输401和信封401。 response.writeHead(httpStatus, { "Content-Type": "application/json" }); response.end(JSON.stringify({ status: httpStatus, msg: "", data, ...extra })); } const server = createServer(async (request, response) => { // 预览服务和假后端端口不同,需要允许测试浏览器发送JSON与Bearer请求。 response.setHeader("Access-Control-Allow-Origin", "*"); response.setHeader( "Access-Control-Allow-Headers", "Authorization, Content-Type, Accept", ); response.setHeader( "Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS", ); if (request.method === "OPTIONS") return response.writeHead(204).end(); const url = new URL(request.url, "http://127.0.0.1"); let body; try { const chunks = []; for await (const chunk of request) chunks.push(chunk); const raw = Buffer.concat(chunks).toString(); body = raw ? JSON.parse(raw) : undefined; } catch { return reply(response, {}, { msg: "Invalid fixture request body" }, 400); } if (url.pathname === "/__control/reset" && request.method === "POST") { // 场景从全新列表和未付款状态开始;11行变体用于删除最后一页唯一记录。 options = body ?? {}; requests = []; paid = false; keys = initialKeys .slice(0, options.sshLastPageSingleRow ? 11 : initialKeys.length) .map((key) => ({ ...key })); return reply(response); } if (url.pathname === "/__control/options" && request.method === "POST") { options = { ...options, ...body }; return reply(response); } if (url.pathname === "/__control/requests") return reply(response, requests); const path = url.pathname.replace(/^\/console\/v1/, ""); const record = { // 请求原文仅在测试进程内存供断言使用,测试输出端另做脱敏,不能直接导出此对象。 method: request.method, path, query: Object.fromEntries(url.searchParams), body, authorization: request.headers.authorization ?? null, referrer: request.headers.referer ?? null, handled: true, }; requests.push(record); if (path === "/common" && request.method === "GET") { // 模拟站点配置决定允许的登录方式;离线场景关闭外部CAPTCHA与OAuth。 return reply(response, { website_name: "本地前端验收", login_phone_password: options.loginMode === "sms-only" ? 0 : 1, login_email_password: options.loginMode === "sms-only" ? 0 : 1, login_phone_verify: options.loginMode === "password-only" ? 0 : 1, first_login_method: options.loginMode === "password-only" ? "code" : "password", captcha_client_login: 0, captcha_client_login_error: 0, captcha_client_login_error_3_times: 0, register_phone: 1, register_email: 1, plugin_configuration: { mp_weixin_notice: { scan_login: 0 } }, oauth: [], }); } if (path === "/login" && request.method === "POST") return reply(response, { jwt: token }); if (path === "/phone/code" && request.method === "POST") return reply(response); // 公共配置和登录之后的接口必须携带实际登录得到的测试令牌。 if (request.headers.authorization !== `Bearer ${token}`) { record.invalidAuthorization = true; return reply(response, {}, { msg: "Missing fixture session" }, 401); } if (path === "/index" && request.method === "GET" && options.expiredLegacy) return reply( response, {}, { status: 401, msg: "Expired legacy fixture session" }, ); if (path === "/index" && request.method === "GET") return reply(response, { account }); if (path === "/account" && request.method === "GET") return reply(response, { account }); if (path === "/menu" && request.method === "GET") { return reply(response, { menu: options.emptyMenu ? [] : [ { id: 1, name: "SSH 密钥", menu_type: "custom", url: "/security/ssh-keys", }, { id: 2, name: "订单", menu_type: "custom", url: "/orders" }, ], }); } if (path === "/client_care/mail" && request.method === "GET") { return reply(response, { credit_limit: { count: 0, list: [], type: [] } }); } if (path === "/logout" && request.method === "POST") { if (options.hangLogout) return; return reply(response); } if (path === "/ssh_key" && request.method === "GET") { if (options.hangSshBody) { // 只发送响应头和不完整JSON,保持连接以复现“响应体解码永不结束”。 response.writeHead(200, { "Content-Type": "application/json" }); response.write('{"status":200,"msg":"","data":'); return; } if (options.expiredSession) return reply( response, {}, { status: 401, msg: "Expired fixture session" }, ); const page = Math.max(1, Number(url.searchParams.get("page")) || 1); // 列表接口仍走后端分页;搜索聚合/切片由真实前端路径负责。 const limit = Math.max(1, Number(url.searchParams.get("limit")) || 10); return reply(response, { list: keys.slice((page - 1) * limit, page * limit), count: keys.length, }); } if (path === "/ssh_key" && request.method === "POST") { const id = Math.max(0, ...keys.map((key) => key.id)) + 1; keys.push({ id, ...body, finger_print: `SHA256:fixture-${id}` }); return reply(response, { id }); } const keyId = /^\/ssh_key\/(\d+)$/.exec(path); if (keyId && request.method === "PUT") { const key = keys.find((entry) => entry.id === Number(keyId[1])); if (!key) return reply(response, {}, { msg: "Fixture key not found" }, 404); Object.assign(key, body); return reply(response); } if (keyId && request.method === "DELETE") { keys = keys.filter((entry) => entry.id !== Number(keyId[1])); return reply(response); } if (path === "/order/42" && request.method === "GET") { // 余额、订单与支付状态共享paid事实,支付成功后回读必须反映已付金额。 if (options.expiredPayment) return reply( response, {}, { status: 401, msg: "Expired payment fixture session" }, ); return reply(response, { order: { id: 42, amount: "20.00", credit: "0.00", amount_unpaid: paid ? "0.00" : "20.00", type: "new", status: paid ? "Paid" : "Unpaid", create_time: 1700000000, pay_time: paid ? 1700000060 : 0, gateway: "alipay", items: [ { id: 1, product_name: "本地测试实例", description: "前端支付验收", amount: "20.00", }, ], }, customfields: { is_pre_invoice: 0 }, }); } if (path === "/order/42/transaction_record" && request.method === "GET") { return reply(response, { list: paid ? [{ amount: "20.00", create_time: 1700000060, description: "alipay" }] : [], }); } if (path === "/gateway" && request.method === "GET") { return reply(response, { list: [{ id: 1, name: "alipay", title: "本地测试支付" }], }); } if (path === "/voucher/pay" && request.method === "GET") return reply(response, { list: [] }); if (path === "/pay" && request.method === "POST") { // Paid是现有后端的顶层字段;挂起变体返回渠道HTML,随后状态查询不响应。 if (options.hangPaymentStatus) return reply(response, { html: '
本地支付渠道
' }); paid = true; return reply(response, {}, { code: "Paid" }); } if (path === "/pay/42/status" && request.method === "GET") { if (options.hangPaymentStatus) return; return reply(response, {}, { code: paid ? "Paid" : "Unpaid" }); } if (path === "/certification/info" && request.method === "GET") { return reply(response, { certification_open: 0, certification_company_open: 0, is_certification: 0, }); } if ( ["/ticket", "/announcement", "/host"].includes(path) && request.method === "GET" ) { return reply(response, { list: [], count: 0, ...(path === "/host" ? { server_page: "classified-v1" } : {}), }); } if (path === "/withdraw/rule/credit" && request.method === "GET") return reply(response, {}); if (path === "/recommend/promoter" && request.method === "GET") return reply(response, { promoter: { permission: 0 } }); // 未实现接口显式失败并留标记,防止新增产品请求被“统一成功”假后端掩盖。 record.handled = false; reply( response, {}, { msg: `Unhandled fixture endpoint: ${record.method} ${path}` }, 404, ); }); server.listen(0, "127.0.0.1", () => { // 由系统选择空闲端口,首行JSON作为父测试进程的启动握手。 process.stdout.write( `${JSON.stringify({ origin: `http://127.0.0.1:${server.address().port}` })}\n`, ); }); process.on("SIGTERM", () => { // 先关闭挂起连接,确保超时测试也能正常清理进程。 server.closeAllConnections(); server.close(() => process.exit(0)); });