201 lines
8.8 KiB
JavaScript
201 lines
8.8 KiB
JavaScript
import assert from 'node:assert/strict';
|
||
import { execFileSync } from 'node:child_process';
|
||
import { readFile } from 'node:fs/promises';
|
||
import { test } from 'node:test';
|
||
import { fileURLToPath } from 'node:url';
|
||
import { dirname, resolve } from 'node:path';
|
||
import {
|
||
renderAndroidPrivacy,
|
||
validateAndroidPrivacyDocument,
|
||
validateLegalUrlPair
|
||
} from '../scripts/app-legal-config.mjs';
|
||
|
||
const mobileRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..');
|
||
const readJson = async (path) => JSON.parse(await readFile(new URL(path, import.meta.url), 'utf8'));
|
||
const hbuilderx522Compiler = '3.0.0-alpha-5020220260725001';
|
||
|
||
test('App 图标与启动图映射均可由预检脚本验证', () => {
|
||
const output = execFileSync(process.execPath, ['scripts/verify-app-assets.mjs'], {
|
||
cwd: mobileRoot,
|
||
encoding: 'utf8'
|
||
});
|
||
assert.match(output, /App asset verification passed/);
|
||
});
|
||
|
||
// 缺少 App 平台编译器时 `uni build -p app` 不会报错,只会静默产出带 /h5/ 基路径的
|
||
// H5 空壳(无 app-service.js/app-renderjs.js),使 build:app 成为假绿,renderjs
|
||
// 实时对练代码实际从未被 App 编译器处理。
|
||
test('App 平台编译器已声明且与其他 uni 包同版本,避免 build:app 静默回落 H5', async () => {
|
||
const pkg = await readJson('../package.json');
|
||
const appPlus = pkg.dependencies?.['@dcloudio/uni-app-plus'];
|
||
|
||
assert.ok(appPlus, '缺少 @dcloudio/uni-app-plus,build:app 会静默产出 H5 空壳');
|
||
assert.equal(appPlus, hbuilderx522Compiler, '编译链必须与当前 HBuilderX 5.22 调试基座匹配');
|
||
assert.equal(appPlus, pkg.dependencies['@dcloudio/uni-app']);
|
||
assert.equal(appPlus, pkg.dependencies['@dcloudio/uni-h5']);
|
||
assert.equal(appPlus, pkg.devDependencies['@dcloudio/vite-plugin-uni']);
|
||
});
|
||
|
||
test('Android 自定义基座显式面向 Android 15 权限模型', async () => {
|
||
const manifest = await readJson('../src/manifest.json');
|
||
const android = manifest['app-plus']?.distribute?.android;
|
||
|
||
assert.equal(android?.packagename, 'com.yincheng.wygj');
|
||
assert.equal(android?.targetSdkVersion, 35, '不得回退到 DCloud 默认的 targetSdkVersion 28 兼容模式');
|
||
assert.equal(android?.usesCleartextTraffic, false, '发布配置不得允许明文 HTTP');
|
||
});
|
||
|
||
test('Android 原生网络安全配置拒绝明文流量且只信任系统证书', async () => {
|
||
const androidManifest = await readFile(
|
||
new URL('../AndroidManifest.xml', import.meta.url),
|
||
'utf8'
|
||
);
|
||
const networkSecurity = await readFile(
|
||
new URL('../nativeResources/android/res/xml/network_security_config.xml', import.meta.url),
|
||
'utf8'
|
||
);
|
||
|
||
assert.match(androidManifest, /android:usesCleartextTraffic="false"/);
|
||
assert.match(androidManifest, /android:networkSecurityConfig="@xml\/network_security_config"/);
|
||
assert.match(networkSecurity, /cleartextTrafficPermitted="false"/);
|
||
assert.match(networkSecurity, /<certificates src="system" \/>/);
|
||
assert.doesNotMatch(networkSecurity, /cleartextTrafficPermitted="true"/);
|
||
assert.doesNotMatch(networkSecurity, /<certificates src="user"/);
|
||
assert.doesNotMatch(networkSecurity, /<debug-overrides\b/);
|
||
});
|
||
|
||
test('App 构建链将 Android 原生网络安全资源同步到云打包输入目录', async () => {
|
||
const pkg = await readJson('../package.json');
|
||
const syncScript = await readFile(
|
||
new URL('../scripts/sync-app-native-resources.mjs', import.meta.url),
|
||
'utf8'
|
||
);
|
||
|
||
assert.match(pkg.scripts?.['build:app'] || '', /sync-app-native-resources\.mjs/);
|
||
assert.match(syncScript, /dist\/build\/app\/AndroidManifest\.xml/);
|
||
assert.match(syncScript, /dist\/build\/app\/nativeResources\/android\/res\/xml\/network_security_config\.xml/);
|
||
});
|
||
|
||
test('Android 启动阶段不主动索取设备信息或外部存储权限', async () => {
|
||
const manifest = await readJson('../src/manifest.json');
|
||
const android = manifest['app-plus']?.distribute?.android;
|
||
const modules = manifest['app-plus']?.modules;
|
||
|
||
assert.equal(manifest['app-plus']?.distribute?.splashscreen?.useOriginalMsgbox, true);
|
||
assert.equal(android?.permissionPhoneState?.request, 'none');
|
||
assert.equal(android?.permissionExternalStorage?.request, 'none');
|
||
assert.ok(Object.hasOwn(modules || {}, 'Camera'), '媒体功能仍需按用户操作动态申请相机权限');
|
||
assert.ok(Object.hasOwn(modules || {}, 'Record'), '语音功能仍需按用户操作动态申请录音权限');
|
||
});
|
||
|
||
test('正式法务地址必须使用不同的公网 HTTPS 页面', () => {
|
||
assert.throws(() => validateLegalUrlPair('', 'https://legal.example.cn/privacy'), /required/);
|
||
assert.throws(
|
||
() => validateLegalUrlPair('http://legal.example.cn/terms', 'https://legal.example.cn/privacy'),
|
||
/HTTPS/
|
||
);
|
||
assert.throws(
|
||
() => validateLegalUrlPair('https://127.0.0.1/terms', 'https://legal.example.cn/privacy'),
|
||
/public hostname/
|
||
);
|
||
assert.throws(
|
||
() => validateLegalUrlPair('https://legal.example.cn/document', 'https://legal.example.cn/document'),
|
||
/must be different/
|
||
);
|
||
assert.throws(
|
||
() => validateLegalUrlPair('https://198.18.0.1/terms', 'https://legal.example.cn/privacy'),
|
||
/public hostname/
|
||
);
|
||
assert.throws(
|
||
() => validateLegalUrlPair('https://legal.invalid/terms', 'https://legal.example.cn/privacy'),
|
||
/reserved hostname/
|
||
);
|
||
assert.doesNotThrow(() => validateLegalUrlPair(
|
||
'https://198.51.99.1/terms',
|
||
'https://fd-company.cn/privacy'
|
||
));
|
||
assert.doesNotThrow(() => validateLegalUrlPair(
|
||
'https://fd-company.cn/terms',
|
||
'https://fd-company.cn/privacy'
|
||
));
|
||
});
|
||
|
||
test('Android 隐私模板的首次与二次弹窗使用同一组最终链接', async () => {
|
||
const template = await readFile(new URL('../androidPrivacy.json.example', import.meta.url), 'utf8');
|
||
const rendered = renderAndroidPrivacy(
|
||
template,
|
||
'https://legal.company.cn/bangdao/terms',
|
||
'https://legal.company.cn/bangdao/privacy'
|
||
);
|
||
const privacy = JSON.parse(rendered);
|
||
|
||
assert.deepEqual(validateAndroidPrivacyDocument(privacy), {
|
||
termsUrl: 'https://legal.company.cn/bangdao/terms',
|
||
privacyUrl: 'https://legal.company.cn/bangdao/privacy'
|
||
});
|
||
assert.doesNotMatch(rendered, /__TERMS_URL__|__PRIVACY_URL__/);
|
||
|
||
privacy.second.message = privacy.second.message.replace('/privacy', '/other-privacy');
|
||
assert.throws(() => validateAndroidPrivacyDocument(privacy), /must use the same legal links/);
|
||
});
|
||
|
||
test('Android 隐私配置写入 uni-app CLI 输入目录并校验编译产物', async () => {
|
||
const configureScript = await readFile(
|
||
new URL('../scripts/configure-android-privacy.mjs', import.meta.url),
|
||
'utf8'
|
||
);
|
||
const verifier = await readFile(
|
||
new URL('../scripts/verify-app-assets.mjs', import.meta.url),
|
||
'utf8'
|
||
);
|
||
|
||
assert.match(configureScript, /resolve\(projectRoot, 'src', 'androidPrivacy\.json'\)/);
|
||
assert.match(verifier, /dist\/build\/app\/androidPrivacy\.json/);
|
||
});
|
||
|
||
test('App 登录页法务地址校验不依赖浏览器 URL 构造器', async () => {
|
||
const legalService = await readFile(
|
||
new URL('../src/services/legal.ts', import.meta.url),
|
||
'utf8'
|
||
);
|
||
|
||
assert.doesNotMatch(legalService, /new URL\(/);
|
||
assert.match(legalService, /\^https:\\\/\\\/\(\[\^\/\?#\]\+\)/);
|
||
assert.match(legalService, /legalLinksReady/);
|
||
});
|
||
|
||
test('APK 门禁区分自定义调试基座与内置业务资源的测试包', async () => {
|
||
const verifier = await readFile(
|
||
new URL('../scripts/verify-android-apk.ps1', import.meta.url),
|
||
'utf8'
|
||
);
|
||
|
||
assert.match(verifier, /ValidateSet\('custom-base', 'dcloud-test', 'release'\)/);
|
||
assert.match(verifier, /assets\\apps/);
|
||
assert.match(verifier, /bundled androidPrivacy\.json/);
|
||
assert.match(verifier, /DCloud custom base APK contains only the native debug runtime/);
|
||
assert.match(verifier, /dist\\build\\app\\app-service\.js/);
|
||
assert.match(verifier, /dist\\build\\app-plus\\app-service\.js/);
|
||
assert.match(verifier, /fixed test SMS code/);
|
||
assert.match(verifier, /'mobile number'\s*=/);
|
||
assert.match(verifier, /dcloud-test.*release.*usesCleartextTraffic/s);
|
||
assert.match(verifier, /res\\xml\\network_security_config\.xml/);
|
||
assert.match(verifier, /network security config must trust system certificates only/);
|
||
assert.match(verifier, /must not contain a cleartext exception/);
|
||
assert.match(verifier, /must not trust user or bundled certificates/);
|
||
assert.match(verifier, /must not contain debug trust overrides/);
|
||
});
|
||
|
||
test('8 月 1 日 RC 脚本默认拒绝脏工作区且记录完整构建证据', async () => {
|
||
const script = await readFile(
|
||
new URL('../../scripts/prepare-aug1-rc.ps1', import.meta.url),
|
||
'utf8'
|
||
);
|
||
|
||
assert.match(script, /requires a clean worktree/);
|
||
assert.match(script, /AllowDirtyRehearsal/);
|
||
assert.match(script, /RC build changed the previously clean worktree/);
|
||
assert.match(script, /releaseEligible = \$releaseEligible/);
|
||
assert.match(script, /APK metadata, legal links, signature, content match and sensitive-value scan/);
|
||
});
|