import { describe, it, expect, vi } from "vitest"; import { createDufsClient, parsePropfind, sortEntries, } from "./dufs-client.ts"; import type { DirEntry } from "./types.ts"; const PROPFIND_XML = ` /Media/2026/07/ /Media/2026/07/sub/ Sat, 12 Jul 2026 03:00:00 GMT /Media/2026/07/a%20b.jpg 2048 Sat, 12 Jul 2026 03:00:00 GMT `; describe("parsePropfind", () => { it("parses entries, skips self, decodes names, reads size/mtime", () => { const entries = parsePropfind(PROPFIND_XML, "/Media/2026/07"); // self ("/Media/2026/07/") and the empty-href entry are dropped expect(entries.map((e) => e.name)).toEqual(["sub", "a b.jpg"]); const file = entries.find((e) => e.name === "a b.jpg"); expect(file?.kind).toBe("file"); expect(file?.size).toBe(2048); expect(file?.mtimeMs).toBeGreaterThan(0); expect(entries.find((e) => e.name === "sub")?.kind).toBe("dir"); }); it("handles missing size/mtime as zero", () => { const xml = `/x.bin`; const [entry] = parsePropfind(xml, "/"); expect(entry?.size).toBe(0); expect(entry?.mtimeMs).toBe(0); }); it("coerces an unparsable size/mtime to zero", () => { const xml = `/y.binnot-a-numbernever`; const [entry] = parsePropfind(xml, "/"); expect(entry?.size).toBe(0); expect(entry?.mtimeMs).toBe(0); }); }); describe("sortEntries", () => { it("dirs first then case-insensitive name", () => { const mk = (name: string, kind: "dir" | "file"): DirEntry => ({ name, path: `/${name}`, kind, size: 0, mtimeMs: 0, }); const sorted = sortEntries([ mk("banana.jpg", "file"), mk("Apple", "dir"), mk("apple.png", "file"), mk("Zeta", "dir"), ]); expect(sorted.map((e) => e.name)).toEqual([ "Apple", "Zeta", "apple.png", "banana.jpg", ]); }); }); function jsonResponse(body: string, ok = true, status = 200): Response { return { ok, status, text: () => Promise.resolve(body), } as unknown as Response; } describe("createDufsClient", () => { it("list() PROPFINDs, sorts, and returns entries", async () => { const fetchImpl = vi.fn(() => Promise.resolve(jsonResponse(PROPFIND_XML)), ); const client = createDufsClient(fetchImpl); const entries = await client.list("/Media/2026/07"); const firstCall = fetchImpl.mock.calls.at(0); expect(firstCall?.[0]).toBe("/Media/2026/07"); expect(firstCall?.[1]?.method).toBe("PROPFIND"); expect(entries.length).toBe(2); }); it("builds file and thumbnail URLs (encoded)", () => { const client = createDufsClient(vi.fn()); expect(client.fileUrl("/Media/a b.jpg")).toBe("/Media/a%20b.jpg"); expect(client.thumbUrl("/Media/a b.jpg")).toBe( "/.thumbs/Media/a%20b.jpg.jpg", ); }); it("upload PUTs to dir/name, remove DELETEs, writeText PUTs, readText GETs", async () => { const fetchImpl = vi.fn(() => Promise.resolve(jsonResponse("content")), ); const client = createDufsClient(fetchImpl); const file = new File(["x"], "n.txt"); await client.upload("/dir", file); await client.remove("/dir/n.txt"); await client.writeText("/dir/n.txt", "hi"); const text = await client.readText("/dir/n.txt"); expect(text).toBe("content"); const methods = fetchImpl.mock.calls.map((c) => c[1]?.method); expect(methods).toEqual(["PUT", "DELETE", "PUT", "GET"]); expect(fetchImpl.mock.calls.at(0)?.[0]).toBe("/dir/n.txt"); }); it("throws on non-ok responses", async () => { const fetchImpl = vi.fn(() => Promise.resolve(jsonResponse("", false, 404)), ); const client = createDufsClient(fetchImpl); await expect(client.remove("/nope")).rejects.toThrow("404"); }); });