1. 新增隐私配置同步脚本,自动从manifest和site.js同步真实隐私协议URL到鸿蒙构建产物 2. 补充鸿蒙元服务隐私托管双轨制适配逻辑 3. 新增完整的隐私弹窗组件单元测试覆盖 4. 修复构建产物隐私配置被占位符覆盖的问题
1157 lines
46 KiB
JavaScript
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
|
|
)
|
|
}) |