Files
Literature-Management/tests/unit/main/utils/scrub-internal.test.ts
2026-10-21 14:42:59 +08:00

114 lines
4.5 KiB
TypeScript

// tests/unit/main/utils/scrub-internal.test.ts
//
// 守 scrubInternal 的**脱敏**契约(2026-10-20,r201):
//
// 把 INTERNAL 错误消息里的用户家目录替换成 <rootPath> 占位符。
//
// 为什么这是隐私面:IPC 错误消息会被结构化日志全量记录(以及任何抓 IPC
// 错误日志的 telemetry 渠道)。fs 抛错原文含 `failed at
// C:\Users\foo\ResearchHub\notes\x.md` ⇒ 家目录一路泄到 renderer / 日志。
//
// 本文件锁住:
// 1. root 全路径被替换成 <rootPath>(后面必须跟分隔符或字符串结尾 ——
// `ResearchHubX\y` 不是 `ResearchHub` 的子目录,不能误伤);
// 2. 大小写不敏感文件系统(win32/darwin)上大小写变体同样脱敏;
// 3. root 带尾分隔符(盘根 `C:\`)时也能正确脱敏(r124 修的静默失效);
// 4. 正则元字符(root 里有 `+` `[` `(` 等)被正确转义;
// 5. 超过 500 字符截断(防单条消息炸 IPC buffer);
// 6. rootPath 未初始化 → 原样返回(不阻断错误上报);
import { describe, expect, it, vi } from 'vitest';
import { isCaseInsensitiveFs } from '../../../../src/main/utils/platform';
const ROOT = 'C:\\Users\\foo\\ResearchHub';
/** 盘根(`C:\`)/POSIX 根(`/`)两条语义都依赖 platform,分别用 skipIf 隔离 */
const isWin = process.platform === 'win32';
/** 加载一份全新的模块实例(safe-write 的 rootPath 是 module 级单例) */
async function load(root?: string): Promise<(msg: string) => string> {
vi.resetModules();
if (root !== undefined) {
const { setRootPath } = await import('../../../../src/main/utils/safe-write');
setRootPath(root);
}
const { scrubInternal } = await import('../../../../src/main/utils/scrub-internal');
return scrubInternal;
}
describe('scrubInternal — 家目录脱敏(r201)', () => {
it('root 全路径 → <rootPath> 占位符', async () => {
const scrub = await load(ROOT);
const out = scrub(`failed at ${ROOT}\\notes\\x.md`);
expect(out).toBe('failed at <rootPath>\\notes\\x.md');
expect(out).not.toContain('Users');
});
it('root 出现在字符串结尾 → 同样替换', async () => {
const scrub = await load(ROOT);
expect(scrub(`path is ${ROOT}`)).toBe('path is <rootPath>');
});
it('仅是前缀相同的兄弟目录(ResearchHubX)→ 不误伤', async () => {
const scrub = await load(ROOT);
const out = scrub(`failed at ${ROOT}X\\y.md`);
expect(out).toContain('ResearchHubX');
expect(out).not.toContain('<rootPath>');
});
it('与 root 无关的路径 → 原样保留', async () => {
const scrub = await load(ROOT);
const msg = 'failed at D:\\other\\place\\y.md';
expect(scrub(msg)).toBe(msg);
});
it.skipIf(!isCaseInsensitiveFs)(
'大小写不敏感 FS(win32/darwin):大小写变体同样脱敏',
async () => {
const scrub = await load(ROOT);
const out = scrub('failed at c:\\users\\FOO\\researchhub\\notes\\x.md');
expect(out).toBe('failed at <rootPath>\\notes\\x.md');
},
);
it.skipIf(isCaseInsensitiveFs || isWin)('大小写敏感 FS(POSIX):大小写变体不脱敏', async () => {
const scrub = await load('/srv/data');
const out = scrub('failed at /SRV/DATA/x.md');
expect(out).toBe('failed at /SRV/DATA/x.md');
});
it.skipIf(!isWin)('root 带尾分隔符(盘根 C:\\)→ 盘内路径仍被脱敏(r124)', async () => {
const scrub = await load('C:\\');
const out = scrub('failed at C:\\Users\\foo\\ResearchHub\\notes\\x.md');
expect(out).toBe('failed at <rootPath>\\Users\\foo\\ResearchHub\\notes\\x.md');
expect(out).not.toContain('C:\\Users');
});
it.skipIf(isWin)('root 长度 < 2(POSIX 根 /)→ 保守跳过,整条消息原样', async () => {
const scrub = await load('/');
const msg = 'failed at /srv/data/x.md';
expect(scrub(msg)).toBe(msg);
});
it('root 含正则元字符 → 被正确转义(不误伤、不漏伤)', async () => {
const weird = 'C:\\a+b[test]\\root';
const scrub = await load(weird);
const out = scrub(`failed at ${weird}\\notes\\x.md`);
expect(out).toBe('failed at <rootPath>\\notes\\x.md');
});
it('超长消息 → 截断到 500 字符 + 省略号(防炸 IPC buffer)', async () => {
const scrub = await load(ROOT);
const long = 'x'.repeat(600);
const out = scrub(long);
expect(out).toHaveLength(501);
expect(out.endsWith('…')).toBe(true);
});
it('rootPath 未初始化 → 原样返回(不阻断错误上报)', async () => {
const scrub = await load();
const msg = 'failed at C:\\Users\\foo\\secret\\x.md';
expect(scrub(msg)).toBe(msg);
});
});