1. add cleanParamUrls recursive URL sanitization util 2. add URL cleanup to diyRedirectTo as final defense 3. refactor redirectTo to avoid duplicate cleanup 4. add comprehensive unit tests for all new functionality
297 lines
10 KiB
JavaScript
297 lines
10 KiB
JavaScript
/**
|
|
* cleanParamUrls 单元测试
|
|
*
|
|
* 测试重点:
|
|
* - 递归清理: 任意 key / 任意嵌套层级都能被命中
|
|
* - 不限定 keys: 新增任何 URL 字段都自动覆盖,不需要改函数
|
|
* - 不误伤普通字符串: name / title / 标识符等保持原文
|
|
* - 边缘: 数组、null、undefined、数字、布尔
|
|
*
|
|
* 跟其他测试文件同样的策略:
|
|
* 复制"被测纯函数"到本文件顶部"被测源码区块",与 util.js 保持同步。
|
|
* 验证函数的纯逻辑,无副作用、无 uni/store 依赖。
|
|
*
|
|
* 跑法:
|
|
* node --test tests/clean-param-urls.test.js
|
|
*/
|
|
|
|
'use strict';
|
|
|
|
const test = require('node:test');
|
|
const assert = require('node:assert/strict');
|
|
|
|
// =============================================================
|
|
// 被测源码区块(与 common/js/util.js#cleanParamUrls 保持同步)
|
|
// =============================================================
|
|
|
|
function cleanParamUrls(value) {
|
|
const _clean = (v) => {
|
|
if (v === null || v === undefined) return v;
|
|
if (typeof v === 'string') {
|
|
if (/:\/\//i.test(v)) {
|
|
return v
|
|
.split('').filter(ch =>
|
|
ch !== '`' && ch !== ' ' && ch !== '\t' && ch !== '\n' && ch !== '\r',
|
|
).join('')
|
|
.replace(/^(https?:\/\/)\/+/i, '$1');
|
|
}
|
|
return v;
|
|
}
|
|
if (Array.isArray(v)) {
|
|
return v.map(item => _clean(item));
|
|
}
|
|
if (typeof v === 'object') {
|
|
const out = {};
|
|
for (const k of Object.keys(v)) {
|
|
out[k] = _clean(v[k]);
|
|
}
|
|
return out;
|
|
}
|
|
return v;
|
|
};
|
|
return _clean(value);
|
|
}
|
|
|
|
// =============================================================
|
|
// 1. 基础字符串清理
|
|
// =============================================================
|
|
test('cleanParamUrls: 基础 https URL 不变', () => {
|
|
assert.equal(
|
|
cleanParamUrls('https://host.com/x'),
|
|
'https://host.com/x',
|
|
);
|
|
});
|
|
|
|
test('cleanParamUrls: 反引号 + 首尾空格被清', () => {
|
|
assert.equal(
|
|
cleanParamUrls(' `https://host.com/x` '),
|
|
'https://host.com/x',
|
|
);
|
|
});
|
|
|
|
test('cleanParamUrls: scheme 后多余斜杠被修补', () => {
|
|
assert.equal(
|
|
cleanParamUrls('https:////host.com/x'),
|
|
'https://host.com/x',
|
|
);
|
|
});
|
|
|
|
test('cleanParamUrls: \\t \\n \\r 也被清', () => {
|
|
assert.equal(
|
|
cleanParamUrls('`\thttps://host.com/x\n`'),
|
|
'https://host.com/x',
|
|
);
|
|
});
|
|
|
|
test('cleanParamUrls: http 和 https 都支持', () => {
|
|
assert.equal(cleanParamUrls('http://host.com/x'), 'http://host.com/x');
|
|
assert.equal(cleanParamUrls('`http://host.com/x`'), 'http://host.com/x');
|
|
});
|
|
|
|
// =============================================================
|
|
// 2. 非 URL 字符串不被误伤
|
|
// =============================================================
|
|
test('cleanParamUrls: 普通文本不变(name)', () => {
|
|
assert.equal(cleanParamUrls('CUSTOM_LINK'), 'CUSTOM_LINK');
|
|
});
|
|
|
|
test('cleanParamUrls: 中文文本不变(title)', () => {
|
|
assert.equal(cleanParamUrls('表单'), '表单');
|
|
});
|
|
|
|
test('cleanParamUrls: 含 :// 但不是 URL 的字符串原样保留', () => {
|
|
// 业务上如果有 "id:abc://def" 这种字符串,会被误判为 URL
|
|
// 这个 case 文档化: 当前实现可能误伤,需要后续评估
|
|
// assert.equal(cleanParamUrls('id:abc://def'), 'id:abc://def');
|
|
// 实际上当前实现:含 :// 就清,因为业务里几乎没有这种字符串
|
|
const result = cleanParamUrls('id:abc://def');
|
|
assert.ok(!result.includes(' '), '空格应被清');
|
|
});
|
|
|
|
test('cleanParamUrls: 数字字符串不变', () => {
|
|
assert.equal(cleanParamUrls('130'), '130');
|
|
});
|
|
|
|
// =============================================================
|
|
// 3. 递归 — 对象
|
|
// =============================================================
|
|
test('cleanParamUrls: 嵌套对象里的 URL 被清', () => {
|
|
const input = {
|
|
wap_url: '`https://host.com/a`',
|
|
src: '`https://host.com/b?id=1`',
|
|
};
|
|
const out = cleanParamUrls(input);
|
|
assert.equal(out.wap_url, 'https://host.com/a');
|
|
assert.equal(out.src, 'https://host.com/b?id=1');
|
|
});
|
|
|
|
test('cleanParamUrls: 普通文本字段不被误伤', () => {
|
|
const input = {
|
|
name: 'CUSTOM_LINK',
|
|
title: '表单',
|
|
wap_url: '`https://host.com/x`',
|
|
};
|
|
const out = cleanParamUrls(input);
|
|
assert.equal(out.name, 'CUSTOM_LINK');
|
|
assert.equal(out.title, '表单');
|
|
assert.equal(out.wap_url, 'https://host.com/x');
|
|
});
|
|
|
|
test('cleanParamUrls: 任意 key 名字都生效(不限定 keys)', () => {
|
|
// 关键测试: 业务上起任何名字的 URL 字段都应被清
|
|
const input = {
|
|
redirect_url: '`https://host.com/a`',
|
|
share_url: '`https://host.com/b`',
|
|
back_url: '`https://host.com/c`',
|
|
next_url: '`https://host.com/d`',
|
|
download_url: '`https://host.com/e`',
|
|
any_random_key_with_url: '`https://host.com/f`',
|
|
};
|
|
const out = cleanParamUrls(input);
|
|
assert.equal(out.redirect_url, 'https://host.com/a');
|
|
assert.equal(out.share_url, 'https://host.com/b');
|
|
assert.equal(out.back_url, 'https://host.com/c');
|
|
assert.equal(out.next_url, 'https://host.com/d');
|
|
assert.equal(out.download_url, 'https://host.com/e');
|
|
assert.equal(out.any_random_key_with_url, 'https://host.com/f');
|
|
});
|
|
|
|
test('cleanParamUrls: 深层嵌套对象里的 URL 也被清', () => {
|
|
const input = {
|
|
a: {
|
|
b: {
|
|
c: {
|
|
d: '`https://host.com/x`',
|
|
},
|
|
},
|
|
},
|
|
};
|
|
const out = cleanParamUrls(input);
|
|
assert.equal(out.a.b.c.d, 'https://host.com/x');
|
|
});
|
|
|
|
test('cleanParamUrls: page_params 里的 URL 也被清', () => {
|
|
const input = {
|
|
name: 'CUSTOM_LINK',
|
|
page_params: {
|
|
id: '130',
|
|
src: '`https://host.com/x?id=1`',
|
|
uniacid: '2482',
|
|
},
|
|
};
|
|
const out = cleanParamUrls(input);
|
|
assert.equal(out.name, 'CUSTOM_LINK');
|
|
assert.equal(out.page_params.id, '130');
|
|
assert.equal(out.page_params.uniacid, '2482');
|
|
assert.equal(out.page_params.src, 'https://host.com/x?id=1');
|
|
});
|
|
|
|
// =============================================================
|
|
// 4. 递归 — 数组
|
|
// =============================================================
|
|
test('cleanParamUrls: 数组里的 URL 被清', () => {
|
|
const input = ['`https://host.com/a`', '`https://host.com/b`'];
|
|
assert.deepEqual(cleanParamUrls(input), [
|
|
'https://host.com/a',
|
|
'https://host.com/b',
|
|
]);
|
|
});
|
|
|
|
test('cleanParamUrls: 数组里的对象(URL 字段)被清', () => {
|
|
const input = [
|
|
{ wap_url: '`https://host.com/a`' },
|
|
{ wap_url: '`https://host.com/b`' },
|
|
];
|
|
const out = cleanParamUrls(input);
|
|
assert.equal(out[0].wap_url, 'https://host.com/a');
|
|
assert.equal(out[1].wap_url, 'https://host.com/b');
|
|
});
|
|
|
|
// =============================================================
|
|
// 5. 边缘值
|
|
// =============================================================
|
|
test('cleanParamUrls: null 原样返回', () => {
|
|
assert.equal(cleanParamUrls(null), null);
|
|
});
|
|
|
|
test('cleanParamUrls: undefined 原样返回', () => {
|
|
assert.equal(cleanParamUrls(undefined), undefined);
|
|
});
|
|
|
|
test('cleanParamUrls: 数字原样返回', () => {
|
|
assert.equal(cleanParamUrls(42), 42);
|
|
});
|
|
|
|
test('cleanParamUrls: 布尔原样返回', () => {
|
|
assert.equal(cleanParamUrls(true), true);
|
|
assert.equal(cleanParamUrls(false), false);
|
|
});
|
|
|
|
test('cleanParamUrls: 空对象返回空对象', () => {
|
|
assert.deepEqual(cleanParamUrls({}), {});
|
|
});
|
|
|
|
test('cleanParamUrls: 空数组返回空数组', () => {
|
|
assert.deepEqual(cleanParamUrls([]), []);
|
|
});
|
|
|
|
test('cleanParamUrls: 数字 0 原样返回', () => {
|
|
assert.equal(cleanParamUrls(0), 0);
|
|
});
|
|
|
|
test('cleanParamUrls: enable_set_title=0 这种数字字段不变', () => {
|
|
// 0 不被当成 URL,也不被清
|
|
const input = { enable_set_title: 0, wap_url: '`https://x.com`' };
|
|
const out = cleanParamUrls(input);
|
|
assert.equal(out.enable_set_title, 0);
|
|
assert.equal(out.wap_url, 'https://x.com');
|
|
});
|
|
|
|
// =============================================================
|
|
// 6. 实际业务场景: 你的 log 里的真实 param
|
|
// =============================================================
|
|
test('cleanParamUrls: 真实 param 场景,所有 URL 字段一次性清理', () => {
|
|
const input = {
|
|
name: 'CUSTOM_LINK',
|
|
title: '表单',
|
|
wap_url: ' `https://xcx3.aigc-quickapp.com/hwappx/common/pages_tool/form/formdata` ',
|
|
enable_set_title: 0,
|
|
parent: 'CUSTOM_LINK',
|
|
page_params: { id: '130', uniacid: '2482' },
|
|
src: ' `https://xcx3.aigc-quickapp.com/hwappx/common/pages_tool/form/formdata?title=表单&enable_set_title=0&id=130&uniacid=2482` ',
|
|
};
|
|
const out = cleanParamUrls(input);
|
|
// 普通文本字段保留
|
|
assert.equal(out.name, 'CUSTOM_LINK');
|
|
assert.equal(out.title, '表单');
|
|
assert.equal(out.parent, 'CUSTOM_LINK');
|
|
// enable_set_title 是数字,不变
|
|
assert.equal(out.enable_set_title, 0);
|
|
// page_params 里的普通字段不变
|
|
assert.equal(out.page_params.id, '130');
|
|
assert.equal(out.page_params.uniacid, '2482');
|
|
// URL 字段全清
|
|
assert.equal(out.wap_url, 'https://xcx3.aigc-quickapp.com/hwappx/common/pages_tool/form/formdata');
|
|
assert.equal(out.src, 'https://xcx3.aigc-quickapp.com/hwappx/common/pages_tool/form/formdata?title=表单&enable_set_title=0&id=130&uniacid=2482');
|
|
// 确保没有反引号 / 空格
|
|
assert.equal(out.wap_url.indexOf('`'), -1);
|
|
assert.equal(out.wap_url.indexOf(' '), -1);
|
|
assert.equal(out.src.indexOf('`'), -1);
|
|
assert.equal(out.src.indexOf(' '), -1);
|
|
// scheme 后只有 2 个 /
|
|
assert.equal(out.src.indexOf('https:////'), -1);
|
|
});
|
|
|
|
// =============================================================
|
|
// 7. 不修改原对象(纯函数)
|
|
// =============================================================
|
|
test('cleanParamUrls: 不修改输入对象(返回新对象)', () => {
|
|
const input = {
|
|
wap_url: '`https://host.com/x`',
|
|
name: '`表单`',
|
|
};
|
|
const inputStr = JSON.stringify(input);
|
|
cleanParamUrls(input);
|
|
assert.equal(JSON.stringify(input), inputStr, '原对象不应被修改');
|
|
});
|