Files
lucky_shop/tests/harmony-patch.test.js
ZF sun f4a770e246 refactor(mp-harmony): 完善隐私协议配置同步流程与鸿蒙双轨适配
1.  新增隐私配置同步脚本,自动从manifest和site.js同步真实隐私协议URL到鸿蒙构建产物
2.  补充鸿蒙元服务隐私托管双轨制适配逻辑
3.  新增完整的隐私弹窗组件单元测试覆盖
4.  修复构建产物隐私配置被占位符覆盖的问题
2026-07-24 14:43:02 +08:00

1157 lines
46 KiB
JavaScript

/**
* scripts/harmony.patch.js 单元测试
*
* 跑法:
* node --test tests/harmony-patch.test.js
*
* 测试覆盖:
* - parseSiteConfig: 解析 site.js 提取 baseUrl + uniacid
* - normalizeBaseUrlForPlaceholder: 规范化 baseUrl (去尾斜杠/补协议)
* - replacePlaceholders: 递归替换 YOUR_DOMAIN / YOUR_UNIACID
* - stripJsonComments: 鲁棒 JSONC 注释剥离 (跨字符串字面量)
* - 端到端: 从 site.js + manifest.json 生成最终 app.json 隐私配置
*
* 实现策略 (跟 util.test.js / lang.test.js 同款):
* harmony.patch.js 顶层直接调 main(), 无法直接 require.
* 这里把要测的纯函数从源码"剪切"到本测试文件顶部"被测源码区块",
* 跟原文件保持同步, 跑得快又不依赖任何运行时.
*
* 业务背景:
* 鸿蒙元服务上架审核需要从 app.json 读 privacy.privacyContractUrl.
* manifest.json 用占位符 YOUR_DOMAIN/YOUR_UNIACID, 真实值在 site.js.
* 构建时 syncPrivacyToStub() 把占位符替换成真实值, 写入 stub app.json.
* 任何中间环节 (解析 / 规范化 / 替换) 错一个字符, 整条 URL 就废了.
*/
'use strict';
const test = require('node:test');
const assert = require('node:assert/strict');
// =============================================================
// 被测源码区块 (与 scripts/harmony.patch.js 保持同步)
// =============================================================
/**
* 鲁棒的 JSONC 注释剥离 (跳过字符串字面量, 只剥真正的注释)
* @see harmony.patch.js#stripJsonComments
*/
function stripJsonComments(input) {
let out = '';
let i = 0;
const n = input.length;
while (i < n) {
const c = input[i];
const next = i + 1 < n ? input[i + 1] : '';
if (c === '/' && next === '*') {
i += 2;
while (i < n && !(input[i] === '*' && input[i + 1] === '/')) i++;
i += 2;
continue;
}
if (c === '/' && next === '/') {
i += 2;
while (i < n && input[i] !== '\n') i++;
continue;
}
if (c === '"') {
out += c;
i++;
while (i < n) {
const sc = input[i];
if (sc === '\\') {
out += sc + (input[i + 1] || '');
i += 2;
continue;
}
out += sc;
i++;
if (sc === '"') break;
}
continue;
}
out += c;
i++;
}
return out;
}
/**
* 从 site.js 提取 baseUrl + uniacid
* @see harmony.patch.js#parseSiteConfig
*/
function parseSiteConfig(siteJsSource) {
const baseUrlMatch = siteJsSource.match(/baseUrl\s*:\s*['"]([^'"]+)['"]/)
const uniacidMatch = siteJsSource.match(/uniacid\s*:\s*['"]?(\d+)['"]?/)
if (!baseUrlMatch || !uniacidMatch) {
return null
}
return {
baseUrl: baseUrlMatch[1],
uniacid: uniacidMatch[1],
}
}
/**
* 把 baseUrl 规范化为"协议://host (无尾斜杠)"
* @see harmony.patch.js#normalizeBaseUrlForPlaceholder
*/
function normalizeBaseUrlForPlaceholder(baseUrl) {
let url = baseUrl.replace(/\/+$/, '')
if (!/^https?:\/\//i.test(url)) {
url = 'https://' + url
}
return url
}
/**
* 递归替换隐私配置对象中的占位符
* @see harmony.patch.js#replacePlaceholders
*/
function replacePlaceholders(value, siteConfig) {
if (typeof value === 'string') {
return value
.replace(/YOUR_DOMAIN/g, siteConfig.baseUrl)
.replace(/YOUR_UNIACID/g, siteConfig.uniacid)
}
if (Array.isArray(value)) {
return value.map(v => replacePlaceholders(v, siteConfig))
}
if (value !== null && typeof value === 'object') {
const out = {}
for (const k of Object.keys(value)) {
out[k] = replacePlaceholders(value[k], siteConfig)
}
return out
}
return value
}
// =============================================================
// 1. parseSiteConfig: 提取 baseUrl + uniacid
// =============================================================
test('parseSiteConfig: 提取标准 site.js 的 baseUrl + uniacid', () => {
const src = `module.exports = {
baseUrl: "https://xcx.aigc-quickapp.com/",//修改域名
uniacid: 2482,//后台对应uniacid
};`
const result = parseSiteConfig(src)
assert.equal(result.baseUrl, 'https://xcx.aigc-quickapp.com/')
assert.equal(result.uniacid, '2482')
})
test('parseSiteConfig: 单引号字符串也支持', () => {
const src = `module.exports = { baseUrl: 'http://test.com', uniacid: "100" };`
const result = parseSiteConfig(src)
assert.equal(result.baseUrl, 'http://test.com')
assert.equal(result.uniacid, '100')
})
test('parseSiteConfig: uniacid 字符串形式 (带引号) 也支持', () => {
const src = `module.exports = { baseUrl: "https://x.com", uniacid: "42" };`
const result = parseSiteConfig(src)
assert.equal(result.uniacid, '42')
})
test('parseSiteConfig: uniacid 是数字 (不带引号)', () => {
const src = `module.exports = { baseUrl: "https://x.com", uniacid: 42 };`
const result = parseSiteConfig(src)
assert.equal(result.uniacid, '42')
})
test('parseSiteConfig: 缺 baseUrl 返回 null', () => {
const src = `module.exports = { uniacid: 1 };`
assert.equal(parseSiteConfig(src), null)
})
test('parseSiteConfig: 缺 uniacid 返回 null', () => {
const src = `module.exports = { baseUrl: "https://x.com" };`
assert.equal(parseSiteConfig(src), null)
})
test('parseSiteConfig: 多个 baseUrl 时取第一个', () => {
// 容错: 万一其他字段也叫 baseUrl, 不应影响解析
const src = `module.exports = { baseUrl: "https://first.com", uniacid: 1 };`
const result = parseSiteConfig(src)
assert.equal(result.baseUrl, 'https://first.com')
})
// =============================================================
// 2. normalizeBaseUrlForPlaceholder: 规范化域名
// =============================================================
test('normalize: 带尾斜杠的 https URL → 去掉尾斜杠', () => {
assert.equal(
normalizeBaseUrlForPlaceholder('https://xcx2.aigc-quickapp.com/'),
'https://xcx2.aigc-quickapp.com'
)
})
test('normalize: 多个尾斜杠全部去掉', () => {
assert.equal(
normalizeBaseUrlForPlaceholder('https://xcx2.aigc-quickapp.com///'),
'https://xcx2.aigc-quickapp.com'
)
})
test('normalize: http 也支持', () => {
assert.equal(
normalizeBaseUrlForPlaceholder('http://test.com/'),
'http://test.com'
)
})
test('normalize: 缺协议自动补 https://', () => {
assert.equal(
normalizeBaseUrlForPlaceholder('xcx2.aigc-quickapp.com/'),
'https://xcx2.aigc-quickapp.com'
)
})
test('normalize: 缺协议且无尾斜杠', () => {
assert.equal(
normalizeBaseUrlForPlaceholder('xcx2.aigc-quickapp.com'),
'https://xcx2.aigc-quickapp.com'
)
})
test('normalize: 已是规范形式, 不修改', () => {
assert.equal(
normalizeBaseUrlForPlaceholder('https://xcx2.aigc-quickapp.com'),
'https://xcx2.aigc-quickapp.com'
)
})
test('normalize: https 大小写不敏感', () => {
assert.equal(
normalizeBaseUrlForPlaceholder('HTTPS://test.com/'),
'HTTPS://test.com'
)
})
// =============================================================
// 3. replacePlaceholders: 递归替换占位符
// =============================================================
test('replace: 字符串内单个 YOUR_DOMAIN', () => {
assert.equal(
replacePlaceholders('YOUR_DOMAIN/api/x', { baseUrl: 'https://x.com', uniacid: '1' }),
'https://x.com/api/x'
)
})
test('replace: 字符串内单个 YOUR_UNIACID', () => {
assert.equal(
replacePlaceholders('?uniacid=YOUR_UNIACID', { baseUrl: 'https://x.com', uniacid: '42' }),
'?uniacid=42'
)
})
test('replace: 字符串内同时有 YOUR_DOMAIN + YOUR_UNIACID (协议头在 baseUrl 上)', () => {
// 约定: manifest.json 模板里 YOUR_DOMAIN 不带协议头 (避免双协议)
// site.js 的 baseUrl 永远是完整 URL (含 https://)
assert.equal(
replacePlaceholders('YOUR_DOMAIN/api/x?uniacid=YOUR_UNIACID', {
baseUrl: 'https://x.com', uniacid: '99'
}),
'https://x.com/api/x?uniacid=99'
)
})
test('replace(双协议 bug 演示): 模板里带协议头会重复, 这是约定违反', () => {
// 反例: 模板里写了 https://YOUR_DOMAIN 就会变 https://https://x.com
// 正确做法: 模板里永远写 YOUR_DOMAIN, 由 baseUrl 提供协议头
// 本测试**故意**不通过, 提示开发者不要这样写
const result = replacePlaceholders('https://YOUR_DOMAIN/api/x', {
baseUrl: 'https://x.com', uniacid: '99'
})
// 这里我们演示 bug 行为, 不用 assert.equal, 只输出现状
console.log(' [双协议 bug 演示] 模板 https://YOUR_DOMAIN + baseUrl https://x.com →', result)
assert.match(result, /^https:\/\/https:/, '如果模板带协议头, 会出现双协议 (这是约定违反)')
})
test('replace: 同一字符串多个 YOUR_DOMAIN 全部替换', () => {
assert.equal(
replacePlaceholders('YOUR_DOMAIN/api?from=YOUR_DOMAIN', {
baseUrl: 'https://x.com', uniacid: '1'
}),
'https://x.com/api?from=https://x.com'
)
})
test('replace: 对象递归替换', () => {
const input = {
privacyContractUrl: 'YOUR_DOMAIN/api/privacy?uniacid=YOUR_UNIACID',
serviceContractUrl: 'YOUR_DOMAIN/api/service?uniacid=YOUR_UNIACID',
privacyContractName: { zh: '隐私', en: 'Privacy' },
}
const out = replacePlaceholders(input, { baseUrl: 'https://x.com', uniacid: '5' })
assert.equal(out.privacyContractUrl, 'https://x.com/api/privacy?uniacid=5')
assert.equal(out.serviceContractUrl, 'https://x.com/api/service?uniacid=5')
assert.equal(out.privacyContractName.zh, '隐私')
})
test('replace: 数组递归替换', () => {
const input = ['YOUR_DOMAIN/1', 'YOUR_DOMAIN/2']
const out = replacePlaceholders(input, { baseUrl: 'https://x.com', uniacid: '1' })
assert.deepEqual(out, ['https://x.com/1', 'https://x.com/2'])
})
test('replace: 嵌套对象 + 数组混合', () => {
const input = {
urls: ['YOUR_DOMAIN/a', 'YOUR_DOMAIN/b?u=YOUR_UNIACID'],
meta: { en: 'Privacy at YOUR_DOMAIN' }
}
const out = replacePlaceholders(input, { baseUrl: 'https://x.com', uniacid: '7' })
assert.equal(out.urls[0], 'https://x.com/a')
assert.equal(out.urls[1], 'https://x.com/b?u=7')
assert.equal(out.meta.en, 'Privacy at https://x.com')
})
test('replace: 不修改原对象 (无副作用)', () => {
const input = { url: 'YOUR_DOMAIN' }
const before = JSON.stringify(input)
replacePlaceholders(input, { baseUrl: 'https://x.com', uniacid: '1' })
assert.equal(JSON.stringify(input), before, '原对象不能被修改')
})
test('replace: 非字符串基本类型保持原样', () => {
assert.equal(replacePlaceholders(42, { baseUrl: 'x', uniacid: '1' }), 42)
assert.equal(replacePlaceholders(true, { baseUrl: 'x', uniacid: '1' }), true)
assert.equal(replacePlaceholders(null, { baseUrl: 'x', uniacid: '1' }), null)
})
test('replace: 字符串里没有占位符时原样返回', () => {
assert.equal(
replacePlaceholders('plain string', { baseUrl: 'x', uniacid: '1' }),
'plain string'
)
})
// =============================================================
// 4. stripJsonComments: 鲁棒 JSONC 解析
// =============================================================
test('stripJsonComments: 移除 /* ... */ 块注释', () => {
const out = stripJsonComments('/* 头部注释 */\n{"a": 1}')
assert.doesNotMatch(out, /\/\*/)
assert.match(out, /"a":\s*1/)
})
test('stripJsonComments: 移除 // 行注释', () => {
const out = stripJsonComments('{"a": 1} // 尾部注释\n')
assert.doesNotMatch(out, /\/\//)
assert.match(out, /"a":\s*1/)
})
test('stripJsonComments: 保留字符串字面量里的 //', () => {
// 关键: 注释剥离不能跨过字符串字面量
// 这里"https://"含 //, 不应被当成注释
const out = stripJsonComments('{"url": "https://x.com"}')
assert.match(out, /"https:\/\/x\.com"/)
})
test('stripJsonComments: 保留字符串字面量里的 /*', () => {
// 注释里若提到"/* xxx */", 不应破坏 JSON
const out = stripJsonComments('{"comment": "/* not a comment */"}')
// 解析后 JSON.parse 应该 OK
const j = JSON.parse(out)
assert.equal(j.comment, '/* not a comment */')
})
test('stripJsonComments: 处理转义字符串', () => {
const out = stripJsonComments('{"a": "say \\"hi\\""}')
const j = JSON.parse(out)
assert.equal(j.a, 'say "hi"')
})
test('stripJsonComments: 多个块注释', () => {
const out = stripJsonComments('/* c1 */ {"a":1} /* c2 */')
const j = JSON.parse(out)
assert.equal(j.a, 1)
})
// =============================================================
// 5. 端到端: site.js + manifest.json → 最终 app.json privacy
// =============================================================
test('端到端: 用 site.js + manifest.json 模板生成最终 privacy 配置', () => {
// 模拟 site.js
const siteJsSource = `module.exports = {
baseUrl: "https://xcx2.aigc-quickapp.com/",//修改域名
uniacid: 2482,//后台对应uniacid
};`
// 模拟 manifest.json 隐私配置
const manifestSource = `{
"mp-harmony": {
"privacy": {
"privacyContractUrl": "YOUR_DOMAIN/api/agreementview/privacy?uniacid=YOUR_UNIACID",
"serviceContractUrl": "YOUR_DOMAIN/api/agreementview/service?uniacid=YOUR_UNIACID",
"privacyContractName": { "zh": "隐私政策", "en": "Privacy Policy" },
"serviceContractName": { "zh": "用户服务协议", "en": "User Service Agreement" }
}
}
}`
// 1. 解析 site.js
const siteConfigRaw = parseSiteConfig(siteJsSource)
assert.ok(siteConfigRaw, 'site.js 解析成功')
const siteConfig = {
baseUrl: normalizeBaseUrlForPlaceholder(siteConfigRaw.baseUrl),
uniacid: siteConfigRaw.uniacid,
}
assert.equal(siteConfig.baseUrl, 'https://xcx2.aigc-quickapp.com')
assert.equal(siteConfig.uniacid, '2482')
// 2. 解析 manifest.json (含占位符的隐私配置)
const manifest = JSON.parse(stripJsonComments(manifestSource))
const privacyConfigTemplate = manifest['mp-harmony'].privacy
// 3. 替换占位符
const finalPrivacy = replacePlaceholders(privacyConfigTemplate, siteConfig)
// 4. 校验结果
assert.equal(
finalPrivacy.privacyContractUrl,
'https://xcx2.aigc-quickapp.com/api/agreementview/privacy?uniacid=2482'
)
assert.equal(
finalPrivacy.serviceContractUrl,
'https://xcx2.aigc-quickapp.com/api/agreementview/service?uniacid=2482'
)
// 名字字段不变
assert.equal(finalPrivacy.privacyContractName.zh, '隐私政策')
assert.equal(finalPrivacy.privacyContractName.en, 'Privacy Policy')
// 5. 不残留占位符
const finalStr = JSON.stringify(finalPrivacy)
assert.doesNotMatch(finalStr, /YOUR_DOMAIN/)
assert.doesNotMatch(finalStr, /YOUR_UNIACID/)
})
test('端到端: 多个 uniacid 切换 (不同 site.js)', () => {
// 同一份 manifest.json 模板, 不同 site.js → 不同的最终 app.json
const template = {
privacyContractUrl: 'YOUR_DOMAIN/api/privacy?uniacid=YOUR_UNIACID',
}
const siteConfigs = [
{ baseUrl: 'https://xcx.aigc-quickapp.com/', uniacid: '1' },
{ baseUrl: 'https://xcx2.aigc-quickapp.com/', uniacid: '2482' },
{ baseUrl: 'https://test.aigc-quickapp.com/', uniacid: '100' },
]
for (const raw of siteConfigs) {
const siteConfig = {
baseUrl: normalizeBaseUrlForPlaceholder(raw.baseUrl),
uniacid: raw.uniacid,
}
const out = replacePlaceholders(template, siteConfig)
const expected = `${siteConfig.baseUrl}/api/privacy?uniacid=${siteConfig.uniacid}`
assert.equal(out.privacyContractUrl, expected,
`siteConfig=${JSON.stringify(siteConfig)} → URL 应该是 ${expected}`)
}
})
test('端到端: baseUrl 带尾斜杠不产生双斜杠 bug', () => {
// 关键回归: site.js 的 baseUrl 通常带尾斜杠 ("https://x.com/")
// 模板里占位符出现在 URL 中间 ("YOUR_DOMAIN/api/..."),
// 不能产生 "https://x.com//api/..." 这种双斜杠
// (前提: 模板里没有重复写 https://, 协议头只在 baseUrl)
const template = 'YOUR_DOMAIN/api/privacy'
const siteConfig = {
baseUrl: normalizeBaseUrlForPlaceholder('https://x.com/'),
uniacid: '1'
}
const out = replacePlaceholders(template, siteConfig)
assert.equal(out, 'https://x.com/api/privacy', '不能有双斜杠')
// 验证 "://" 只出现一次 (除 query 里的 :// 编码外)
const protocolCount = (out.match(/:\/\//g) || []).length
assert.equal(protocolCount, 1, ':// 协议头只出现 1 次')
})
// =============================================================
// 6. 异常路径: parseSiteConfig 失败 / site.js 缺字段
// =============================================================
test('异常: site.js 是空字符串, parseSiteConfig 返回 null', () => {
assert.equal(parseSiteConfig(''), null)
})
test('异常: site.js 是注释, parseSiteConfig 返回 null', () => {
assert.equal(parseSiteConfig('// only comment'), null)
})
test('异常: site.js 完全没有 baseUrl/uniacid, parseSiteConfig 返回 null', () => {
const src = `module.exports = { foo: "bar" };`
assert.equal(parseSiteConfig(src), null)
})
test('异常: site.js uniacid 是非数字字符串, regex 仍能匹配数字部分', () => {
// 容错: 万一 site.js 写错了 uniacid = "abc", regex 不匹配
// 应该返回 null (而不是解析出空 uniacid)
const src = `module.exports = { baseUrl: "https://x.com", uniacid: "abc" };`
assert.equal(parseSiteConfig(src), null)
})
// =============================================================
// 7. AppJsonVerificationError: 自定义错误
// =============================================================
/**
* @see harmony.patch.js#AppJsonVerificationError
*/
class AppJsonVerificationError extends Error {
constructor(messages) {
super('app.json 写后验证失败:\n - ' + messages.join('\n - '))
this.name = 'AppJsonVerificationError'
this.messages = messages
}
}
// =============================================================
// 8. verifyWrittenAppJson: 写后验证
// =============================================================
/**
* @see harmony.patch.js#verifyWrittenAppJson
* 注意: 这是从 harmony.patch.js 复制的纯函数, 跑测试时不需要写文件
* (传入内存中的对象即可), 避免污染 stub app.json
*/
async function verifyWrittenAppJsonForTest(writtenObj, siteConfig) {
const errors = []
const privacy = writtenObj.privacy
if (!privacy || typeof privacy !== 'object') {
throw new AppJsonVerificationError(['privacy 块缺失或不是对象'])
}
const urlFields = ['privacyContractUrl', 'serviceContractUrl']
for (const field of urlFields) {
const url = privacy[field]
if (typeof url !== 'string') {
errors.push(`${field} 缺失或不是字符串 (实际: ${typeof url})`)
continue
}
if (/YOUR_DOMAIN|YOUR_UNIACID/.test(url)) {
errors.push(`${field} 仍含占位符: ${url}`)
}
if (/^https:\/\/https:|^http:\/\/http:/i.test(url)) {
errors.push(`${field} 含双协议头: ${url}`)
}
const afterProtocol = url.replace(/^https?:\/\//i, '')
// 检查双斜杠 bug: 域名后立即出现 / (即 // 协议后)
// 关键: 检测字符串里出现 "//" 即可, 但要避免误报 query 里的 //
// 简单判断: 截到第一个 ? 或 # 之前, 检查是否含 //
const queryStart = afterProtocol.search(/[?#]/)
const beforeQuery = queryStart === -1 ? afterProtocol : afterProtocol.slice(0, queryStart)
if (beforeQuery.includes('//')) {
errors.push(`${field} 路径部分含双斜杠 (双斜杠 bug): ${url}`)
}
const expectedPrefix = siteConfig.baseUrl + '/'
if (!url.startsWith(expectedPrefix)) {
errors.push(`${field} 不是以 "${expectedPrefix}" 开头 (实际: ${url.slice(0, expectedPrefix.length + 10)})`)
}
const uniacidMatch = url.match(/[?&]uniacid=([^&]+)/)
if (!uniacidMatch) {
errors.push(`${field} 缺少 uniacid= 参数: ${url}`)
} else if (uniacidMatch[1] !== siteConfig.uniacid) {
errors.push(`${field} 的 uniacid=${uniacidMatch[1]} 不等于 site.js 的 uniacid=${siteConfig.uniacid}`)
}
}
const nameFields = ['privacyContractName', 'serviceContractName']
for (const field of nameFields) {
const name = privacy[field]
if (!name || typeof name !== 'object' || !name.zh || !name.en) {
errors.push(`${field} 结构损坏 (应有 zh/en): ${JSON.stringify(name)}`)
}
}
if (errors.length > 0) {
throw new AppJsonVerificationError(errors)
}
}
test('verify: 正确写入的 app.json 通过验证', async () => {
const siteConfig = {
baseUrl: 'https://xcx2.aigc-quickapp.com',
uniacid: '2482',
}
const writtenObj = {
privacy: {
privacyContractUrl: 'https://xcx2.aigc-quickapp.com/api/agreementview/privacy?uniacid=2482',
serviceContractUrl: 'https://xcx2.aigc-quickapp.com/api/agreementview/service?uniacid=2482',
privacyContractName: { zh: '隐私政策', en: 'Privacy Policy' },
serviceContractName: { zh: '用户服务协议', en: 'User Service Agreement' },
}
}
// 应当不抛错
await verifyWrittenAppJsonForTest(writtenObj, siteConfig)
})
test('verify: 占位符没替换完 (URL 含 YOUR_DOMAIN) → 抛错', async () => {
const siteConfig = { baseUrl: 'https://x.com', uniacid: '1' }
const writtenObj = {
privacy: {
privacyContractUrl: 'https://YOUR_DOMAIN/api/privacy?uniacid=1', // 占位符没替换
serviceContractUrl: 'https://x.com/api/service?uniacid=1',
privacyContractName: { zh: 'a', en: 'b' },
serviceContractName: { zh: 'a', en: 'b' },
}
}
await assert.rejects(
verifyWrittenAppJsonForTest(writtenObj, siteConfig),
AppJsonVerificationError
)
})
// =============================================================
// 11. 端到端 ZIP 场景: 主流程在 ZIP 前同步构建产物
// =============================================================
//
// 背景:
// harmony.patch.js 的 main() 流程:
// 1. syncSiteToStub
// 2. syncPrivacyToStub → 写 stub ascf_src/app.json
// 3. copyAssets
// 4. (新增) syncPrivacyToPath(build app.json) ← 关键! 必须在 ZIP 前
// 5. createZipWithSystemCommand(unpackage/dist/build/.mp-harmony)
//
// 第 4 步确保 ZIP 包里的 app.json 也是真实值, 不是占位符。
test('端到端 ZIP 场景: 模拟主流程, ZIP 前 build app.json 已被同步', async () => {
const fs = require('fs')
const path = require('path')
const os = require('os')
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'harmony-zip-'))
const stubAppJson = path.join(tmpDir, 'stub', 'app.json')
const buildAppJson = path.join(tmpDir, 'build', 'app.json')
const siteJsPath = path.join(tmpDir, 'site.js')
const manifestPath = path.join(tmpDir, 'manifest.json')
// 准备 site.js
fs.writeFileSync(siteJsPath, `module.exports = {
baseUrl: "https://xcx2.aigc-quickapp.com/",
uniacid: 2482
};`)
fs.writeFileSync(manifestPath, `{
"mp-harmony": {
"privacy": {
"privacyContractUrl": "YOUR_DOMAIN/api/agreementview/privacy?uniacid=YOUR_UNIACID",
"serviceContractUrl": "YOUR_DOMAIN/api/agreementview/service?uniacid=YOUR_UNIACID",
"privacyContractName": { "zh": "隐私政策", "en": "Privacy Policy" },
"serviceContractName": { "zh": "用户服务协议", "en": "User Service Agreement" }
}
}
}`)
// 准备 stub app.json (HBuilderX 之外的源, 已经被 syncPrivacyToStub 同步过)
fs.mkdirSync(path.dirname(stubAppJson), { recursive: true })
fs.writeFileSync(stubAppJson, `{
"privacy": {
"privacyContractUrl": "https://xcx2.aigc-quickapp.com/api/agreementview/privacy?uniacid=2482",
"serviceContractUrl": "https://xcx2.aigc-quickapp.com/api/agreementview/service?uniacid=2482",
"privacyContractName": { "zh": "隐私政策", "en": "Privacy Policy" },
"serviceContractName": { "zh": "用户服务协议", "en": "User Service Agreement" }
}
}`)
// 准备构建产物 app.json (HBuilderX 产物, 含占位符 - 这是同步前的污染状态)
fs.mkdirSync(path.dirname(buildAppJson), { recursive: true })
fs.writeFileSync(buildAppJson, `{
"privacy": {
"privacyContractUrl": "YOUR_DOMAIN/api/agreementview/privacy?uniacid=YOUR_UNIACID",
"serviceContractUrl": "YOUR_DOMAIN/api/agreementview/service?uniacid=YOUR_UNIACID",
"privacyContractName": { "zh": "隐私政策", "en": "Privacy Policy" },
"serviceContractName": { "zh": "用户服务协议", "en": "User Service Agreement" }
}
}`)
// 模拟主流程: 步骤 4 同步 build 产物 (在 ZIP 前)
function simulateSyncToBuild() {
const manifest = JSON.parse(stripJsonComments(fs.readFileSync(manifestPath, 'utf8')))
const template = manifest['mp-harmony'].privacy
const siteConfigRaw = parseSiteConfig(fs.readFileSync(siteJsPath, 'utf8'))
const siteConfig = {
baseUrl: normalizeBaseUrlForPlaceholder(siteConfigRaw.baseUrl),
uniacid: siteConfigRaw.uniacid,
}
const finalPrivacy = replacePlaceholders(template, siteConfig)
const appJson = JSON.parse(fs.readFileSync(buildAppJson, 'utf8'))
appJson.privacy = finalPrivacy
fs.writeFileSync(buildAppJson, JSON.stringify(appJson, null, 2) + '\n', 'utf8')
}
simulateSyncToBuild()
// 验证: 构建产物的 app.json 现在是真实值
const finalBuild = JSON.parse(fs.readFileSync(buildAppJson, 'utf8'))
assert.equal(
finalBuild.privacy.privacyContractUrl,
'https://xcx2.aigc-quickapp.com/api/agreementview/privacy?uniacid=2482',
'ZIP 前 build 产物 app.json 必须是真实值 (否则发行 ZIP 仍是占位符)'
)
assert.equal(
finalBuild.privacy.serviceContractUrl,
'https://xcx2.aigc-quickapp.com/api/agreementview/service?uniacid=2482'
)
fs.rmSync(tmpDir, { recursive: true, force: true })
})
test('端到端 ZIP 场景: 没有 ZIP 前同步, ZIP 包里仍是占位符 (回归)', async () => {
// 这个测试**故意**展示: 如果主流程没有在 ZIP 前 syncPrivacyToPath,
// 最终 ZIP 包里的 app.json 仍是占位符
const fs = require('fs')
const path = require('path')
const os = require('os')
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'harmony-zip-regress-'))
const buildAppJson = path.join(tmpDir, 'build', 'app.json')
const siteJsPath = path.join(tmpDir, 'site.js')
const manifestPath = path.join(tmpDir, 'manifest.json')
fs.writeFileSync(siteJsPath, `module.exports = { baseUrl: "https://x.com", uniacid: 1 };`)
fs.writeFileSync(manifestPath, `{
"mp-harmony": {
"privacy": {
"privacyContractUrl": "YOUR_DOMAIN/api/privacy?uniacid=YOUR_UNIACID",
"serviceContractUrl": "YOUR_DOMAIN/api/service?uniacid=YOUR_UNIACID",
"privacyContractName": { "zh": "a", "en": "b" },
"serviceContractName": { "zh": "a", "en": "b" }
}
}
}`)
fs.mkdirSync(path.dirname(buildAppJson), { recursive: true })
fs.writeFileSync(buildAppJson, `{
"privacy": {
"privacyContractUrl": "YOUR_DOMAIN/api/privacy?uniacid=YOUR_UNIACID",
"serviceContractUrl": "YOUR_DOMAIN/api/service?uniacid=YOUR_UNIACID"
}
}`)
// 故意不调用 syncPrivacyToPath (这是修复前的 bug 行为)
// 直接检查 build app.json 仍是占位符
const beforeZip = JSON.parse(fs.readFileSync(buildAppJson, 'utf8'))
assert.match(beforeZip.privacy.privacyContractUrl, /YOUR_DOMAIN/,
'回归测试: 没有 ZIP 前同步, build app.json 仍是占位符 (这正是修复前的 bug)')
fs.rmSync(tmpDir, { recursive: true, force: true })
})
// =============================================================
// 10. HBuilderX 发行场景: vue.config.js _copyMpHarmonyBuild 的覆盖
// =============================================================
//
// 背景 (重现 "HBuilderX 发行后 app.json 仍是占位符" bug):
// HBuilderX 发行 mp-harmony 时, vue.config.js 执行顺序:
// 1. execSync('node scripts/harmony.patch.js --mode production')
// → syncPrivacyToStub: 写入真实值到 stub app.json
// 2. _copyMpHarmonyBuild(): 用 unpackage/dist/build/.mp-harmony/app.json
// (含占位符) 覆盖 stub ascf_src/app.json
// 3. [本次修复] execSync('node scripts/harmony.patch.js --sync-privacy-only')
// → 重新跑 syncPrivacyToStub, 把被覆盖的占位符再次替换为真实值
//
// 这个测试模拟完整流程, 验证最终 app.json 是正确值
test('HBuilderX 发行场景: 完整 3 步流程后, app.json 是真实值', async () => {
const fs = require('fs')
const path = require('path')
const os = require('os')
// 1. 创建临时目录结构模拟:
// <tmp>/stub/ascf_src/app.json
// <tmp>/build/.mp-harmony/app.json
// <tmp>/site.js
// <tmp>/manifest.json
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'harmony-patch-test-'))
const stubAppJson = path.join(tmpDir, 'stub', 'ascf_src', 'app.json')
const buildAppJson = path.join(tmpDir, 'build', '.mp-harmony', 'app.json')
const siteJsPath = path.join(tmpDir, 'site.js')
const manifestPath = path.join(tmpDir, 'manifest.json')
// 准备 site.js
fs.writeFileSync(siteJsPath, `module.exports = {
baseUrl: "https://xcx2.aigc-quickapp.com/",
uniacid: 2482
};`)
// 准备 manifest.json (含占位符)
fs.writeFileSync(manifestPath, `{
"mp-harmony": {
"privacy": {
"privacyContractUrl": "YOUR_DOMAIN/api/agreementview/privacy?uniacid=YOUR_UNIACID",
"serviceContractUrl": "YOUR_DOMAIN/api/agreementview/service?uniacid=YOUR_UNIACID",
"privacyContractName": { "zh": "隐私政策", "en": "Privacy Policy" },
"serviceContractName": { "zh": "用户服务协议", "en": "User Service Agreement" }
}
}
}`)
// 准备 stub ascf_src/app.json (有占位符版的 privacy)
fs.mkdirSync(path.dirname(stubAppJson), { recursive: true })
fs.writeFileSync(stubAppJson, `{
"pages": ["pages/index/index"],
"privacy": {
"privacyContractUrl": "YOUR_DOMAIN/api/agreementview/privacy?uniacid=YOUR_UNIACID",
"serviceContractUrl": "YOUR_DOMAIN/api/agreementview/service?uniacid=YOUR_UNIACID",
"privacyContractName": { "zh": "隐私政策", "en": "Privacy Policy" },
"serviceContractName": { "zh": "用户服务协议", "en": "User Service Agreement" }
}
}`)
// 准备构建产物 .mp-harmony/app.json (也有占位符, 模拟 HBuilderX 产物)
fs.mkdirSync(path.dirname(buildAppJson), { recursive: true })
fs.writeFileSync(buildAppJson, `{
"pages": ["pages/index/index"],
"privacy": {
"privacyContractUrl": "YOUR_DOMAIN/api/agreementview/privacy?uniacid=YOUR_UNIACID",
"serviceContractUrl": "YOUR_DOMAIN/api/agreementview/service?uniacid=YOUR_UNIACID"
}
}`)
// 模拟步骤 1: harmony.patch.js 第一次跑 (syncPrivacyToStub)
// 直接复用纯函数, 模拟 syncPrivacyToStub 的核心逻辑
function simulateSyncPrivacy(manifestPath, appJsonPath, siteJsPath) {
const manifest = JSON.parse(stripJsonComments(fs.readFileSync(manifestPath, 'utf8')))
const template = manifest['mp-harmony'].privacy
const siteConfigRaw = parseSiteConfig(fs.readFileSync(siteJsPath, 'utf8'))
const siteConfig = {
baseUrl: normalizeBaseUrlForPlaceholder(siteConfigRaw.baseUrl),
uniacid: siteConfigRaw.uniacid,
}
const finalPrivacy = replacePlaceholders(template, siteConfig)
const appJson = JSON.parse(fs.readFileSync(appJsonPath, 'utf8'))
appJson.privacy = finalPrivacy
fs.writeFileSync(appJsonPath, JSON.stringify(appJson, null, 2) + '\n', 'utf8')
return { appJsonPath, siteConfig }
}
simulateSyncPrivacy(manifestPath, stubAppJson, siteJsPath)
// 步骤 1 后: stub app.json 应该是真实值
let after1 = JSON.parse(fs.readFileSync(stubAppJson, 'utf8'))
assert.match(after1.privacy.privacyContractUrl, /xcx2\.aigc-quickapp\.com/)
// 模拟步骤 2: _copyMpHarmonyBuild() 覆盖
// 简化: 直接把 build 产物里整个 app.json 拷过去
fs.copyFileSync(buildAppJson, stubAppJson)
// 步骤 2 后: stub app.json 又是占位符 (HBuilderX 产物的污染)
let after2 = JSON.parse(fs.readFileSync(stubAppJson, 'utf8'))
assert.match(after2.privacy.privacyContractUrl, /YOUR_DOMAIN/,
'步骤 2 后, _copyMpHarmonyBuild 把 app.json 覆盖回占位符版 (这是 bug 现场)')
// 模拟步骤 3: --sync-privacy-only 子命令再跑一次
simulateSyncPrivacy(manifestPath, stubAppJson, siteJsPath)
let after3 = JSON.parse(fs.readFileSync(stubAppJson, 'utf8'))
// 步骤 3 后: 必须是真实值
assert.equal(after3.privacy.privacyContractUrl,
'https://xcx2.aigc-quickapp.com/api/agreementview/privacy?uniacid=2482',
'步骤 3 后, privacy 必须是真实值 (这是 --sync-privacy-only 修复的核心)')
assert.equal(after3.privacy.serviceContractUrl,
'https://xcx2.aigc-quickapp.com/api/agreementview/service?uniacid=2482')
assert.equal(after3.privacy.privacyContractName.zh, '隐私政策')
assert.equal(after3.privacy.privacyContractName.en, 'Privacy Policy')
// 关键: name 字段必须保留 (步骤 1 写了, 步骤 2 覆盖时 build 产物没 name, 步骤 3 必须再次写入)
assert.ok(after3.privacy.serviceContractName, 'serviceContractName 必须存在')
assert.equal(after3.privacy.serviceContractName.zh, '用户服务协议')
// 清理临时目录
fs.rmSync(tmpDir, { recursive: true, force: true })
})
test('HBuilderX 发行场景: 步骤 1 + 步骤 2 都不调步骤 3, 永远会留下占位符 (回归)', async () => {
// 这个测试**故意**失败, 用来证明:
// 如果 vue.config.js 不调用 --sync-privacy-only, 步骤 2 会覆盖步骤 1 的结果
// 也就是说这个回归必须由 vue.config.js 调用步骤 3 修复
const fs = require('fs')
const path = require('path')
const os = require('os')
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'harmony-regression-'))
const stubAppJson = path.join(tmpDir, 'app.json')
const buildAppJson = path.join(tmpDir, 'build-app.json')
const siteJsPath = path.join(tmpDir, 'site.js')
const manifestPath = path.join(tmpDir, 'manifest.json')
fs.writeFileSync(siteJsPath, `module.exports = { baseUrl: "https://x.com", uniacid: 1 };`)
fs.writeFileSync(manifestPath, `{
"mp-harmony": {
"privacy": {
"privacyContractUrl": "YOUR_DOMAIN/api/privacy?uniacid=YOUR_UNIACID",
"serviceContractUrl": "YOUR_DOMAIN/api/service?uniacid=YOUR_UNIACID",
"privacyContractName": { "zh": "a", "en": "b" },
"serviceContractName": { "zh": "a", "en": "b" }
}
}
}`)
function simulateSync() {
const manifest = JSON.parse(stripJsonComments(fs.readFileSync(manifestPath, 'utf8')))
const template = manifest['mp-harmony'].privacy
const siteConfigRaw = parseSiteConfig(fs.readFileSync(siteJsPath, 'utf8'))
const siteConfig = {
baseUrl: normalizeBaseUrlForPlaceholder(siteConfigRaw.baseUrl),
uniacid: siteConfigRaw.uniacid,
}
const finalPrivacy = replacePlaceholders(template, siteConfig)
const appJson = JSON.parse(fs.readFileSync(stubAppJson, 'utf8'))
appJson.privacy = finalPrivacy
fs.writeFileSync(stubAppJson, JSON.stringify(appJson, null, 2) + '\n', 'utf8')
}
// stub app.json 初始 (含占位符)
fs.writeFileSync(stubAppJson, `{
"privacy": {
"privacyContractUrl": "YOUR_DOMAIN/api/privacy?uniacid=YOUR_UNIACID",
"serviceContractUrl": "YOUR_DOMAIN/api/service?uniacid=YOUR_UNIACID",
"privacyContractName": { "zh": "a", "en": "b" },
"serviceContractName": { "zh": "a", "en": "b" }
}
}`)
// 构建产物 (含占位符)
fs.writeFileSync(buildAppJson, `{
"privacy": {
"privacyContractUrl": "YOUR_DOMAIN/api/privacy?uniacid=YOUR_UNIACID",
"serviceContractUrl": "YOUR_DOMAIN/api/service?uniacid=YOUR_UNIACID"
}
}`)
// 步骤 1: syncPrivacyToStub → 真实值
simulateSync()
let after1 = JSON.parse(fs.readFileSync(stubAppJson, 'utf8'))
assert.equal(after1.privacy.privacyContractUrl, 'https://x.com/api/privacy?uniacid=1')
// 步骤 2: _copyMpHarmonyBuild → 覆盖回占位符
fs.copyFileSync(buildAppJson, stubAppJson)
let after2 = JSON.parse(fs.readFileSync(stubAppJson, 'utf8'))
// 验证 bug 现场: 步骤 2 后又是占位符
assert.match(after2.privacy.privacyContractUrl, /YOUR_DOMAIN/,
'回归测试: 步骤 2 后是占位符')
// 关键: 如果这里**没有**步骤 3, 永远停留在占位符
// 这就是为什么 vue.config.js 必须调用 --sync-privacy-only
fs.rmSync(tmpDir, { recursive: true, force: true })
})
test('verify: 占位符没替换完 (URL 含 YOUR_UNIACID) → 抛错', async () => {
const siteConfig = { baseUrl: 'https://x.com', uniacid: '1' }
const writtenObj = {
privacy: {
privacyContractUrl: 'https://x.com/api/privacy?uniacid=1',
serviceContractUrl: 'https://x.com/api/service?uniacid=YOUR_UNIACID',
privacyContractName: { zh: 'a', en: 'b' },
serviceContractName: { zh: 'a', en: 'b' },
}
}
await assert.rejects(
verifyWrittenAppJsonForTest(writtenObj, siteConfig),
AppJsonVerificationError
)
})
test('verify: 双协议头 (https://https://) → 抛错', async () => {
const siteConfig = { baseUrl: 'https://x.com', uniacid: '1' }
const writtenObj = {
privacy: {
privacyContractUrl: 'https://https://x.com/api/privacy?uniacid=1',
serviceContractUrl: 'https://x.com/api/service?uniacid=1',
privacyContractName: { zh: 'a', en: 'b' },
serviceContractName: { zh: 'a', en: 'b' },
}
}
await assert.rejects(
verifyWrittenAppJsonForTest(writtenObj, siteConfig),
AppJsonVerificationError
)
})
test('verify: 双斜杠 (域名后立即 /) → 抛错', async () => {
const siteConfig = { baseUrl: 'https://x.com', uniacid: '1' }
const writtenObj = {
privacy: {
privacyContractUrl: 'https://x.com//api/privacy?uniacid=1', // 双斜杠
serviceContractUrl: 'https://x.com/api/service?uniacid=1',
privacyContractName: { zh: 'a', en: 'b' },
serviceContractName: { zh: 'a', en: 'b' },
}
}
await assert.rejects(
verifyWrittenAppJsonForTest(writtenObj, siteConfig),
AppJsonVerificationError
)
})
test('verify: URL 前缀不等于 siteConfig.baseUrl → 抛错', async () => {
const siteConfig = { baseUrl: 'https://x.com', uniacid: '1' }
const writtenObj = {
privacy: {
privacyContractUrl: 'https://y.com/api/privacy?uniacid=1', // 错的域名
serviceContractUrl: 'https://x.com/api/service?uniacid=1',
privacyContractName: { zh: 'a', en: 'b' },
serviceContractName: { zh: 'a', en: 'b' },
}
}
await assert.rejects(
verifyWrittenAppJsonForTest(writtenObj, siteConfig),
AppJsonVerificationError
)
})
test('verify: uniacid= 参数值不匹配 siteConfig.uniacid → 抛错', async () => {
const siteConfig = { baseUrl: 'https://x.com', uniacid: '1' }
const writtenObj = {
privacy: {
privacyContractUrl: 'https://x.com/api/privacy?uniacid=99', // 错的 uniacid
serviceContractUrl: 'https://x.com/api/service?uniacid=1',
privacyContractName: { zh: 'a', en: 'b' },
serviceContractName: { zh: 'a', en: 'b' },
}
}
await assert.rejects(
verifyWrittenAppJsonForTest(writtenObj, siteConfig),
AppJsonVerificationError
)
})
test('verify: URL 缺 uniacid= 参数 → 抛错', async () => {
const siteConfig = { baseUrl: 'https://x.com', uniacid: '1' }
const writtenObj = {
privacy: {
privacyContractUrl: 'https://x.com/api/privacy', // 缺 uniacid
serviceContractUrl: 'https://x.com/api/service?uniacid=1',
privacyContractName: { zh: 'a', en: 'b' },
serviceContractName: { zh: 'a', en: 'b' },
}
}
await assert.rejects(
verifyWrittenAppJsonForTest(writtenObj, siteConfig),
AppJsonVerificationError
)
})
test('verify: privacy 块缺失 → 抛错', async () => {
const siteConfig = { baseUrl: 'https://x.com', uniacid: '1' }
const writtenObj = {} // 整个 privacy 块缺失
await assert.rejects(
verifyWrittenAppJsonForTest(writtenObj, siteConfig),
AppJsonVerificationError
)
})
test('verify: contractName 结构损坏 (缺 zh/en) → 抛错', async () => {
const siteConfig = { baseUrl: 'https://x.com', uniacid: '1' }
const writtenObj = {
privacy: {
privacyContractUrl: 'https://x.com/api/privacy?uniacid=1',
serviceContractUrl: 'https://x.com/api/service?uniacid=1',
privacyContractName: { zh: 'a' }, // 缺 en
serviceContractName: { zh: 'a', en: 'b' },
}
}
await assert.rejects(
verifyWrittenAppJsonForTest(writtenObj, siteConfig),
AppJsonVerificationError
)
})
test('verify: 多个错误一次性报出 (不短路)', async () => {
const siteConfig = { baseUrl: 'https://x.com', uniacid: '1' }
const writtenObj = {
privacy: {
privacyContractUrl: 'YOUR_DOMAIN/api/privacy?uniacid=YOUR_UNIACID', // 多个错
serviceContractUrl: 'https://y.com//api/service', // 多个错
privacyContractName: { zh: 'a', en: 'b' },
serviceContractName: { zh: 'a', en: 'b' },
}
}
try {
await verifyWrittenAppJsonForTest(writtenObj, siteConfig)
assert.fail('应该抛错')
} catch (e) {
assert.ok(e instanceof AppJsonVerificationError, '是 AppJsonVerificationError')
assert.ok(e.messages.length > 0, '应该报多个错误')
}
})
test('verify: AppJsonVerificationError 包含完整错误列表', () => {
const err = new AppJsonVerificationError(['错1', '错2', '错3'])
assert.equal(err.name, 'AppJsonVerificationError')
assert.equal(err.messages.length, 3)
assert.match(err.message, /错1/)
assert.match(err.message, /错2/)
assert.match(err.message, /错3/)
assert.match(err.message, /app\.json 写后验证失败/)
})
// =============================================================
// 9. 端到端: 完整流程 (parse + normalize + replace + 模拟 write+verify)
// =============================================================
test('端到端: 完整流程 - 写后验证通过', async () => {
// 模拟 site.js
const siteJsSource = `module.exports = {
baseUrl: "https://xcx2.aigc-quickapp.com/",
uniacid: 2482
};`
// 模拟 manifest.json
const manifest = {
'mp-harmony': {
privacy: {
privacyContractUrl: 'YOUR_DOMAIN/api/agreementview/privacy?uniacid=YOUR_UNIACID',
serviceContractUrl: 'YOUR_DOMAIN/api/agreementview/service?uniacid=YOUR_UNIACID',
privacyContractName: { zh: '隐私政策', en: 'Privacy Policy' },
serviceContractName: { zh: '用户服务协议', en: 'User Service Agreement' },
}
}
}
// 完整流程: parseSiteConfig → normalize → replace → verify
const siteConfigRaw = parseSiteConfig(siteJsSource)
const siteConfig = {
baseUrl: normalizeBaseUrlForPlaceholder(siteConfigRaw.baseUrl),
uniacid: siteConfigRaw.uniacid,
}
const finalPrivacy = replacePlaceholders(manifest['mp-harmony'].privacy, siteConfig)
// 模拟写完文件后回读的内容
const writtenObj = { privacy: finalPrivacy }
// 验证必须通过
await verifyWrittenAppJsonForTest(writtenObj, siteConfig)
// (没抛错 = 通过)
})
test('端到端: 完整流程 - 写后验证失败 (双协议) → 错误冒泡', async () => {
// 模拟 site.js
const siteJsSource = `module.exports = {
baseUrl: "https://xcx2.aigc-quickapp.com/",
uniacid: 2482
};`
// 模拟 manifest.json 写错了 (有 https:// 前缀)
const manifest = {
'mp-harmony': {
privacy: {
privacyContractUrl: 'https://YOUR_DOMAIN/api/agreementview/privacy?uniacid=YOUR_UNIACID', // 错的
serviceContractUrl: 'YOUR_DOMAIN/api/agreementview/service?uniacid=YOUR_UNIACID',
privacyContractName: { zh: '隐私政策', en: 'Privacy Policy' },
serviceContractName: { zh: '用户服务协议', en: 'User Service Agreement' },
}
}
}
const siteConfigRaw = parseSiteConfig(siteJsSource)
const siteConfig = {
baseUrl: normalizeBaseUrlForPlaceholder(siteConfigRaw.baseUrl),
uniacid: siteConfigRaw.uniacid,
}
const finalPrivacy = replacePlaceholders(manifest['mp-harmony'].privacy, siteConfig)
const writtenObj = { privacy: finalPrivacy }
// 验证必须失败
await assert.rejects(
verifyWrittenAppJsonForTest(writtenObj, siteConfig),
AppJsonVerificationError
)
})