/
raiden
/
obsidian-git-encrypt
Обзор
Документация
Войти
/
raiden
/
obsidian-git-encrypt
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
CI/CD
Аналитика
Безопасность
master
tests/session/sessionManager.test.ts
189 строк
8 KB
Robert Kuzhin
feat: add manual encrypted source control
09 авг 2026, 20:57
09 авг 2026, 20:57
3c8fa38
Код
Авторство
О чём код?
import { describe, expect, it, vi } from "vitest"; import { KeyManager, type KeyHandle } from "../../src/crypto/index.js"; import { SessionManager, SessionStateError, type SessionRuntime } from "../../src/session/index.js"; describe("SessionManager", () => { it("creates, starts, flushes, stops, and destroys one opaque-key session", async () => { const fixture = await sessionFixture(); const states: string[] = []; fixture.manager.subscribe((state) => states.push(state.status)); const password = new TextEncoder().encode("session password"); await fixture.manager.createVault(password); expect(password).toEqual(new Uint8Array(password.length)); expect(fixture.manager.requireKeyHandle()).toBe(fixture.key); await fixture.manager.lock(); expect(fixture.runtime.start).toHaveBeenCalledOnce(); expect(fixture.runtime.flush).toHaveBeenCalledOnce(); expect(fixture.runtime.stop).toHaveBeenCalledOnce(); expect(fixture.destroyKey).toHaveBeenCalledWith(fixture.key); expect(states).toEqual(["locked", "unlocking", "unlocked", "locking", "locked"]); }); it("returns tracked metadata only while the session is unlocked", async () => { const fixture = await sessionFixture(); const tracked = [ { path: "Notes/Private.md", size: 42, modifiedAt: 1_700_000_000_000, objectVersion: 3, deleted: false, }, ]; fixture.runtime.listTrackedFiles.mockResolvedValue(tracked); expect(() => fixture.manager.listTrackedFiles()).toThrow(SessionStateError); await fixture.manager.unlock(new TextEncoder().encode("password")); await expect(fixture.manager.listTrackedFiles()).resolves.toEqual(tracked); expect(fixture.runtime.listTrackedFiles).toHaveBeenCalledOnce(); await fixture.manager.lock({ force: true }); expect(() => fixture.manager.listTrackedFiles()).toThrow(SessionStateError); }); it("opens diff data without emitting a repository-state refresh", async () => { const fixture = await sessionFixture(); await fixture.manager.unlock(new TextEncoder().encode("password")); const states: string[] = []; fixture.manager.subscribe((state) => states.push(state.status)); states.length = 0; await expect(fixture.manager.getDiff("file-id", "working")).resolves.toMatchObject({ path: "Notes.md", target: "working", }); expect(states).toEqual([]); await fixture.manager.lock({ force: true }); }); it("reports authentication errors without retaining a key or password", async () => { const fixture = await sessionFixture(); const password = new TextEncoder().encode("wrong password"); fixture.backend.unlock = vi.fn(async () => { password.fill(0); throw new (await import("../../src/crypto/index.js")).AuthenticationError(); }); await expect(fixture.manager.unlock(password)).rejects.toThrow(); expect(password).toEqual(new Uint8Array(password.length)); expect(fixture.manager.getState()).toEqual({ status: "error", hasKey: false, message: "Wrong password or damaged encrypted configuration.", }); expect(() => fixture.manager.requireKeyHandle()).toThrow(SessionStateError); }); it("retains the unlocked session when safe lock flush fails, then supports force lock", async () => { const fixture = await sessionFixture(); await fixture.manager.unlock(new TextEncoder().encode("password")); fixture.runtime.flush.mockRejectedValueOnce(new Error("network unavailable")); await expect(fixture.manager.lock()).rejects.toThrow("network unavailable"); expect(fixture.manager.getState().hasKey).toBe(true); expect(fixture.destroyKey).not.toHaveBeenCalled(); await fixture.manager.lock({ force: true }); expect(fixture.destroyKey).toHaveBeenCalledOnce(); }); it("rejects overlapping state transitions", async () => { const fixture = await sessionFixture(); let release: (() => void) | undefined; fixture.runtime.start.mockImplementation( () => new Promise<void>((resolve) => { release = resolve; }), ); const unlocking = fixture.manager.unlock(new TextEncoder().encode("password")); await vi.waitFor(() => expect(fixture.manager.getState().status).toBe("unlocking")); await expect(fixture.manager.unlock(new TextEncoder().encode("second"))).rejects.toBeInstanceOf( SessionStateError, ); release!(); await unlocking; await fixture.manager.lock({ force: true }); }); }); async function sessionFixture(): Promise<{ manager: SessionManager; backend: { create: ReturnType<typeof vi.fn<() => Promise<SessionRuntime>>>; unlock: ReturnType<typeof vi.fn<() => Promise<SessionRuntime>>>; destroyKey: ReturnType<typeof vi.fn<(key: KeyHandle) => void>>; }; runtime: { key: KeyHandle; start: ReturnType<typeof vi.fn<() => Promise<void>>>; listTrackedFiles: ReturnType<typeof vi.fn<SessionRuntime["listTrackedFiles"]>>; getSourceControlStatus: ReturnType<typeof vi.fn<SessionRuntime["getSourceControlStatus"]>>; stage: ReturnType<typeof vi.fn<SessionRuntime["stage"]>>; stageAll: ReturnType<typeof vi.fn<SessionRuntime["stageAll"]>>; unstage: ReturnType<typeof vi.fn<SessionRuntime["unstage"]>>; unstageAll: ReturnType<typeof vi.fn<SessionRuntime["unstageAll"]>>; commitSelected: ReturnType<typeof vi.fn<SessionRuntime["commitSelected"]>>; fetchRemote: ReturnType<typeof vi.fn<SessionRuntime["fetchRemote"]>>; pullRemote: ReturnType<typeof vi.fn<SessionRuntime["pullRemote"]>>; pushRemote: ReturnType<typeof vi.fn<SessionRuntime["pushRemote"]>>; getDiff: ReturnType<typeof vi.fn<SessionRuntime["getDiff"]>>; flush: ReturnType<typeof vi.fn<() => Promise<void>>>; changePassword: ReturnType<typeof vi.fn<() => Promise<void>>>; stop: ReturnType<typeof vi.fn<() => Promise<void>>>; }; key: KeyHandle; destroyKey: ReturnType<typeof vi.fn<(key: KeyHandle) => void>>; }> { const keyManager = new KeyManager(); const created = await keyManager.create(new TextEncoder().encode("fixture key password")); const runtime = { key: created.key, start: vi.fn(() => Promise.resolve()), listTrackedFiles: vi.fn(() => Promise.resolve([])), getSourceControlStatus: vi.fn(() => Promise.resolve({ headCommit: "a".repeat(40), remoteCommit: null, remoteKnown: false, ahead: 0, behind: 0, staged: [], changes: [], incoming: [], conflicts: 0, })), stage: vi.fn(() => Promise.resolve()), stageAll: vi.fn(() => Promise.resolve()), unstage: vi.fn(() => Promise.resolve()), unstageAll: vi.fn(() => Promise.resolve()), commitSelected: vi.fn(() => Promise.resolve("a".repeat(40))), fetchRemote: vi.fn(() => Promise.resolve({ headCommit: "a".repeat(40), remoteCommit: null, remoteKnown: true, ahead: 0, behind: 0, staged: [], changes: [], incoming: [], conflicts: 0, })), pullRemote: vi.fn(() => Promise.resolve({ status: "up-to-date" as const })), pushRemote: vi.fn(() => Promise.resolve()), getDiff: vi.fn(() => Promise.resolve({ path: "Notes.md", target: "working" as const, binary: false, text: "No textual changes.", })), flush: vi.fn(() => Promise.resolve()), changePassword: vi.fn(() => Promise.resolve()), stop: vi.fn(() => Promise.resolve()), }; const destroyKey = vi.fn((key: KeyHandle) => keyManager.destroy(key)); const backend = { create: vi.fn(() => Promise.resolve(runtime)), unlock: vi.fn(() => Promise.resolve(runtime)), destroyKey, }; return { manager: new SessionManager(backend), backend, runtime, key: created.key, destroyKey }; }