108 lines
3.5 KiB
JavaScript
108 lines
3.5 KiB
JavaScript
// main/file-ops.js —— recursiveCopy 单测
|
||
//
|
||
// 覆盖:
|
||
// - 单层文件复制
|
||
// - 多层文件夹树递归复制(含子目录 + 子文件)
|
||
// - 源目录中的符号链接子项被跳过(安全:不跟随软链)
|
||
// - 目标名冲突由调用方(_prepareRelocate)用 resolveRenameFolderName 解,
|
||
// 本测试只验证 recursiveCopy 对给定 destAbs 的完整落地。
|
||
//
|
||
// 纯 Node,用 mkdtempSync 建真实临时目录;Windows 上 symlink 需要管理员权限,
|
||
// 故 symlink 用例用 try/catch 跳过(无权限环境不报错、不污染结果)。
|
||
|
||
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
||
import {
|
||
mkdtempSync, rmSync, writeFileSync, mkdirSync, symlinkSync, readFileSync, readdirSync, statSync,
|
||
} from 'fs';
|
||
import { tmpdir } from 'os';
|
||
import { join, sep } from 'path';
|
||
|
||
const fileOps = require('../../main/file-ops.js');
|
||
const { recursiveCopy } = fileOps;
|
||
|
||
let dir;
|
||
|
||
beforeEach(() => {
|
||
dir = mkdtempSync(join(tmpdir(), 'notes-reccopy-'));
|
||
});
|
||
|
||
afterEach(() => {
|
||
rmSync(dir, { recursive: true, force: true });
|
||
});
|
||
|
||
function lsTree(root) {
|
||
// 返回 { 相对路径(用 / 归一): 'file' | 'dir' },相对 root
|
||
const out = {};
|
||
(function walk(abs, rel) {
|
||
const entries = readdirSync(abs);
|
||
for (const name of entries) {
|
||
const a = join(abs, name);
|
||
const r = (rel ? join(rel, name) : name).split(sep).join('/');
|
||
const st = statSync(a);
|
||
if (st.isDirectory()) { out[r] = 'dir'; walk(a, r); }
|
||
else if (st.isFile()) out[r] = 'file';
|
||
}
|
||
})(root, '');
|
||
return out;
|
||
}
|
||
|
||
describe('recursiveCopy', () => {
|
||
it('复制单个文件', async () => {
|
||
const src = join(dir, 'src');
|
||
const dst = join(dir, 'dst');
|
||
mkdirSync(src);
|
||
writeFileSync(join(src, 'a.md'), 'hello');
|
||
|
||
await recursiveCopy(src, dst);
|
||
|
||
expect(statSync(join(dst, 'a.md')).isFile()).toBe(true);
|
||
expect(readFileSync(join(dst, 'a.md'), 'utf8')).toBe('hello');
|
||
});
|
||
|
||
it('递归复制多层文件夹树', async () => {
|
||
const src = join(dir, 'src');
|
||
const dst = join(dir, 'dst');
|
||
mkdirSync(join(src, 'sub'), { recursive: true });
|
||
writeFileSync(join(src, 'root.md'), 'r');
|
||
writeFileSync(join(src, 'sub', 'child.md'), 'c');
|
||
|
||
await recursiveCopy(src, dst);
|
||
|
||
const tree = lsTree(dst);
|
||
expect(tree['root.md']).toBe('file');
|
||
expect(tree['sub']).toBe('dir');
|
||
expect(tree['sub/child.md']).toBe('file');
|
||
expect(readFileSync(join(dst, 'sub', 'child.md'), 'utf8')).toBe('c');
|
||
});
|
||
|
||
it('源目录中的符号链接子项被跳过(不跟随、不落地)', async () => {
|
||
const src = join(dir, 'src');
|
||
const dst = join(dir, 'dst');
|
||
mkdirSync(src);
|
||
writeFileSync(join(src, 'real.md'), 'real');
|
||
try {
|
||
// 用一个真实文件做软链目标,再链过去
|
||
symlinkSync(join(src, 'real.md'), join(src, 'link.md'));
|
||
} catch {
|
||
// Windows 无管理员权限时 symlink 会抛 EPERM;跳过后本用例视为满足「无软链落地」
|
||
expect(true).toBe(true);
|
||
return;
|
||
}
|
||
|
||
await recursiveCopy(src, dst);
|
||
|
||
expect(statSync(join(dst, 'real.md')).isFile()).toBe(true);
|
||
expect(() => statSync(join(dst, 'link.md'))).toThrow(); // 软链未复制
|
||
});
|
||
|
||
it('目标已存在同名目录时抛错(不静默覆盖)', async () => {
|
||
const src = join(dir, 'src');
|
||
const dst = join(dir, 'dst');
|
||
mkdirSync(src);
|
||
mkdirSync(dst); // 已存在
|
||
writeFileSync(join(src, 'x.md'), 'x');
|
||
|
||
await expect(recursiveCopy(src, dst)).rejects.toBeTruthy();
|
||
});
|
||
});
|