feat: expose core identity transport
This commit is contained in:
@@ -0,0 +1,151 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { ApiConfigError, createApiRuntimeConfig } from "../src/config.js";
|
||||
|
||||
const validSecret = "correct-horse-battery-staple-0123456789";
|
||||
|
||||
function validInput() {
|
||||
return {
|
||||
databaseUrl: "postgresql://ekklesia:[email protected]:5432/ekklesia",
|
||||
host: "127.0.0.1",
|
||||
identity: {
|
||||
baseUrl: "https://identity.example.test",
|
||||
secret: validSecret,
|
||||
trustedOrigins: ["https://identity.example.test"],
|
||||
},
|
||||
port: 3000,
|
||||
};
|
||||
}
|
||||
|
||||
describe("API runtime configuration", () => {
|
||||
it("accepts valid API and identity configuration", () => {
|
||||
const config = createApiRuntimeConfig(validInput(), { environment: "production" });
|
||||
|
||||
expect(config).toEqual({
|
||||
databaseUrl: validInput().databaseUrl,
|
||||
host: "127.0.0.1",
|
||||
identity: {
|
||||
baseUrl: "https://identity.example.test",
|
||||
secret: validSecret,
|
||||
trustedOrigins: ["https://identity.example.test"],
|
||||
},
|
||||
port: 3000,
|
||||
});
|
||||
expect(Object.isFrozen(config)).toBe(true);
|
||||
});
|
||||
|
||||
it("defaults host to loopback and port to 3000 when omitted", () => {
|
||||
const { host: _host, port: _port, ...rest } = validInput();
|
||||
const config = createApiRuntimeConfig(rest, { environment: "production" });
|
||||
|
||||
expect(config.host).toBe("127.0.0.1");
|
||||
expect(config.port).toBe(3000);
|
||||
});
|
||||
|
||||
it("permits an explicit non-loopback bind address", () => {
|
||||
const config = createApiRuntimeConfig(
|
||||
{ ...validInput(), host: "0.0.0.0" },
|
||||
{ environment: "production" },
|
||||
);
|
||||
|
||||
expect(config.host).toBe("0.0.0.0");
|
||||
});
|
||||
|
||||
it("rejects a missing database URL", () => {
|
||||
let captured: unknown;
|
||||
try {
|
||||
createApiRuntimeConfig({ ...validInput(), databaseUrl: undefined });
|
||||
} catch (error) {
|
||||
captured = error;
|
||||
}
|
||||
expect(captured).toBeInstanceOf(ApiConfigError);
|
||||
expect(captured).toMatchObject({ code: "DATABASE_URL_MISSING" });
|
||||
});
|
||||
|
||||
it("rejects an invalid database URL", () => {
|
||||
for (const databaseUrl of ["", "not-a-url", "mysql://example.test/db", "http://example.test"]) {
|
||||
let captured: unknown;
|
||||
try {
|
||||
createApiRuntimeConfig({ ...validInput(), databaseUrl });
|
||||
} catch (error) {
|
||||
captured = error;
|
||||
}
|
||||
expect(captured).toBeInstanceOf(ApiConfigError);
|
||||
}
|
||||
});
|
||||
|
||||
it("rejects an empty listen host", () => {
|
||||
let captured: unknown;
|
||||
try {
|
||||
createApiRuntimeConfig({ ...validInput(), host: "" });
|
||||
} catch (error) {
|
||||
captured = error;
|
||||
}
|
||||
expect(captured).toMatchObject({ code: "HOST_INVALID" });
|
||||
});
|
||||
|
||||
it("rejects an invalid listen port", () => {
|
||||
for (const port of [-1, 1.5, 70_000, Number.NaN, "not-a-port"]) {
|
||||
let captured: unknown;
|
||||
try {
|
||||
createApiRuntimeConfig({ ...validInput(), port });
|
||||
} catch (error) {
|
||||
captured = error;
|
||||
}
|
||||
expect(captured).toMatchObject({ code: "PORT_INVALID" });
|
||||
}
|
||||
});
|
||||
|
||||
it("accepts port 0 as an explicit request for an OS-assigned ephemeral port", () => {
|
||||
const config = createApiRuntimeConfig({ ...validInput(), port: 0 });
|
||||
expect(config.port).toBe(0);
|
||||
});
|
||||
|
||||
it("delegates identity validation to Core's createIdentityConfig, rejecting a short secret", () => {
|
||||
let captured: unknown;
|
||||
try {
|
||||
createApiRuntimeConfig({
|
||||
...validInput(),
|
||||
identity: { ...validInput().identity, secret: "too-short" },
|
||||
});
|
||||
} catch (error) {
|
||||
captured = error;
|
||||
}
|
||||
expect(captured).toMatchObject({ code: "SECRET_TOO_SHORT" });
|
||||
});
|
||||
|
||||
it("explicitly allows local development origins only when environment is development", () => {
|
||||
const config = createApiRuntimeConfig(
|
||||
{
|
||||
databaseUrl: validInput().databaseUrl,
|
||||
host: "127.0.0.1",
|
||||
identity: {
|
||||
baseUrl: "http://localhost:3000",
|
||||
secret: validSecret,
|
||||
trustedOrigins: ["http://localhost:3000"],
|
||||
},
|
||||
port: 3000,
|
||||
},
|
||||
{ environment: "development" },
|
||||
);
|
||||
|
||||
expect(config.identity.baseUrl).toBe("http://localhost:3000");
|
||||
});
|
||||
|
||||
it("never leaks the database URL, secret, or trusted origins through a thrown error", () => {
|
||||
const hostileUrl = "postgresql://admin:[email protected]:5432/ekklesia";
|
||||
let captured: unknown;
|
||||
try {
|
||||
createApiRuntimeConfig({ ...validInput(), databaseUrl: hostileUrl, port: -1 });
|
||||
} catch (error) {
|
||||
captured = error;
|
||||
}
|
||||
|
||||
expect(captured).toBeInstanceOf(ApiConfigError);
|
||||
const publicText = `${String(captured)}\n${
|
||||
captured instanceof Error ? (captured.stack ?? "") : ""
|
||||
}\n${JSON.stringify(captured)}`;
|
||||
expect(publicText).not.toContain(hostileUrl);
|
||||
expect(publicText).not.toContain("s3cr3t");
|
||||
expect(publicText).not.toContain("db.internal.example");
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user