import { describe, expect, it } from "@suaveplan/testing/runner"; import * as fc from "fast-check"; import { createEmptyContainer, decryptContainer, encryptContainer, } from "../src/container/container.js"; import { decryptField, encryptField, } from "../src/encryption/field-encryption.js"; import { VAULT_CODES } from "../src/errors/codes.js"; import { VaultError } from "../src/errors/vault-error.js"; import { createManifest, packManifest, unpackManifest, } from "../src/manifest/manifest.js"; import type { DriveLike, DriveSharingLike, SyncDelegationResult, SyncSnapshot, VaultSyncLike, } from "../src/types/adapters.js"; import type { VaultConfig } from "../src/types/types.js"; import { Vault } from "../src/vault/vault.js"; function vaultKey(): Uint8Array { const key = new Uint8Array(32); crypto.getRandomValues(key); return key; } function memoryDrive(): DriveLike & { blobs: Map } { const blobs = new Map(); return { blobs, async read(path) { const v = blobs.get(path); if (v === undefined) throw new Error(`missing ${path}`); return v; }, async write(path, data) { blobs.set(path, new Uint8Array(data)); }, async delete(path) { return blobs.delete(path); }, async exists(path) { return blobs.has(path); }, }; } function noopSharing(): DriveSharingLike { return { async createLink() { return { id: "x", url: "https://x.example/x" }; }, async revokeLink() {}, async isActive() { return true; }, }; } function noopSync(): VaultSyncLike { let last: SyncSnapshot = { items: [], snapshotAt: 0 }; return { hydrate(s) { last = s; }, async pull(): Promise { return { merged: [], conflicts: [], deleted: [], durationMs: 0 }; }, async push(): Promise { return { merged: [], conflicts: [], deleted: [], durationMs: 0 }; }, async sync(): Promise { return { merged: [], conflicts: [], deleted: [], durationMs: 0 }; }, currentSnapshot() { return last; }, }; } function makeConfig(): VaultConfig { return { vaultId: "personal", email: "u@example.com", containerPath: "/v.bin", attachmentsPrefix: "/att", argon2id: { memoryKB: 65536, iterations: 3, parallelism: 1, outputLength: 32, }, argon2idSalt: new Uint8Array(16), drive: memoryDrive(), driveSharing: noopSharing(), sync: noopSync(), }; } describe("vault — property tests", () => { it("encryptField + decryptField round-trip preserves arbitrary plaintexts", { timeout: 30000, }, () => { const key = vaultKey(); fc.assert( fc.property(fc.string({ maxLength: 256 }), (value) => { const envelope = encryptField({ vaultKey: key, itemId: "i1", fieldName: "f", value, }); const recovered = decryptField({ vaultKey: key, itemId: "i1", fieldName: "f", envelope, }); return recovered === value; }), { numRuns: 50 } ); }); it("manifest pack/unpack round-trips for arbitrary parameter combinations", { timeout: 30000, }, () => { fc.assert( fc.property( fc.integer({ min: 1, max: 0xff_ff_ff_ff }), fc.integer({ min: 1, max: 0xff_ff }), fc.integer({ min: 1, max: 0xff }), fc.integer({ min: 0, max: 0xff_ff }), (memoryKB, iterations, parallelism, schemaVersion) => { const m = createManifest({ vaultId: "v", argon2idMemoryKB: memoryKB, argon2idIterations: iterations, argon2idParallelism: parallelism, schemaVersion, now: 1_700_000_000_000, }); const packed = packManifest(m); const round = unpackManifest(packed); return ( round.argon2idMemoryKB === memoryKB && round.argon2idIterations === iterations && round.argon2idParallelism === parallelism && round.schemaVersion === schemaVersion ); } ), { numRuns: 30 } ); }); it("container encrypt + decrypt round-trips arbitrary item ids", { timeout: 30000, }, () => { const key = vaultKey(); fc.assert( fc.property( fc.string({ minLength: 1, maxLength: 32 }), fc.string({ maxLength: 64 }), (id, title) => { const { manifest, state } = createEmptyContainer({ vaultId: "v", argon2idMemoryKB: 65536, argon2idIterations: 3, argon2idParallelism: 4, now: 1, }); state.items.set(id, { id, createdAt: 1, updatedAt: 1, version: 1, deleted: false, title, category: "x", fieldsJson: "{}", concealedFieldsJson: "{}", tagsJson: "[]", favorite: false, }); const blob = encryptContainer({ manifest, state, vaultKey: key, }); const round = decryptContainer(blob, key); return round.state.items.get(id)?.title === title; } ), { numRuns: 20 } ); }); it("lock is idempotent across repeat calls", { timeout: 30000, }, async () => { const vault = Vault.open(makeConfig()); await vault.lock(); await vault.lock(); await vault.lock(); expect(vault.isLocked).toBe(true); }); it("every CRUD method on a locked vault throws VAULT.LOCKED", { timeout: 30000, }, () => { const vault = Vault.open(makeConfig()); const ops: Array<() => unknown> = [ () => vault.items, () => vault.search, () => vault.attachments, () => vault.shares, () => vault.sync, () => vault.currentManifest(), ]; for (const fn of ops) { let captured: unknown = null; try { fn(); } catch (err) { captured = err; } expect(VaultError.is(captured)).toBe(true); expect((captured as VaultError).code).toBe(VAULT_CODES.LOCKED); } }); });