Archived
test(workspaces): split file content service specs
This commit is contained in:
@@ -0,0 +1,127 @@
|
||||
import { mkdir, readFile, symlink, writeFile } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
import { afterEach, describe, expect, it } from "vitest";
|
||||
import { moveWorkspaceFile, readWorkspaceFile } from "./fileContentService.js";
|
||||
import { cleanupTempWorkspaces, createTempWorkspace } from "./fileContentService.testSupport.js";
|
||||
|
||||
afterEach(async () => {
|
||||
await cleanupTempWorkspaces();
|
||||
});
|
||||
|
||||
describe("moveWorkspaceFile", () => {
|
||||
it("moves a file to a new path", async () => {
|
||||
const root = await createTempWorkspace();
|
||||
await writeFile(join(root, "original.txt"), "content");
|
||||
|
||||
const result = await moveWorkspaceFile(root, "original.txt", "moved.txt");
|
||||
|
||||
expect(result).toMatchObject({ fromPath: "original.txt", toPath: "moved.txt" });
|
||||
expect(result.size).toBe(7);
|
||||
expect(Date.parse(result.modifiedAt)).not.toBeNaN();
|
||||
// Source should no longer exist
|
||||
await expect(readWorkspaceFile(root, "original.txt")).rejects.toThrow("Path does not exist");
|
||||
// Target should exist
|
||||
const target = await readWorkspaceFile(root, "moved.txt");
|
||||
expect(target.content).toBe("content");
|
||||
});
|
||||
|
||||
it("creates intermediate directories by default", async () => {
|
||||
const root = await createTempWorkspace();
|
||||
await writeFile(join(root, "file.txt"), "data");
|
||||
|
||||
await moveWorkspaceFile(root, "file.txt", "deep/nested/dir/file.txt");
|
||||
|
||||
const target = await readWorkspaceFile(root, "deep/nested/dir/file.txt");
|
||||
expect(target.content).toBe("data");
|
||||
});
|
||||
|
||||
it("fails when createDirs is false and parent directory does not exist", async () => {
|
||||
const root = await createTempWorkspace();
|
||||
await writeFile(join(root, "file.txt"), "data");
|
||||
|
||||
await expect(moveWorkspaceFile(root, "file.txt", "missing/dir/file.txt", { createDirs: false })).rejects.toThrow();
|
||||
const source = await readWorkspaceFile(root, "file.txt");
|
||||
expect(source.content).toBe("data");
|
||||
});
|
||||
|
||||
it("overwrites target when overwrite is true", async () => {
|
||||
const root = await createTempWorkspace();
|
||||
await writeFile(join(root, "source.txt"), "source content");
|
||||
await writeFile(join(root, "target.txt"), "target content");
|
||||
|
||||
const result = await moveWorkspaceFile(root, "source.txt", "target.txt", { overwrite: true });
|
||||
|
||||
expect(result.toPath).toBe("target.txt");
|
||||
const target = await readWorkspaceFile(root, "target.txt");
|
||||
expect(target.content).toBe("source content");
|
||||
});
|
||||
|
||||
it("throws when target exists and overwrite is false (default)", async () => {
|
||||
const root = await createTempWorkspace();
|
||||
await writeFile(join(root, "source.txt"), "source");
|
||||
await writeFile(join(root, "target.txt"), "target");
|
||||
|
||||
await expect(moveWorkspaceFile(root, "source.txt", "target.txt")).rejects.toThrow("File already exists");
|
||||
// Source and target should remain unchanged
|
||||
const source = await readWorkspaceFile(root, "source.txt");
|
||||
expect(source.content).toBe("source");
|
||||
const target = await readWorkspaceFile(root, "target.txt");
|
||||
expect(target.content).toBe("target");
|
||||
});
|
||||
|
||||
it("rejects source path traversal", async () => {
|
||||
const root = await createTempWorkspace();
|
||||
|
||||
await expect(moveWorkspaceFile(root, "../secret.txt", "target.txt")).rejects.toThrow("Path traversal is not allowed");
|
||||
});
|
||||
|
||||
it("rejects target path traversal", async () => {
|
||||
const root = await createTempWorkspace();
|
||||
await writeFile(join(root, "source.txt"), "data");
|
||||
|
||||
await expect(moveWorkspaceFile(root, "source.txt", "../secret.txt")).rejects.toThrow("Path traversal is not allowed");
|
||||
const source = await readWorkspaceFile(root, "source.txt");
|
||||
expect(source.content).toBe("data");
|
||||
});
|
||||
|
||||
it("rejects moving a directory", async () => {
|
||||
const root = await createTempWorkspace();
|
||||
await mkdir(join(root, "mydir"), { recursive: true });
|
||||
|
||||
await expect(moveWorkspaceFile(root, "mydir", "newdir")).rejects.toThrow("Source path is not a file");
|
||||
});
|
||||
|
||||
it("rejects missing fromPath or toPath", async () => {
|
||||
const root = await createTempWorkspace();
|
||||
|
||||
await expect(moveWorkspaceFile(root, undefined, "target.txt")).rejects.toThrow("fromPath query parameter is required");
|
||||
await expect(moveWorkspaceFile(root, "source.txt", undefined)).rejects.toThrow("toPath query parameter is required");
|
||||
await expect(moveWorkspaceFile(root, "", "target.txt")).rejects.toThrow("fromPath query parameter is required");
|
||||
await expect(moveWorkspaceFile(root, "source.txt", "")).rejects.toThrow("toPath query parameter is required");
|
||||
});
|
||||
|
||||
it("prevents moving through symlinks that escape the workspace", async () => {
|
||||
const root = await createTempWorkspace();
|
||||
await mkdir(join(root, "subdir"), { recursive: true });
|
||||
await writeFile(join(root, "subdir", "file.txt"), "data");
|
||||
// Create a symlink inside the workspace that points outside
|
||||
const outsideDir = await createTempWorkspace("pi-web-move-outside-");
|
||||
await symlink(outsideDir, join(root, "subdir", "escape"), "junction");
|
||||
|
||||
await expect(moveWorkspaceFile(root, "subdir/file.txt", "subdir/escape/evil.txt")).rejects.toThrow("Path escapes workspace");
|
||||
const source = await readWorkspaceFile(root, "subdir/file.txt");
|
||||
expect(source.content).toBe("data");
|
||||
await expect(readFile(join(outsideDir, "evil.txt"), "utf8")).rejects.toMatchObject({ code: "ENOENT" });
|
||||
});
|
||||
|
||||
it("prevents moving a source symlink that escapes the workspace", async () => {
|
||||
const root = await createTempWorkspace();
|
||||
const outsideDir = await createTempWorkspace("pi-web-move-source-outside-");
|
||||
await writeFile(join(outsideDir, "secret.txt"), "secret");
|
||||
await symlink(join(outsideDir, "secret.txt"), join(root, "source-link.txt"));
|
||||
|
||||
await expect(moveWorkspaceFile(root, "source-link.txt", "moved.txt")).rejects.toThrow("Path escapes workspace");
|
||||
await expect(readWorkspaceFile(root, "moved.txt")).rejects.toThrow("Path does not exist");
|
||||
await expect(readFile(join(outsideDir, "secret.txt"), "utf8")).resolves.toBe("secret");
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user