Files
Notes/tests/unit/file-move-copy.test.js
2026-10-16 16:34:15 +08:00

108 lines
3.5 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.
// 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();
});
});