Initial Commit

This commit is contained in:
2026-05-27 23:47:53 +01:00
parent fbc70f96e6
commit 9b28db724f
18103 changed files with 1600554 additions and 0 deletions

View File

@@ -0,0 +1,154 @@
import { EventBus } from "@suaveplan/events";
import { describe, expect, it } from "@suaveplan/testing/runner";
import { createConfig } from "../src/core/config-builder/config-builder.js";
import { ConfigError } from "../src/error-codes/error-codes.js";
import { cliLayer } from "../src/layers/cli-layer/cli-layer.js";
import { defaultsLayer } from "../src/layers/defaults-layer/defaults-layer.js";
import { envLayer } from "../src/layers/env-layer/env-layer.js";
import { fileLayer } from "../src/layers/file-layer/file-layer.js";
import { objectLayer } from "../src/layers/object-layer/object-layer.js";
describe("createConfig integration", () => {
it("full 5-layer pipeline produces correct merged config", {
timeout: 5000,
}, () => {
const store = createConfig({
layers: [
defaultsLayer({
server: { port: 3000, host: "0.0.0.0" },
db: { pool: 5, timeout: 30_000, password: "defaultpw" },
log: { level: "info", pretty: true },
}),
fileLayer({
content: {
server: { port: 3100 },
db: { pool: 10 },
},
}),
objectLayer({
server: { port: 3200 },
}),
envLayer({
mapping: { PORT: "server.port", DB_POOL: "db.pool" },
source: { PORT: "3300", DB_POOL: "15" },
}),
cliLayer({
args: ["--port=3400", "--debug"],
mapping: { port: "server.port", debug: "log.debug" },
}),
],
sensitive: ["db.password"],
});
// CLI (priority 40) wins for port
expect(store.get("server.port")).toBe(3400);
// ENV (priority 30) wins for db.pool over file/object/defaults
expect(store.get("db.pool")).toBe(15);
// CLI added debug flag
expect(store.get("log.debug")).toBe(true);
// Defaults survive for host
expect(store.get("server.host")).toBe("0.0.0.0");
// File layer contributed timeout (no higher layer overrode it)
expect(store.get("db.timeout")).toBe(30_000);
// sensitive masking
const masked = store.toMasked() as Record<
string,
Record<string, unknown>
>;
expect(masked.db.password).toBe("****");
expect(store.get("db.password")).toBe("defaultpw");
});
it("has() returns true/false correctly", { timeout: 5000 }, () => {
const store = createConfig({
layers: [defaultsLayer({ port: 3000 })],
});
expect(store.has("port")).toBe(true);
expect(store.has("missing")).toBe(false);
});
it("toJSON returns full frozen config", { timeout: 5000 }, () => {
const store = createConfig({
layers: [defaultsLayer({ port: 3000 })],
});
const json = store.toJSON();
expect(json).toEqual({ port: 3000 });
expect(() => {
(json as Record<string, unknown>).port = 9999;
}).toThrow();
});
it("reload with validation throws when validator rejects", {
timeout: 30000,
}, async () => {
let pass = true;
const dynamic = {
name: "dynamic",
priority: 0,
resolve: () => ({ port: pass ? 3000 : "bad" }),
};
const store = createConfig({
layers: [dynamic],
validate: (d) => {
if (typeof (d as Record<string, unknown>).port !== "number") {
throw new Error("port must be number");
}
return d;
},
});
pass = false;
await expect(store.reload()).rejects.toBeInstanceOf(ConfigError);
});
it("watch-mode: fileLayer with EventBus triggers reload", {
timeout: 30000,
}, async () => {
type BusMap = { "config:file:changed": Record<string, never> };
const bus = new EventBus<BusMap>();
let fileContent = { port: 3000 };
const fLayer = fileLayer({
get content() {
return fileContent;
},
watchBus: bus,
});
const store = createConfig({
layers: [fLayer],
});
expect(store.get("port")).toBe(3000);
// Simulate file change
fileContent = { port: 4000 };
await bus.emit("config:file:changed", {});
// Give the async reload a tick to complete
await new Promise((resolve) => setTimeout(resolve, 50));
expect(store.get("port")).toBe(4000);
});
it("environment override applies on reload", {
timeout: 30000,
}, async () => {
let counter = 0;
const store = createConfig({
layers: [
{
name: "dynamic",
priority: 0,
resolve: () => ({ count: ++counter, debug: false }),
},
],
environments: { test: { debug: true } },
environment: "test",
});
expect(store.get("debug")).toBe(true);
await store.reload();
expect(store.get("debug")).toBe(true);
});
});

View File

@@ -0,0 +1,240 @@
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 }
);
});
});

View File

@@ -0,0 +1,8 @@
// Ensure `globalThis.crypto` is available in bare-Node vitest runs.
import { webcrypto } from "node:crypto";
type WritableGlobal = { crypto?: Crypto };
const g = globalThis as WritableGlobal;
if (g.crypto === undefined) {
g.crypto = webcrypto as unknown as Crypto;
}