const assert = require('node:assert/strict'); const fs = require('node:fs'); const path = require('node:path'); const test = require('node:test'); const VipPages = require('../public/js/vip-pages.js'); const root = path.resolve(__dirname, '..'); const packageId = '2062179707935264769'; const genealogyId = '2062179707935264770'; const orderId = '2062179707935264771'; const transactionId = '2062179707935264772'; function packageFixture(overrides) { return Object.assign({ packageId, packageName: '家谱年度会员', packageType: 'vip', packageDesc: '适合持续维护家谱', price: '99.00', originalPrice: '129.00', durationValue: 1, durationUnit: 'year', genealogyLimit: 3, memberLimit: 500, storageLimitMb: 10240, featureJson: '{"export":true}', sortOrder: 1, status: '0', remark: '年度套餐' }, overrides); } function orderFixture(overrides) { return Object.assign({ orderId, orderNo: 'VIP202607291200001234', packageId, packageName: '家谱年度会员', appUserId: '2062179707935264701', appUserNickName: '叶子', appUserPhone: '19100000000', genealogyId, genealogyNo: 'G20260729001', genealogyName: '叶氏家谱', orderAmount: '99.00', payAmount: '99.00', payType: 'wechat', payStatus: '0', payTime: null, expireTime: null, status: '0', remark: '', transactionId, canRefreshPayment: true, canClose: true }, overrides); } test('VIP 套餐只保留可展示字段并保持金额和长 ID 字符串', () => { assert.deepEqual(VipPages.normalizeVipPackage(packageFixture()), { packageId, packageName: '家谱年度会员', packageType: 'vip', packageDesc: '适合持续维护家谱', price: '99.00', originalPrice: '129.00', durationValue: 1, durationUnit: 'year', genealogyLimit: 3, memberLimit: 500, storageLimitMb: 10240, remark: '年度套餐' }); assert.equal(VipPages.normalizeVipPackage(packageFixture({ packageId: Number.MAX_SAFE_INTEGER + 1 })), null); assert.equal(VipPages.normalizeVipPackage(packageFixture({ packageType: 'unknown' })), null); assert.equal(VipPages.normalizeVipPackage(packageFixture({ durationUnit: 'week' })), null); assert.equal(VipPages.normalizeVipPackage(packageFixture({ price: '-1.00' })), null); assert.equal(VipPages.normalizeVipPackage(packageFixture({ status: '1' })), null); }); test('套餐列表过滤停用或非法记录', () => { assert.equal(VipPages.normalizeVipPackages([ packageFixture(), packageFixture({ packageId: '2062179707935264772', status: '1' }) ]).length, 1); assert.deepEqual(VipPages.normalizeVipPackages({ rows: [packageFixture()] }), []); }); test('家谱选择项只接受响应中的稳定 ID 和名称', () => { assert.deepEqual(VipPages.normalizeGenealogyOption({ genealogyId, genealogyName: '叶氏家谱', surname: '叶', ownerUserId: 'must-drop' }), { genealogyId, genealogyName: '叶氏家谱', surname: '叶' }); assert.equal(VipPages.normalizeGenealogyOption({ genealogyId: Number.MAX_SAFE_INTEGER + 1, genealogyName: '叶氏家谱' }), null); assert.equal(VipPages.normalizeGenealogyOption({ genealogyId, genealogyName: '' }), null); assert.deepEqual(VipPages.normalizeGenealogyOptions([ { genealogyId, genealogyName: '叶氏家谱', surname: '叶' }, { genealogyId: 'unsafe', genealogyName: '无效家谱' } ]), [{ genealogyId, genealogyName: '叶氏家谱', surname: '叶' }]); assert.equal(VipPages.resolveContextGenealogyId([ { genealogyId, genealogyName: '叶氏家谱', surname: '叶' } ], genealogyId), genealogyId); assert.equal(VipPages.resolveContextGenealogyId([ { genealogyId, genealogyName: '叶氏家谱', surname: '叶' } ], 'unsafe'), ''); }); test('订单请求只构造 PcVipOrderBody,支付方式由后端确定', () => { assert.deepEqual(VipPages.buildVipOrderBody({ packageId: ' ' + packageId + ' ', genealogyId: ' ' + genealogyId + ' ', payType: '', orderId: 'must-drop', appUserId: 'must-drop', payStatus: 'must-drop' }), { packageId, genealogyId }); assert.deepEqual(VipPages.buildVipOrderBody({ packageId, payType: ' wechat ' }), { packageId }); }); test('订单请求要求安全套餐 ID,并只接受已确认的可选值', () => { assert.equal(VipPages.validateVipOrderBody({ packageId }), ''); assert.equal(VipPages.validateVipOrderBody({ packageId, genealogyId }), ''); assert.match(VipPages.validateVipOrderBody({ packageId: '' }), /套餐/); assert.match(VipPages.validateVipOrderBody({ packageId, genealogyId: 'unsafe' }), /家谱/); assert.equal(VipPages.validateVipOrderBody({ packageId, payType: 'alipay' }), ''); }); test('VIP 订单隐藏账号字段并校验金额和状态', () => { assert.deepEqual(VipPages.normalizeVipOrder(orderFixture()), { orderId, orderNo: 'VIP202607291200001234', packageId, packageName: '家谱年度会员', genealogyId, genealogyNo: 'G20260729001', genealogyName: '叶氏家谱', orderAmount: '99.00', payAmount: '99.00', payType: 'wechat', payStatus: '0', payTime: '', expireTime: '', status: '0', remark: '', transactionId, canRefreshPayment: true, canClose: true }); assert.equal(VipPages.normalizeVipOrder(orderFixture({ orderId: Number.MAX_SAFE_INTEGER + 1 })), null); assert.equal(VipPages.normalizeVipOrder(orderFixture({ payAmount: '-1' })), null); assert.equal(VipPages.normalizeVipOrder(orderFixture({ payStatus: '9' })), null); assert.equal(VipPages.normalizeVipOrder(orderFixture({ status: '9' })), null); }); test('套餐和订单渲染不出现内部账号信息,并按服务端能力提供支付操作', () => { const packagesHtml = VipPages.renderVipPackages([packageFixture({ packageName: '<年度会员>' })], packageId); const ordersHtml = VipPages.renderVipOrders([orderFixture({ appUserId: 'secret-user', appUserPhone: 'secret-phone' })]); const combined = packagesHtml + ordersHtml; assert.match(packagesHtml, /<年度会员>/); assert.match(packagesHtml, new RegExp('data-vip-package-id="' + packageId + '"')); assert.match(ordersHtml, /待支付/); assert.match(ordersHtml, /data-vip-payment-refresh/); assert.match(ordersHtml, /data-vip-payment-close/); assert.doesNotMatch(combined, /secret-user|secret-phone|appUserId|appUserPhone|模拟支付|取消订单|退款/); }); test('套餐渲染接受初始化流程已经规范化的数据', () => { const normalized = VipPages.normalizeVipPackages([packageFixture()]); const html = VipPages.renderVipPackages(normalized, packageId); assert.match(html, /家谱年度会员/); assert.match(html, /data-vip-package-id=/); assert.doesNotMatch(html, /当前没有可用会员套餐/); }); test('会员服务页只从接口结果选择套餐和家谱,并使用微信扫码支付', () => { const source = fs.readFileSync(path.join(root, 'profile-services.html'), 'utf8'); assert.doesNotMatch(source, /data-feature-status="pending"/); assert.match(source, /src="public\/js\/vip-pages\.js"/); assert.match(source, /data-vip-package-list/); assert.match(source, /data-vip-selected-package/); assert.match(source, /]+data-vip-genealogy-options/); assert.match(source, /data-vip-order-refresh/); assert.match(source, /data-vip-order-list/); assert.match(source, /data-vip-order-form novalidate/); assert.match(source, /data-vip-order-status[^>]*role="status"/); assert.match(source, /data-api-state="loading"/); assert.doesNotMatch(source, /]+name="(?:packageId|genealogyId|orderId)"/); assert.doesNotMatch(source, /name="payType"/); assert.match(source, /data-vip-payment-panel/); assert.match(source, /public\/js\/qrcode\.min\.js/); assert.doesNotMatch(source, /模拟支付|取消订单|退款/); assert.ok(source.indexOf('src="public/js/profile-common.js"') < source.indexOf('src="public/js/vip-pages.js"')); }); test('会员服务页补齐桌面端设置与协议入口,不伪造账户接口', () => { const source = fs.readFileSync(path.join(root, 'profile-services.html'), 'utf8'); assert.match(source, /平台设置与协议/); assert.match(source, /href="about\.html"/); assert.match(source, /href="privacy\.html"/); assert.match(source, /href="children-privacy\.html"/); assert.match(source, /href="terms\.html"/); assert.match(source, /个性化推荐/); assert.match(source, /推荐设置正在完善,目前不会保存开关状态/); assert.doesNotMatch(source, /data-setting-form|data-withdraw-form|data-share-reward/); assert.doesNotMatch(source, /data-recommendation-toggle|name="recommendation"/); }); test('会员服务运行时读取支付能力并展示后端返回的 NATIVE 支付单', () => { const source = fs.readFileSync(path.join(root, 'public/js/vip-pages.js'), 'utf8'); assert.match(source, /api\.vipCapability\(\), api\.vipPackages\(\), api\.genealogiesMine\(\), api\.vipOrders\(\)/); assert.match(source, /payment = normalizePaymentOrder\(await api\.createVipOrder\(body\)\)/); assert.match(source, /showPayment\(payment\)/); assert.match(source, /refreshed = normalizeVipOrders\(await api\.vipOrders\(\)\)/); assert.match(source, /item\.orderId === created\.orderId/); }); test('会员服务将加载、空数据、提交校验与请求失败写入统一状态组件', () => { const source = fs.readFileSync(path.join(root, 'public/js/vip-pages.js'), 'utf8'); assert.match(source, /function setApiState\(target, type, message\)/); assert.match(source, /root\.ProfileUI && root\.ProfileUI\.setApiState/); assert.match(source, /setListState\(packageList, 'loading', '正在读取会员套餐…'\)/); assert.match(source, /setListState\(orderList, 'empty', '当前没有会员订单。'\)/); assert.match(source, /setStatus\('error', validation\)/); assert.match(source, /setPendingButton\(submit, true, '正在创建订单…'\)/); assert.match(source, /root\.ProfileUI && typeof root\.ProfileUI\.getGenealogyId === 'function'/); assert.match(source, /已关联当前家谱,请选择会员套餐/); });