Files
obsidian-livesync/src/apps/cli/adapters/NodeVaultAdapter.unit.spec.ts
T

141 lines
6.9 KiB
TypeScript

import { fsPromises, os, path } from "@vrtmrz/livesync-commonlib/node";
import { describe, expect, it, vi } from "vitest";
import type { FilePath } from "@vrtmrz/livesync-commonlib/compat/common/types";
import { NodeFileSystemAdapter } from "./NodeFileSystemAdapter";
import { NodeVaultAdapter } from "./NodeVaultAdapter";
describe("NodeVaultAdapter.rename", () => {
it("changes the directory entry case without changing the content", async () => {
const directory = await fsPromises.mkdtemp(path.join(os.tmpdir(), "livesync-case-rename-"));
try {
await fsPromises.writeFile(path.join(directory, "Calculus.md"), "content", "utf8");
const adapter = new NodeVaultAdapter(directory);
const file = {
path: "Calculus.md" as FilePath,
stat: { ctime: 1, mtime: 2, size: 7, type: "file" as const },
};
await adapter.rename(file, "calculus.md");
expect(await fsPromises.readdir(directory)).toEqual(["calculus.md"]);
expect(await fsPromises.readFile(path.join(directory, "calculus.md"), "utf8")).toBe("content");
expect(file.path).toBe("calculus.md");
} finally {
await fsPromises.rm(directory, { recursive: true, force: true });
}
});
it("does not move a file through a symbolic link outside the vault root", async () => {
const directory = await fsPromises.mkdtemp(path.join(os.tmpdir(), "livesync-rename-root-"));
const outsideDirectory = await fsPromises.mkdtemp(path.join(os.tmpdir(), "livesync-rename-outside-"));
try {
await fsPromises.writeFile(path.join(directory, "source.md"), "content", "utf8");
await fsPromises.symlink(
outsideDirectory,
path.join(directory, "linked"),
process.platform === "win32" ? "junction" : "dir"
);
const adapter = new NodeVaultAdapter(directory);
const file = {
path: "source.md" as FilePath,
stat: { ctime: 1, mtime: 2, size: 7, type: "file" as const },
};
await expect(adapter.rename(file, "linked/moved.md")).rejects.toThrow(/symbolic link/i);
await expect(fsPromises.readFile(path.join(directory, "source.md"), "utf8")).resolves.toBe("content");
await expect(fsPromises.stat(path.join(outsideDirectory, "moved.md"))).rejects.toMatchObject({
code: "ENOENT",
});
} finally {
await fsPromises.rm(directory, { recursive: true, force: true });
await fsPromises.rm(outsideDirectory, { recursive: true, force: true });
}
});
it("does not modify a file through a symbolic link outside the vault root", async () => {
const directory = await fsPromises.mkdtemp(path.join(os.tmpdir(), "livesync-modify-root-"));
const outsideDirectory = await fsPromises.mkdtemp(path.join(os.tmpdir(), "livesync-modify-outside-"));
try {
await fsPromises.writeFile(path.join(outsideDirectory, "victim.md"), "before", "utf8");
await fsPromises.symlink(
outsideDirectory,
path.join(directory, "linked"),
process.platform === "win32" ? "junction" : "dir"
);
const adapter = new NodeVaultAdapter(directory);
const file = {
path: "linked/victim.md" as FilePath,
stat: { ctime: 1, mtime: 2, size: 6, type: "file" as const },
};
await expect(adapter.modify(file, "after")).rejects.toThrow(/symbolic link/i);
await expect(fsPromises.readFile(path.join(outsideDirectory, "victim.md"), "utf8")).resolves.toBe("before");
} finally {
await fsPromises.rm(directory, { recursive: true, force: true });
await fsPromises.rm(outsideDirectory, { recursive: true, force: true });
}
});
});
describe("NodeFileSystemAdapter path case", () => {
it("finds the stored case and refreshes the cache after a case-only rename", async () => {
const directory = await fsPromises.mkdtemp(path.join(os.tmpdir(), "livesync-case-cache-"));
try {
await fsPromises.writeFile(path.join(directory, "Calculus.md"), "content", "utf8");
const adapter = new NodeFileSystemAdapter(directory);
await expect(adapter.getAbstractFileByPath("calculus.md")).resolves.toBeNull();
const existingFile = await adapter.getAbstractFileByPathInsensitive("calculus.md");
expect(existingFile?.path).toBe("Calculus.md");
if (!existingFile) throw new Error("Expected to find Calculus.md case-insensitively");
const renamedFile = await adapter.renameFile(existingFile, "calculus.md");
expect(renamedFile.path).toBe("calculus.md");
expect((await adapter.getFiles()).map((file) => file.path)).toEqual(["calculus.md"]);
expect(await fsPromises.readdir(directory)).toEqual(["calculus.md"]);
} finally {
await fsPromises.rm(directory, { recursive: true, force: true });
}
});
it("reports directory scan failures through the injected diagnostic callback", async () => {
const directory = await fsPromises.mkdtemp(path.join(os.tmpdir(), "livesync-scan-diagnostic-"));
const missingDirectory = path.join(directory, "missing");
const reportDiagnostic = vi.fn();
try {
const adapter = new NodeFileSystemAdapter(missingDirectory, reportDiagnostic);
await adapter.scanDirectory();
expect(reportDiagnostic).toHaveBeenCalledWith(
`Error scanning directory ${missingDirectory}:`,
expect.any(Error)
);
} finally {
await fsPromises.rm(directory, { recursive: true, force: true });
}
});
it("does not discover a file through a symbolic link outside the vault root", async () => {
const directory = await fsPromises.mkdtemp(path.join(os.tmpdir(), "livesync-discovery-root-"));
const outsideDirectory = await fsPromises.mkdtemp(path.join(os.tmpdir(), "livesync-discovery-outside-"));
try {
await fsPromises.writeFile(path.join(outsideDirectory, "outside.md"), "content", "utf8");
await fsPromises.symlink(
outsideDirectory,
path.join(directory, "linked"),
process.platform === "win32" ? "junction" : "dir"
);
const adapter = new NodeFileSystemAdapter(directory);
await expect(adapter.getAbstractFileByPath("linked/outside.md")).resolves.toBeNull();
await expect(adapter.getFiles()).resolves.toEqual([]);
} finally {
await fsPromises.rm(directory, { recursive: true, force: true });
await fsPromises.rm(outsideDirectory, { recursive: true, force: true });
}
});
});