Files
2026-05-27 23:47:53 +01:00

241 lines
7.6 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { describe, expect, it } from "@suaveplan/testing/runner";
import fc from "fast-check";
import { parseArgs } from "../src/layers/cli-layer/cli-layer.js";
import { coerceValue } from "../src/layers/env-layer/env-layer.js";
import type { ConfigData } from "../src/types/common.js";
import { getByPath, maskSensitive, setByPath } from "../src/utils/mask.js";
import { deepMerge, mergeLayers } from "../src/utils/merge.js";
/**
* Arbitrarily generates a plain JSON-serialisable config object with no
* undefined values (required for associativity property).
*/
const plainObjectArb = fc.dictionary(
fc
.string({ minLength: 1, maxLength: 10 })
.filter((s) => /^[a-z]+$/i.test(s)),
fc.oneof(
fc.integer(),
fc.string({ maxLength: 20 }),
fc.boolean(),
fc.constant(null)
),
{ minKeys: 0, maxKeys: 5 }
) as fc.Arbitrary<ConfigData>;
/**
* Valid dot-path: 1–4 alphanum segments.
*/
const validPathArb = fc
.array(
fc
.string({ minLength: 1, maxLength: 8 })
.filter((s) => /^[a-zA-Z0-9]+$/.test(s)),
{ minLength: 1, maxLength: 4 }
)
.map((parts) => parts.join("."));
describe("deepMerge associativity (no undefined values)", () => {
it("merge(merge(a,b),c) === merge(a,merge(b,c))", {
timeout: 30000,
}, () => {
fc.assert(
fc.property(
plainObjectArb,
plainObjectArb,
plainObjectArb,
(a, b, c) => {
const left = deepMerge(deepMerge(a, b), c);
const right = deepMerge(a, deepMerge(b, c));
expect(left).toEqual(right);
}
),
{ numRuns: 200 }
);
});
});
describe("mergeLayers priority ordering", () => {
it("result equals manually sorted-then-merged layers", {
timeout: 30000,
}, () => {
const layerArb = fc
.record({
priority: fc.integer({ min: 0, max: 100 }),
data: plainObjectArb,
})
.map((l) => ({ name: `layer-${l.priority}`, ...l }));
fc.assert(
fc.property(
fc.array(layerArb, { minLength: 2, maxLength: 8 }),
(layers) => {
const resultFromMergeLayers = mergeLayers(
[...layers].sort((a, b) => a.priority - b.priority)
);
const sortedAndFolded = layers
.slice()
.sort((a, b) => a.priority - b.priority)
.reduce<ConfigData>(
(acc, l) => deepMerge(acc, l.data),
{}
);
expect(resultFromMergeLayers).toEqual(sortedAndFolded);
}
),
{ numRuns: 200 }
);
});
});
describe("setByPath / getByPath round-trip", () => {
it("getByPath(setByPath({}, path, v), path) === v", {
timeout: 30000,
}, () => {
const leafArb = fc.oneof(
fc.integer(),
fc.string({ maxLength: 20 }),
fc.boolean(),
fc.constant(null)
);
fc.assert(
fc.property(validPathArb, leafArb, (path, value) => {
const obj: ConfigData = {};
setByPath(obj, path, value);
expect(getByPath(obj, path)).toStrictEqual(value);
}),
{ numRuns: 300 }
);
});
});
describe("maskSensitive idempotency", () => {
it("applying mask twice equals applying once", { timeout: 30000 }, () => {
const pathsArb = fc.array(validPathArb, { minLength: 0, maxLength: 4 });
fc.assert(
fc.property(plainObjectArb, pathsArb, (data, paths) => {
const once = maskSensitive(data, paths);
const twice = maskSensitive(once, paths);
expect(twice).toEqual(once);
}),
{ numRuns: 200 }
);
});
});
describe("coerceValue stability", () => {
it("coercing the string representation of a non-zero safe integer yields the same number", {
timeout: 30000,
}, () => {
// "0" is in FALSY_STRINGS and maps to false, not 0 — excluded here.
// "1" is in TRUTHY_STRINGS and maps to true — also excluded.
fc.assert(
fc.property(fc.integer({ min: 2, max: 1_000_000 }), (n) => {
const coerced = coerceValue(String(n));
expect(coerced).toBe(n);
}),
{ numRuns: 200 }
);
});
it("coercing the string representation of a negative integer (not -1) yields the same number", {
timeout: 30000,
}, () => {
fc.assert(
fc.property(fc.integer({ min: -1_000_000, max: -2 }), (n) => {
const coerced = coerceValue(String(n));
expect(coerced).toBe(n);
}),
{ numRuns: 200 }
);
});
it("coercing the string representation of a boolean yields the same boolean", {
timeout: 30000,
}, () => {
fc.assert(
fc.property(fc.boolean(), (b) => {
const coerced = coerceValue(String(b));
expect(coerced).toBe(b);
}),
{ numRuns: 100 }
);
});
it("coercing JSON.stringify of an object yields a deeply equal object", {
timeout: 30000,
}, () => {
fc.assert(
fc.property(
fc.dictionary(
fc
.string({ minLength: 1, maxLength: 8 })
.filter((s) => /^[a-z]+$/.test(s)),
fc.oneof(
fc.integer(),
fc.string({ maxLength: 10 }),
fc.boolean()
),
{ minKeys: 1, maxKeys: 4 }
),
(obj) => {
const coerced = coerceValue(JSON.stringify(obj));
expect(coerced).toEqual(obj);
}
),
{ numRuns: 200 }
);
});
});
describe("parseArgs completeness", () => {
it("every --key value pair appears in output when key is in mapping", {
timeout: 30000,
}, () => {
const keyArb = fc
.string({ minLength: 2, maxLength: 10 })
.filter((s) => /^[a-z][a-z0-9-]*$/.test(s));
const valueArb = fc
.string({ minLength: 1, maxLength: 20 })
.filter((s) => !s.startsWith("-"));
const pairsArb = fc.array(fc.tuple(keyArb, valueArb), {
minLength: 1,
maxLength: 6,
});
fc.assert(
fc.property(pairsArb, (pairs) => {
// Deduplicate keys (last wins in args, but first in mapping)
const seen = new Set<string>();
const unique: [string, string][] = [];
for (const [k, v] of pairs) {
if (!seen.has(k)) {
seen.add(k);
unique.push([k, v]);
}
}
const mapping: Record<string, string> = {};
for (const [k] of unique) {
mapping[k] = `config.${k}`;
}
const args: string[] = [];
for (const [k, v] of unique) {
args.push(`--${k}`, v);
}
const result = parseArgs(args, mapping);
for (const [k, v] of unique) {
expect(result[k]).toBe(v);
}
}),
{ numRuns: 200 }
);
});
});