tests / fileUtilities.test.ts
tests / fileUtilities.test.ts
import { describe, it, expect, beforeEach, afterEach } from "vitest";
import * as fs from "fs";
import * as path from "path";
import * as os from "os";
import { readFile, writeFileSync } from "../src/fileIO";
import { diffFiles } from "../src/diff";
import { zipCreate, zipExtract, zipList } from "../src/zip";
import { fileChecksum } from "../src/checksum";
let tmpDir: string;
beforeEach(() => {
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "lms-test-"));
});
afterEach(() => {
if (tmpDir && fs.existsSync(tmpDir)) {
fs.rmSync(tmpDir, { recursive: true, force: true });
}
});
describe("readFile", () => {
it("should read a normal file", () => {
const filePath = path.join(tmpDir, "test.txt");
fs.writeFileSync(filePath, "hello world", "utf8");
const content = readFile(filePath);
expect(content).toBe("hello world");
});
it("should read an empty file", () => {
const filePath = path.join(tmpDir, "empty.txt");
fs.writeFileSync(filePath, "", "utf8");
const content = readFile(filePath);
expect(content).toBe("");
});
it("should throw for missing file", () => {
const filePath = path.join(tmpDir, "nonexistent.txt");
expect(() => readFile(filePath)).toThrow("Failed to read file");
});
it("should read file with unicode content", () => {
const filePath = path.join(tmpDir, "unicode.txt");
fs.writeFileSync(filePath, "héllo wörld 日本語", "utf8");
const content = readFile(filePath);
expect(content).toBe("héllo wörld 日本語");
});
it("should read file with multiline content", () => {
const filePath = path.join(tmpDir, "multiline.txt");
fs.writeFileSync(filePath, "line1\nline2\nline3", "utf8");
const content = readFile(filePath);
expect(content).toBe("line1\nline2\nline3");
});
});
describe("writeFileSync", () => {
it("should write content to a new file", () => {
const filePath = path.join(tmpDir, "output.txt");
writeFileSync(filePath, "test content");
expect(fs.readFileSync(filePath, "utf8")).toBe("test content");
});
it("should overwrite existing file", () => {
const filePath = path.join(tmpDir, "overwrite.txt");
fs.writeFileSync(filePath, "original", "utf8");
writeFileSync(filePath, "updated");
expect(fs.readFileSync(filePath, "utf8")).toBe("updated");
});
it("should create parent directories", () => {
const filePath = path.join(tmpDir, "sub", "dir", "file.txt");
writeFileSync(filePath, "nested content");
expect(fs.readFileSync(filePath, "utf8")).toBe("nested content");
});
it("should write empty content", () => {
const filePath = path.join(tmpDir, "empty.txt");
writeFileSync(filePath, "");
expect(fs.readFileSync(filePath, "utf8")).toBe("");
});
it("should write unicode content", () => {
const filePath = path.join(tmpDir, "unicode.txt");
writeFileSync(filePath, "héllo wörld 日本語");
expect(fs.readFileSync(filePath, "utf8")).toBe("héllo wörld 日本語");
});
});
describe("diffFiles", () => {
it("should return empty string for identical files", () => {
const file1 = path.join(tmpDir, "a.txt");
const file2 = path.join(tmpDir, "b.txt");
fs.writeFileSync(file1, "same content\n");
fs.writeFileSync(file2, "same content\n");
const diff = diffFiles(file1, file2);
expect(diff).toBe("");
});
it("should show differences for different files", () => {
const file1 = path.join(tmpDir, "a.txt");
const file2 = path.join(tmpDir, "b.txt");
fs.writeFileSync(file1, "line1\nline2\nline3\n");
fs.writeFileSync(file2, "line1\nline2 modified\nline3\n");
const diff = diffFiles(file1, file2);
expect(diff).toContain("-line2");
expect(diff).toContain("+line2 modified");
});
it("should handle empty file vs non-empty file", () => {
const file1 = path.join(tmpDir, "empty.txt");
const file2 = path.join(tmpDir, "nonempty.txt");
fs.writeFileSync(file1, "");
fs.writeFileSync(file2, "content\n");
const diff = diffFiles(file1, file2);
expect(diff).toContain("+content");
});
it("should handle both empty files", () => {
const file1 = path.join(tmpDir, "empty1.txt");
const file2 = path.join(tmpDir, "empty2.txt");
fs.writeFileSync(file1, "");
fs.writeFileSync(file2, "");
const diff = diffFiles(file1, file2);
expect(diff).toBe("");
});
it("should handle file with only additions", () => {
const file1 = path.join(tmpDir, "a.txt");
const file2 = path.join(tmpDir, "b.txt");
fs.writeFileSync(file1, "line1\n");
fs.writeFileSync(file2, "line1\nline2\nline3\n");
const diff = diffFiles(file1, file2);
expect(diff).toContain("+line2");
expect(diff).toContain("+line3");
});
it("should handle file with only deletions", () => {
const file1 = path.join(tmpDir, "a.txt");
const file2 = path.join(tmpDir, "b.txt");
fs.writeFileSync(file1, "line1\nline2\nline3\n");
fs.writeFileSync(file2, "line1\n");
const diff = diffFiles(file1, file2);
expect(diff).toContain("-line2");
expect(diff).toContain("-line3");
});
});
describe("zipCreate / zipExtract round-trip", () => {
it("should create and extract a zip with single file", () => {
const srcFile = path.join(tmpDir, "source.txt");
fs.writeFileSync(srcFile, "hello zip content");
const zipPath = path.join(tmpDir, "output.zip");
const result = zipCreate([{ source: srcFile, name: "test.txt" }], zipPath);
expect(result.success).toBe(true);
expect(result.filesCount).toBe(1);
const extractDir = path.join(tmpDir, "extracted");
const extractResult = zipExtract(zipPath, extractDir);
expect(extractResult.success).toBe(true);
expect(extractResult.extractedFiles).toContain("test.txt");
const extractedContent = fs.readFileSync(
path.join(extractDir, "test.txt"),
"utf8"
);
expect(extractedContent).toBe("hello zip content");
});
it("should create and extract a zip with multiple files", () => {
const src1 = path.join(tmpDir, "file1.txt");
const src2 = path.join(tmpDir, "file2.txt");
fs.writeFileSync(src1, "content one");
fs.writeFileSync(src2, "content two");
const zipPath = path.join(tmpDir, "multi.zip");
const result = zipCreate(
[
{ source: src1, name: "file1.txt" },
{ source: src2, name: "file2.txt" },
],
zipPath
);
expect(result.success).toBe(true);
expect(result.filesCount).toBe(2);
const extractDir = path.join(tmpDir, "extracted-multi");
const extractResult = zipExtract(zipPath, extractDir);
expect(extractResult.success).toBe(true);
expect(extractResult.extractedFiles).toHaveLength(2);
expect(fs.readFileSync(path.join(extractDir, "file1.txt"), "utf8")).toBe("content one");
expect(fs.readFileSync(path.join(extractDir, "file2.txt"), "utf8")).toBe("content two");
});
it("should preserve file content exactly", () => {
const content = "line1\nline2\nunicode: 日本語\n";
const srcFile = path.join(tmpDir, "exact.txt");
fs.writeFileSync(srcFile, content);
const zipPath = path.join(tmpDir, "exact.zip");
zipCreate([{ source: srcFile, name: "exact.txt" }], zipPath);
const extractDir = path.join(tmpDir, "extracted-exact");
zipExtract(zipPath, extractDir);
const extracted = fs.readFileSync(path.join(extractDir, "exact.txt"), "utf8");
expect(extracted).toBe(content);
});
});
describe("zipList", () => {
it("should list files in a zip", () => {
const srcFile = path.join(tmpDir, "list-test.txt");
fs.writeFileSync(srcFile, "list content");
const zipPath = path.join(tmpDir, "list.zip");
zipCreate([{ source: srcFile, name: "listed.txt" }], zipPath);
const result = zipList(zipPath);
expect(result.success).toBe(true);
expect(result.entries).toHaveLength(1);
expect(result.entries[0].name).toBe("listed.txt");
});
it("should return error for invalid zip", () => {
const notZip = path.join(tmpDir, "notzip.zip");
fs.writeFileSync(notZip, "this is not a zip file");
const result = zipList(notZip);
expect(result.success).toBe(false);
});
});
describe("fileChecksum", () => {
it("should compute sha256 checksum", async () => {
const filePath = path.join(tmpDir, "checksum.txt");
fs.writeFileSync(filePath, "hello world");
const checksum = await fileChecksum(filePath, "sha256");
// Known sha256 of "hello world"
expect(checksum).toBe(
"b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9"
);
});
it("should compute md5 checksum", async () => {
const filePath = path.join(tmpDir, "checksum.txt");
fs.writeFileSync(filePath, "hello world");
const checksum = await fileChecksum(filePath, "md5");
// Known md5 of "hello world"
expect(checksum).toBe("5eb63bbbe01eeed093cb22bb8f5acdc3");
});
it("should produce same checksum for identical content", async () => {
const file1 = path.join(tmpDir, "a.txt");
const file2 = path.join(tmpDir, "b.txt");
fs.writeFileSync(file1, "identical content");
fs.writeFileSync(file2, "identical content");
const cs1 = await fileChecksum(file1);
const cs2 = await fileChecksum(file2);
expect(cs1).toBe(cs2);
});
it("should produce different checksums for different content", async () => {
const file1 = path.join(tmpDir, "a.txt");
const file2 = path.join(tmpDir, "b.txt");
fs.writeFileSync(file1, "content one");
fs.writeFileSync(file2, "content two");
const cs1 = await fileChecksum(file1);
const cs2 = await fileChecksum(file2);
expect(cs1).not.toBe(cs2);
});
});
import { describe, it, expect, beforeEach, afterEach } from "vitest";
import * as fs from "fs";
import * as path from "path";
import * as os from "os";
import { readFile, writeFileSync } from "../src/fileIO";
import { diffFiles } from "../src/diff";
import { zipCreate, zipExtract, zipList } from "../src/zip";
import { fileChecksum } from "../src/checksum";
let tmpDir: string;
beforeEach(() => {
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "lms-test-"));
});
afterEach(() => {
if (tmpDir && fs.existsSync(tmpDir)) {
fs.rmSync(tmpDir, { recursive: true, force: true });
}
});
describe("readFile", () => {
it("should read a normal file", () => {
const filePath = path.join(tmpDir, "test.txt");
fs.writeFileSync(filePath, "hello world", "utf8");
const content = readFile(filePath);
expect(content).toBe("hello world");
});
it("should read an empty file", () => {
const filePath = path.join(tmpDir, "empty.txt");
fs.writeFileSync(filePath, "", "utf8");
const content = readFile(filePath);
expect(content).toBe("");
});
it("should throw for missing file", () => {
const filePath = path.join(tmpDir, "nonexistent.txt");
expect(() => readFile(filePath)).toThrow("Failed to read file");
});
it("should read file with unicode content", () => {
const filePath = path.join(tmpDir, "unicode.txt");
fs.writeFileSync(filePath, "héllo wörld 日本語", "utf8");
const content = readFile(filePath);
expect(content).toBe("héllo wörld 日本語");
});
it("should read file with multiline content", () => {
const filePath = path.join(tmpDir, "multiline.txt");
fs.writeFileSync(filePath, "line1\nline2\nline3", "utf8");
const content = readFile(filePath);
expect(content).toBe("line1\nline2\nline3");
});
});
describe("writeFileSync", () => {
it("should write content to a new file", () => {
const filePath = path.join(tmpDir, "output.txt");
writeFileSync(filePath, "test content");
expect(fs.readFileSync(filePath, "utf8")).toBe("test content");
});
it("should overwrite existing file", () => {
const filePath = path.join(tmpDir, "overwrite.txt");
fs.writeFileSync(filePath, "original", "utf8");
writeFileSync(filePath, "updated");
expect(fs.readFileSync(filePath, "utf8")).toBe("updated");
});
it("should create parent directories", () => {
const filePath = path.join(tmpDir, "sub", "dir", "file.txt");
writeFileSync(filePath, "nested content");
expect(fs.readFileSync(filePath, "utf8")).toBe("nested content");
});
it("should write empty content", () => {
const filePath = path.join(tmpDir, "empty.txt");
writeFileSync(filePath, "");
expect(fs.readFileSync(filePath, "utf8")).toBe("");
});
it("should write unicode content", () => {
const filePath = path.join(tmpDir, "unicode.txt");
writeFileSync(filePath, "héllo wörld 日本語");
expect(fs.readFileSync(filePath, "utf8")).toBe("héllo wörld 日本語");
});
});
describe("diffFiles", () => {
it("should return empty string for identical files", () => {
const file1 = path.join(tmpDir, "a.txt");
const file2 = path.join(tmpDir, "b.txt");
fs.writeFileSync(file1, "same content\n");
fs.writeFileSync(file2, "same content\n");
const diff = diffFiles(file1, file2);
expect(diff).toBe("");
});
it("should show differences for different files", () => {
const file1 = path.join(tmpDir, "a.txt");
const file2 = path.join(tmpDir, "b.txt");
fs.writeFileSync(file1, "line1\nline2\nline3\n");
fs.writeFileSync(file2, "line1\nline2 modified\nline3\n");
const diff = diffFiles(file1, file2);
expect(diff).toContain("-line2");
expect(diff).toContain("+line2 modified");
});
it("should handle empty file vs non-empty file", () => {
const file1 = path.join(tmpDir, "empty.txt");
const file2 = path.join(tmpDir, "nonempty.txt");
fs.writeFileSync(file1, "");
fs.writeFileSync(file2, "content\n");
const diff = diffFiles(file1, file2);
expect(diff).toContain("+content");
});
it("should handle both empty files", () => {
const file1 = path.join(tmpDir, "empty1.txt");
const file2 = path.join(tmpDir, "empty2.txt");
fs.writeFileSync(file1, "");
fs.writeFileSync(file2, "");
const diff = diffFiles(file1, file2);
expect(diff).toBe("");
});
it("should handle file with only additions", () => {
const file1 = path.join(tmpDir, "a.txt");
const file2 = path.join(tmpDir, "b.txt");
fs.writeFileSync(file1, "line1\n");
fs.writeFileSync(file2, "line1\nline2\nline3\n");
const diff = diffFiles(file1, file2);
expect(diff).toContain("+line2");
expect(diff).toContain("+line3");
});
it("should handle file with only deletions", () => {
const file1 = path.join(tmpDir, "a.txt");
const file2 = path.join(tmpDir, "b.txt");
fs.writeFileSync(file1, "line1\nline2\nline3\n");
fs.writeFileSync(file2, "line1\n");
const diff = diffFiles(file1, file2);
expect(diff).toContain("-line2");
expect(diff).toContain("-line3");
});
});
describe("zipCreate / zipExtract round-trip", () => {
it("should create and extract a zip with single file", () => {
const srcFile = path.join(tmpDir, "source.txt");
fs.writeFileSync(srcFile, "hello zip content");
const zipPath = path.join(tmpDir, "output.zip");
const result = zipCreate([{ source: srcFile, name: "test.txt" }], zipPath);
expect(result.success).toBe(true);
expect(result.filesCount).toBe(1);
const extractDir = path.join(tmpDir, "extracted");
const extractResult = zipExtract(zipPath, extractDir);
expect(extractResult.success).toBe(true);
expect(extractResult.extractedFiles).toContain("test.txt");
const extractedContent = fs.readFileSync(
path.join(extractDir, "test.txt"),
"utf8"
);
expect(extractedContent).toBe("hello zip content");
});
it("should create and extract a zip with multiple files", () => {
const src1 = path.join(tmpDir, "file1.txt");
const src2 = path.join(tmpDir, "file2.txt");
fs.writeFileSync(src1, "content one");
fs.writeFileSync(src2, "content two");
const zipPath = path.join(tmpDir, "multi.zip");
const result = zipCreate(
[
{ source: src1, name: "file1.txt" },
{ source: src2, name: "file2.txt" },
],
zipPath
);
expect(result.success).toBe(true);
expect(result.filesCount).toBe(2);
const extractDir = path.join(tmpDir, "extracted-multi");
const extractResult = zipExtract(zipPath, extractDir);
expect(extractResult.success).toBe(true);
expect(extractResult.extractedFiles).toHaveLength(2);
expect(fs.readFileSync(path.join(extractDir, "file1.txt"), "utf8")).toBe("content one");
expect(fs.readFileSync(path.join(extractDir, "file2.txt"), "utf8")).toBe("content two");
});
it("should preserve file content exactly", () => {
const content = "line1\nline2\nunicode: 日本語\n";
const srcFile = path.join(tmpDir, "exact.txt");
fs.writeFileSync(srcFile, content);
const zipPath = path.join(tmpDir, "exact.zip");
zipCreate([{ source: srcFile, name: "exact.txt" }], zipPath);
const extractDir = path.join(tmpDir, "extracted-exact");
zipExtract(zipPath, extractDir);
const extracted = fs.readFileSync(path.join(extractDir, "exact.txt"), "utf8");
expect(extracted).toBe(content);
});
});
describe("zipList", () => {
it("should list files in a zip", () => {
const srcFile = path.join(tmpDir, "list-test.txt");
fs.writeFileSync(srcFile, "list content");
const zipPath = path.join(tmpDir, "list.zip");
zipCreate([{ source: srcFile, name: "listed.txt" }], zipPath);
const result = zipList(zipPath);
expect(result.success).toBe(true);
expect(result.entries).toHaveLength(1);
expect(result.entries[0].name).toBe("listed.txt");
});
it("should return error for invalid zip", () => {
const notZip = path.join(tmpDir, "notzip.zip");
fs.writeFileSync(notZip, "this is not a zip file");
const result = zipList(notZip);
expect(result.success).toBe(false);
});
});
describe("fileChecksum", () => {
it("should compute sha256 checksum", async () => {
const filePath = path.join(tmpDir, "checksum.txt");
fs.writeFileSync(filePath, "hello world");
const checksum = await fileChecksum(filePath, "sha256");
// Known sha256 of "hello world"
expect(checksum).toBe(
"b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9"
);
});
it("should compute md5 checksum", async () => {
const filePath = path.join(tmpDir, "checksum.txt");
fs.writeFileSync(filePath, "hello world");
const checksum = await fileChecksum(filePath, "md5");
// Known md5 of "hello world"
expect(checksum).toBe("5eb63bbbe01eeed093cb22bb8f5acdc3");
});
it("should produce same checksum for identical content", async () => {
const file1 = path.join(tmpDir, "a.txt");
const file2 = path.join(tmpDir, "b.txt");
fs.writeFileSync(file1, "identical content");
fs.writeFileSync(file2, "identical content");
const cs1 = await fileChecksum(file1);
const cs2 = await fileChecksum(file2);
expect(cs1).toBe(cs2);
});
it("should produce different checksums for different content", async () => {
const file1 = path.join(tmpDir, "a.txt");
const file2 = path.join(tmpDir, "b.txt");
fs.writeFileSync(file1, "content one");
fs.writeFileSync(file2, "content two");
const cs1 = await fileChecksum(file1);
const cs2 = await fileChecksum(file2);
expect(cs1).not.toBe(cs2);
});
});