Files
obsidian-livesync/src/serviceModules/FileSystemAdapters/ObsidianVaultAdapter.unit.spec.ts
T
Kim Jansheden b71cc14022 Reject unlisted filesystem aliases before vault mutations
(src/serviceModules/FileSystemAdapters/pathAliasGuard.ts)
- Require every existing, unindexed path component to use a name listed by its parent directory.
- Accept case-only and Unicode-normalisation differences while caching positive directory listings briefly.

(src/serviceModules/FileSystemAdapters/ObsidianStorageAdapter.ts, src/serviceModules/FileSystemAdapters/ObsidianVaultAdapter.ts)
- Apply the alias boundary before text, binary, append, directory, removal, create, modify, rename, delete, and trash operations.
- Check both the source and destination of vault renames before calling the host API.

(src/serviceModules/FileSystemAdapters/pathAliasGuard.unit.spec.ts)
- Cover directory and file aliases across every mutating adapter route with synthetic host behaviour.
- Exercise real host filesystem aliases where the platform exposes them, including new files and directories below an aliased folder.
- Preserve listed names, case-only differences, indexed paths, and new-file behaviour.

(src/serviceModules/FileSystemAdapters/ObsidianStorageAdapter.unit.spec.ts, src/serviceModules/FileSystemAdapters/ObsidianVaultAdapter.unit.spec.ts)
- Extend existing adapter fixtures with the vault-index contract used by the guard.
2026-10-14 17:15:20 +02:00

69 lines
2.8 KiB
TypeScript

import { describe, expect, it, vi } from "vitest";
import type { App, TFile } from "obsidian";
import { ObsidianVaultAdapter } from "./ObsidianVaultAdapter";
describe("ObsidianVaultAdapter", () => {
it("preserves a UTF-8 BOM so the content size matches the file stat", async () => {
const path = "Transcripts/字幕.md";
const contentWithoutBom = "字幕の検証行です。\n";
const contentWithBom = `\ufeff${contentWithoutBom}`;
const read = vi.fn().mockResolvedValue(contentWithoutBom);
const adapterRead = vi.fn().mockResolvedValue(contentWithBom);
const app = {
vault: {
read,
adapter: {
read: adapterRead,
},
},
} as unknown as App;
const file = {
path,
stat: {
ctime: 1,
mtime: 2,
size: new Blob([contentWithBom]).size,
},
} as TFile;
const adapter = new ObsidianVaultAdapter(app);
const result = await adapter.read(file);
expect(new Blob([result]).size).toBe(file.stat.size);
expect(result.charCodeAt(0)).toBe(0xfeff);
expect(adapterRead).toHaveBeenCalledWith(path);
expect(read).not.toHaveBeenCalled();
});
it("floors write-option timestamps before calling Obsidian vault methods", async () => {
const modify = vi.fn().mockResolvedValue(undefined);
const modifyBinary = vi.fn().mockResolvedValue(undefined);
const create = vi.fn().mockResolvedValue({});
const createBinary = vi.fn().mockResolvedValue({});
const app = {
vault: {
getAbstractFileByPath: () => ({}),
modify,
modifyBinary,
create,
createBinary,
},
} as unknown as App;
const file = { path: "note.md" } as TFile;
const adapter = new ObsidianVaultAdapter(app);
const options = { ctime: 1778511180024.462, mtime: 1778511180999.913 };
const expectedOptions = { ctime: 1778511180024, mtime: 1778511180999 };
await adapter.modify(file, "text", options);
await adapter.modifyBinary(file, new ArrayBuffer(0), options);
await adapter.create("created.md", "text", options);
await adapter.createBinary("created.bin", new ArrayBuffer(0), options);
expect(modify).toHaveBeenCalledWith(file, "text", expectedOptions);
expect(modifyBinary).toHaveBeenCalledWith(file, expect.any(ArrayBuffer), expectedOptions);
expect(create).toHaveBeenCalledWith("created.md", "text", expectedOptions);
expect(createBinary).toHaveBeenCalledWith("created.bin", expect.any(ArrayBuffer), expectedOptions);
expect(options).toEqual({ ctime: 1778511180024.462, mtime: 1778511180999.913 });
});
});