Files
Todo-List/scripts/check-settings.mjs
2026-09-12 13:55:57 +08:00

175 lines
6.7 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { check, ok, bad, summary, printSummary } from './_lib/check.mjs';
import { makeStore } from './_lib/store-fixture.mjs';
import { installApiStub } from './_lib/api-stub.mjs';
// 设置存储的回归测试:覆盖 update() 和 load() 的输入校验路径。
//
// 触发 bug 的两种姿势,都靠「脏值穿过校验、污染内存 + 配置文件」来静默损坏用户设置:
//
// 1. update({ theme: null })
// 旧实现只校验「值 !== 当前值」和「key 在 DEFAULT_SETTINGS 中」,于是 theme
// 被设成 null。resolveTheme() 能兜底返回 'dark',但 this._settings.theme
// 仍是 null下一次 setSortBy 等比较触发意外分支;更糟的是下次保存会把脏
// theme: null 写入 config.json —— 旧用户的设置从此永久损坏。
//
// 2. 用户手工编辑 config.json写成 `theme: null` 或 `fontSize: "huge"`
// 加载端只按 key 字段过滤,不校验值类型 / 是否在枚举内 —— 同样的污染路径,
// 只是触发位置从 update 换到了 load。
//
// 这份脚本直接构造 SettingsStore 实例 + 桩 api逐项验证上述姿势都被拦截。
// 跑法node scripts/check-settings.mjs
// 同 check-importsSettingsStore 的模块作用域会注册事件回调,但仅用到
// 内存对象没有副作用。installApiStub() 默认就是空实现。
installApiStub();
const { SettingsStore } = await import('../src/settings-store.js');
// ============================================
// update() 输入校验
// ============================================
console.log('[1] update() 拒绝非法值');
{
const s = new SettingsStore();
await s.update({ theme: null });
if (s.get('theme') === 'dark') ok('update({theme: null}) 被忽略theme 保持 dark');
else bad('theme 被 null 污染', `当前 ${JSON.stringify(s.get('theme'))}`);
}
{
const s = new SettingsStore();
await s.update({ theme: undefined });
if (s.get('theme') === 'dark') ok('update({theme: undefined}) 被忽略');
else bad('theme 被 undefined 污染', `当前 ${JSON.stringify(s.get('theme'))}`);
}
{
const s = new SettingsStore();
await s.update({ filter: 42 });
if (s.get('filter') === 'all') ok('update({filter: 42}) 数字被拒绝');
else bad('filter 被数字污染', `当前 ${JSON.stringify(s.get('filter'))}`);
}
{
const s = new SettingsStore();
await s.update({ fontSize: 'huge' });
if (s.get('fontSize') === 'medium') ok('update({fontSize: "huge"}) 枚举外字符串被拒绝');
else bad('fontSize 被非法字符串污染', `当前 ${JSON.stringify(s.get('fontSize'))}`);
}
{
const s = new SettingsStore();
await s.update({ sortBy: { evil: true } });
if (s.get('sortBy') === 'manual') ok('update({sortBy: object}) 对象被拒绝');
else bad('sortBy 被对象污染', `当前 ${JSON.stringify(s.get('sortBy'))}`);
}
{
const s = new SettingsStore();
await s.update({ __proto__: { theme: 'light' } });
// 原型污染__proto__ 不是 own propertyObject.entries 不该枚举它
if (s.get('theme') === 'dark') ok('update({__proto__}) 原型污染被拒绝');
else bad('theme 被原型污染', `当前 ${JSON.stringify(s.get('theme'))}`);
}
{
const s = new SettingsStore();
await s.update({ unknownField: 'value' });
if (s.get('unknownField') === undefined) ok('update({unknownField}) 未知字段被忽略');
else bad('未知字段被写入', `当前 ${JSON.stringify(s.get('unknownField'))}`);
}
// dataDir 单独测null 是合法语义(恢复默认),字符串也合法
{
const s = new SettingsStore();
await s.update({ dataDir: null });
if (s.get('dataDir') === null) ok('update({dataDir: null}) 保留 null恢复默认语义');
else bad('dataDir: null 被错误拒绝', `当前 ${JSON.stringify(s.get('dataDir'))}`);
}
{
const s = new SettingsStore();
await s.update({ dataDir: 'D:/Tasks' });
if (s.get('dataDir') === 'D:/Tasks') ok('update({dataDir: "D:/Tasks"}) 字符串路径生效');
else bad('dataDir 字符串路径未生效', `当前 ${JSON.stringify(s.get('dataDir'))}`);
}
{
const s = new SettingsStore();
await s.update({ dataDir: 123 });
if (s.get('dataDir') === null) ok('update({dataDir: 123}) 数字被拒绝(保持 null');
else bad('dataDir 数字污染', `当前 ${JSON.stringify(s.get('dataDir'))}`);
}
// 合法值的 happy path
{
const s = new SettingsStore();
const result = await s.update({ theme: 'light', fontSize: 'large' });
if (s.get('theme') === 'light' && s.get('fontSize') === 'large' && result === true) {
ok('update({theme: "light", fontSize: "large"}) 合法值生效并返回 true');
} else {
bad('合法值未生效', JSON.stringify({ theme: s.get('theme'), fontSize: s.get('fontSize'), result }));
}
}
// ============================================
// load() 输入校验 —— 模拟配置文件被外部改成脏值
// ============================================
console.log('\n[2] load() 从脏配置恢复默认值');
{
const s = new SettingsStore();
s._settings = {}; // 重置,确保 load 是从主进程取数
// 临时替换 api 返回脏配置
const orig = globalThis.window.api.getSettings;
globalThis.window.api.getSettings = async () => ({
theme: null,
filter: 42,
sortBy: 'random',
fontSize: 'huge',
dataDir: 'D:/Real' // 合法值,应保留
});
try {
await s.load();
const cur = s.all;
if (cur.theme === 'dark' &&
cur.filter === 'all' &&
cur.sortBy === 'manual' &&
cur.fontSize === 'medium' &&
cur.dataDir === 'D:/Real') {
ok('脏 theme/filter/sortBy/fontSize 全部回退到默认值;合法 dataDir 保留');
} else {
bad('load 污染了设置', JSON.stringify(cur));
}
} finally {
globalThis.window.api.getSettings = orig;
}
}
{
const s = new SettingsStore();
const orig = globalThis.window.api.getSettings;
globalThis.window.api.getSettings = async () => ({
theme: 'light',
filter: 'pending', // 老 enum 值load 端应迁移到 'current'
extraGarbage: 'ignored',
__proto__: { theme: 'dark' } // 原型污染load 端同样要抗
});
try {
await s.load();
const cur = s.all;
if (cur.theme === 'light' && cur.filter === 'current' && cur.extraGarbage === undefined) {
ok('合法字段生效;老 enumpending→current自动迁移未知字段忽略原型污染无效');
} else {
bad('load 表现异常', JSON.stringify(cur));
}
} finally {
globalThis.window.api.getSettings = orig;
}
}
// ============================================
console.log(`\n${'─'.repeat(48)}`);
console.log(`通过 ${summary.pass} / 失败 ${summary.fail}`);
process.exit(summary.fail === 0 ? 0 : 1);