import { promises as fs } from "node:fs"; import { tmpdir } from "node:os"; import path from "node:path"; import { ed25519 } from "@suaveplan/crypto-curve"; import { type ChainLevel, TlvChainCodec } from "@suaveplan/crypto-keys"; import { afterAll, beforeAll, describe, expect, it, } from "@suaveplan/testing/runner"; import fc from "fast-check"; import { prepareFileCommand } from "../src/commands/files/files.js"; import { serveFileCommand } from "../src/commands/files/serve-file.js"; import { createRootKeysCommand } from "../src/commands/keys/keys.js"; function arbBytes(min: number, max: number): fc.Arbitrary { return fc .array(fc.integer({ min: 0, max: 255 }), { minLength: min, maxLength: max, }) .map((arr) => Uint8Array.from(arr)); } describe("property: TLV chain codec round-trip", () => { const codec = new TlvChainCodec(); // Build a syntactically-valid ChainLevel by deriving an ed25519 // signerPublicKey from a random seed; signature/payload are arbitrary // byte arrays — the codec does not validate the cryptographic // relationship, only the byte layout. function arbChainLevel(): fc.Arbitrary { return fc .record({ seed: arbBytes(32, 32), payload: arbBytes(0, 256), signature: arbBytes(0, 64), }) .map(({ seed, payload, signature }) => { const signerPublicKey = ed25519.getPublicKey(seed); return { algorithm: "ed25519", signerPublicKey, payload, signature, } satisfies ChainLevel; }); } it("decode(encode(levels)) is byte-equal to the input for any depth in [1, 5]", { timeout: 30000, }, () => { fc.assert( fc.property( fc.array(arbChainLevel(), { minLength: 1, maxLength: 5 }), (levels) => { const encoded = codec.encode(levels); const decoded = codec.decode(encoded); expect(decoded.length).toBe(levels.length); for (let i = 0; i < levels.length; i++) { const a = levels[i]; const b = decoded[i]; if (!a || !b) throw new Error("missing level"); expect(b.algorithm).toBe(a.algorithm); expect(Array.from(b.signerPublicKey)).toEqual( Array.from(a.signerPublicKey) ); expect(Array.from(b.payload)).toEqual( Array.from(a.payload) ); expect(Array.from(b.signature)).toEqual( Array.from(a.signature) ); } } ), { numRuns: 200 } ); }); }); describe("property: packet prepare → serve round-trip", () => { let dir = ""; let vaultPath = ""; const PW = "proppw1234"; beforeAll(async () => { dir = path.join( tmpdir(), `sk-prop-${Date.now()}-${Math.random().toString(36).slice(2)}` ); await fs.mkdir(dir, { recursive: true }); await createRootKeysCommand(PW, { output: dir }); vaultPath = path.join(dir, "root.vault"); }); afterAll(async () => { await fs.rm(dir, { recursive: true, force: true }); }); // Each fast-check run decrypts the vault (Argon2id) inside // prepareFileCommand, which dominates wall-clock time at ~5s/run. // The chain codec property above runs at 200 numRuns; here we use a // smaller `numRuns` so the suite stays under the package's 30s // testTimeout while still exercising a meaningful spread of file // sizes and packet sizes. it("serve(prepare(file)) is byte-identical for arbitrary file bytes (1..10 KiB) and packet sizes", { timeout: 300000, }, async () => { let runIndex = 0; await fc.assert( fc.asyncProperty( arbBytes(1, 10 * 1024), fc.integer({ min: 64, max: 4096 }), async (fileBytes, packetSize) => { const label = `run-${runIndex++}`; const inputPath = path.join(dir, `${label}.bin`); const preparedDir = path.join(dir, `${label}-prepared`); const outputPath = path.join(dir, `${label}-out.bin`); await fs.writeFile(inputPath, fileBytes); await prepareFileCommand(inputPath, vaultPath, PW, { output: preparedDir, packetSize, }); const result = await serveFileCommand(preparedDir, { output: outputPath, }); expect(result.bytesWritten).toBe(fileBytes.byteLength); const reassembled = await fs.readFile(outputPath); expect(reassembled.byteLength).toBe(fileBytes.byteLength); for (let i = 0; i < fileBytes.byteLength; i++) { if (reassembled[i] !== fileBytes[i]) { throw new Error( `byte mismatch at offset ${i} (run=${label})` ); } } await fs.rm(inputPath, { force: true }); await fs.rm(preparedDir, { recursive: true, force: true }); await fs.rm(outputPath, { force: true }); } ), { numRuns: 10 } ); }); });