/
raiden
/
obsidian-git-encrypt
Обзор
Документация
Войти
/
raiden
/
obsidian-git-encrypt
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
CI/CD
Аналитика
Безопасность
master
tests/sync/deviceStateStore.integration.test.ts
119 строк
4 KB
Robert Kuzhin
feat: orchestrate recoverable two-device sync
09 авг 2026, 12:16
09 авг 2026, 12:16
7b0e3b9
Код
Авторство
О чём код?
import { mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises"; import { tmpdir } from "node:os"; import path from "node:path"; import { afterAll, beforeAll, describe, expect, it } from "vitest"; import { CryptoService, KeyManager, ObjectAuthenticationError, type KeyHandle, } from "../../src/crypto/index.js"; import { PathSafetyError, type Manifest } from "../../src/mirror/index.js"; import { DeviceStateStore, SyncStateValidationError } from "../../src/sync/index.js"; const encoder = new TextEncoder(); const manager = new KeyManager(); const cryptoService = new CryptoService(); const deviceId = "af1d12f8-1fd3-4eb0-83a7-333730f6684e"; const baseCommit = "a".repeat(40); let root: string; let mirrorRoot: string; let stateRoot: string; let vaultId: string; let key: KeyHandle; let store: DeviceStateStore; describe.sequential("DeviceStateStore", () => { beforeAll(async () => { root = await mkdtemp(path.join(tmpdir(), "obsidian-encrypted-git-device-state-")); mirrorRoot = path.join(root, "mirror"); stateRoot = path.join(root, "local-state"); await mkdir(mirrorRoot); const created = await manager.create(encoder.encode("device state password")); vaultId = created.config.vaultId; key = created.key; store = await DeviceStateStore.open({ stateRoot, encryptedRepositoryRoot: mirrorRoot, crypto: cryptoService, vaultId, }); }, 30_000); afterAll(async () => { manager.destroy(key); await rm(root, { recursive: true, force: true }); }); it("returns undefined before initialization", async () => { await expect(store.load(key)).resolves.toBeUndefined(); }); it("atomically persists an encrypted base manifest and stable device identity", async () => { const baseManifest = manifest(7, "Private/Plans.md"); const state = await store.initialize(baseManifest, baseCommit, key, deviceId); expect(await store.load(key)).toEqual(state); const onDisk = await readFile(path.join(stateRoot, "device-state.enc")); expect(onDisk.includes(Buffer.from("Private/Plans.md"))).toBe(false); expect(onDisk.includes(Buffer.from(baseCommit))).toBe(false); const advanced = { ...state, baseCommit: "b".repeat(40), baseManifest: manifest(8, "Next.md") }; await store.save(advanced, key); await expect(store.load(key)).resolves.toEqual(advanced); }); it("detects ciphertext tampering before returning state", async () => { const filename = path.join(stateRoot, "device-state.enc"); const tampered = await readFile(filename); tampered[tampered.length - 1] = tampered.at(-1)! ^ 1; await writeFile(filename, tampered); await expect(store.load(key)).rejects.toBeInstanceOf(ObjectAuthenticationError); await store.initialize(manifest(8, "Next.md"), "b".repeat(40), key, deviceId); }); it("rejects a state opened under another vault identity", async () => { const otherVaultId = "0cc5ec59-1a93-47f9-8aa0-4782db9030f0"; const otherStore = await DeviceStateStore.open({ stateRoot, encryptedRepositoryRoot: mirrorRoot, crypto: cryptoService, vaultId: otherVaultId, }); await expect(otherStore.load(key)).rejects.toBeInstanceOf(SyncStateValidationError); }); it("rejects a state directory nested in the Git mirror before creating it", async () => { const unsafeRoot = path.join(mirrorRoot, "local-state"); await expect( DeviceStateStore.open({ stateRoot: unsafeRoot, encryptedRepositoryRoot: mirrorRoot, crypto: cryptoService, vaultId, }), ).rejects.toBeInstanceOf(PathSafetyError); await expect(readFile(unsafeRoot)).rejects.toMatchObject({ code: "ENOENT" }); }); }); function manifest(revision: number, vaultPath: string): Manifest { const fileId = "91f982c5-e456-45d1-a393-9328a995f92c"; return { formatVersion: 1, vaultId, revision, files: { [fileId]: { fileId, path: vaultPath, plaintextHash: `sha256:${revision.toString(16).repeat(64)}`, objectVersion: revision, size: revision, modifiedAt: revision, }, }, }; }