Initial Commit
This commit is contained in:
@@ -0,0 +1,148 @@
|
||||
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<Uint8Array> {
|
||||
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<ChainLevel> {
|
||||
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 }
|
||||
);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user