155 lines
5.2 KiB
TypeScript
155 lines
5.2 KiB
TypeScript
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);
|
|
});
|
|
});
|