test(gateway): table-drive runtime config validation matrix

This commit is contained in:
Peter Steinberger
2026-02-21 23:29:29 +00:00
parent 4ab85cee0b
commit d748657265

View File

@@ -1,303 +1,165 @@
import { describe, expect, it } from "vitest"; import { describe, expect, it } from "vitest";
import { resolveGatewayRuntimeConfig } from "./server-runtime-config.js"; import { resolveGatewayRuntimeConfig } from "./server-runtime-config.js";
const TRUSTED_PROXY_AUTH = {
mode: "trusted-proxy" as const,
trustedProxy: {
userHeader: "x-forwarded-user",
},
};
const TOKEN_AUTH = {
mode: "token" as const,
token: "test-token-123",
};
describe("resolveGatewayRuntimeConfig", () => { describe("resolveGatewayRuntimeConfig", () => {
describe("trusted-proxy auth mode", () => { describe("trusted-proxy auth mode", () => {
// This test validates BOTH validation layers: // This test validates BOTH validation layers:
// 1. CLI validation in src/cli/gateway-cli/run.ts (line 246) // 1. CLI validation in src/cli/gateway-cli/run.ts (line 246)
// 2. Runtime config validation in src/gateway/server-runtime-config.ts (line 99) // 2. Runtime config validation in src/gateway/server-runtime-config.ts (line 99)
// Both must allow lan binding when authMode === "trusted-proxy" // Both must allow lan binding when authMode === "trusted-proxy"
it("should allow lan binding with trusted-proxy auth mode", async () => { it.each([
const cfg = { {
name: "lan binding",
cfg: {
gateway: { gateway: {
bind: "lan" as const, bind: "lan" as const,
auth: { auth: TRUSTED_PROXY_AUTH,
mode: "trusted-proxy" as const,
trustedProxy: {
userHeader: "x-forwarded-user",
},
},
trustedProxies: ["192.168.1.1"], trustedProxies: ["192.168.1.1"],
}, },
}; },
expectedBindHost: "0.0.0.0",
const result = await resolveGatewayRuntimeConfig({ },
cfg, {
port: 18789, name: "loopback binding with 127.0.0.1 proxy",
}); cfg: {
expect(result.authMode).toBe("trusted-proxy");
expect(result.bindHost).toBe("0.0.0.0");
});
it("should allow loopback binding with trusted-proxy auth mode", async () => {
const cfg = {
gateway: { gateway: {
bind: "loopback" as const, bind: "loopback" as const,
auth: { auth: TRUSTED_PROXY_AUTH,
mode: "trusted-proxy" as const,
trustedProxy: {
userHeader: "x-forwarded-user",
},
},
trustedProxies: ["127.0.0.1"], trustedProxies: ["127.0.0.1"],
}, },
}; },
expectedBindHost: "127.0.0.1",
const result = await resolveGatewayRuntimeConfig({ },
cfg, {
port: 18789, name: "loopback binding with ::1 proxy",
}); cfg: {
expect(result.bindHost).toBe("127.0.0.1"); gateway: { bind: "loopback" as const, auth: TRUSTED_PROXY_AUTH, trustedProxies: ["::1"] },
},
expectedBindHost: "127.0.0.1",
},
])("allows $name", async ({ cfg, expectedBindHost }) => {
const result = await resolveGatewayRuntimeConfig({ cfg, port: 18789 });
expect(result.authMode).toBe("trusted-proxy");
expect(result.bindHost).toBe(expectedBindHost);
}); });
it("should allow loopback trusted-proxy when trustedProxies includes ::1", async () => { it.each([
const cfg = { {
gateway: { name: "loopback binding without trusted proxies",
bind: "loopback" as const, cfg: {
auth: { gateway: { bind: "loopback" as const, auth: TRUSTED_PROXY_AUTH, trustedProxies: [] },
mode: "trusted-proxy" as const,
trustedProxy: {
userHeader: "x-forwarded-user",
}, },
}, expectedMessage:
trustedProxies: ["::1"],
},
};
const result = await resolveGatewayRuntimeConfig({
cfg,
port: 18789,
});
expect(result.bindHost).toBe("127.0.0.1");
});
it("should reject loopback trusted-proxy without trustedProxies configured", async () => {
const cfg = {
gateway: {
bind: "loopback" as const,
auth: {
mode: "trusted-proxy" as const,
trustedProxy: {
userHeader: "x-forwarded-user",
},
},
trustedProxies: [],
},
};
await expect(
resolveGatewayRuntimeConfig({
cfg,
port: 18789,
}),
).rejects.toThrow(
"gateway auth mode=trusted-proxy requires gateway.trustedProxies to be configured", "gateway auth mode=trusted-proxy requires gateway.trustedProxies to be configured",
); },
}); {
name: "loopback binding without loopback trusted proxy",
it("should reject loopback trusted-proxy when trustedProxies has no loopback address", async () => { cfg: {
const cfg = {
gateway: { gateway: {
bind: "loopback" as const, bind: "loopback" as const,
auth: { auth: TRUSTED_PROXY_AUTH,
mode: "trusted-proxy" as const,
trustedProxy: {
userHeader: "x-forwarded-user",
},
},
trustedProxies: ["10.0.0.1"], trustedProxies: ["10.0.0.1"],
}, },
}; },
expectedMessage:
await expect(
resolveGatewayRuntimeConfig({
cfg,
port: 18789,
}),
).rejects.toThrow(
"gateway auth mode=trusted-proxy with bind=loopback requires gateway.trustedProxies to include 127.0.0.1, ::1, or a loopback CIDR", "gateway auth mode=trusted-proxy with bind=loopback requires gateway.trustedProxies to include 127.0.0.1, ::1, or a loopback CIDR",
);
});
it("should reject trusted-proxy without trustedProxies configured", async () => {
const cfg = {
gateway: {
bind: "lan" as const,
auth: {
mode: "trusted-proxy" as const,
trustedProxy: {
userHeader: "x-forwarded-user",
}, },
{
name: "lan binding without trusted proxies",
cfg: {
gateway: { bind: "lan" as const, auth: TRUSTED_PROXY_AUTH, trustedProxies: [] },
}, },
trustedProxies: [], expectedMessage:
},
};
await expect(
resolveGatewayRuntimeConfig({
cfg,
port: 18789,
}),
).rejects.toThrow(
"gateway auth mode=trusted-proxy requires gateway.trustedProxies to be configured", "gateway auth mode=trusted-proxy requires gateway.trustedProxies to be configured",
},
])("rejects $name", async ({ cfg, expectedMessage }) => {
await expect(resolveGatewayRuntimeConfig({ cfg, port: 18789 })).rejects.toThrow(
expectedMessage,
); );
}); });
}); });
describe("token/password auth modes", () => { describe("token/password auth modes", () => {
it("should reject token mode without token configured", async () => { it.each([
const cfg = { {
gateway: { name: "lan binding with token",
bind: "lan" as const, cfg: { gateway: { bind: "lan" as const, auth: TOKEN_AUTH } },
auth: { expectedAuthMode: "token",
mode: "token" as const, expectedBindHost: "0.0.0.0",
}, },
{
name: "loopback binding with explicit none auth",
cfg: { gateway: { bind: "loopback" as const, auth: { mode: "none" as const } } },
expectedAuthMode: "none",
expectedBindHost: "127.0.0.1",
}, },
}; ])("allows $name", async ({ cfg, expectedAuthMode, expectedBindHost }) => {
const result = await resolveGatewayRuntimeConfig({ cfg, port: 18789 });
await expect( expect(result.authMode).toBe(expectedAuthMode);
resolveGatewayRuntimeConfig({ expect(result.bindHost).toBe(expectedBindHost);
cfg,
port: 18789,
}),
).rejects.toThrow("gateway auth mode is token, but no token was configured");
}); });
it("should allow lan binding with token", async () => { it.each([
const cfg = { {
gateway: { name: "token mode without token",
bind: "lan" as const, cfg: { gateway: { bind: "lan" as const, auth: { mode: "token" as const } } },
auth: { expectedMessage: "gateway auth mode is token, but no token was configured",
mode: "token" as const,
token: "test-token-123",
}, },
{
name: "lan binding with explicit none auth",
cfg: { gateway: { bind: "lan" as const, auth: { mode: "none" as const } } },
expectedMessage: "refusing to bind gateway",
}, },
}; {
name: "loopback binding that resolves to non-loopback host",
const result = await resolveGatewayRuntimeConfig({ cfg: { gateway: { bind: "loopback" as const, auth: { mode: "none" as const } } },
cfg,
port: 18789,
});
expect(result.authMode).toBe("token");
expect(result.bindHost).toBe("0.0.0.0");
});
it("should allow loopback binding with explicit none mode", async () => {
const cfg = {
gateway: {
bind: "loopback" as const,
auth: {
mode: "none" as const,
},
},
};
const result = await resolveGatewayRuntimeConfig({
cfg,
port: 18789,
});
expect(result.authMode).toBe("none");
expect(result.bindHost).toBe("127.0.0.1");
});
it("should reject lan binding with explicit none mode", async () => {
const cfg = {
gateway: {
bind: "lan" as const,
auth: {
mode: "none" as const,
},
},
};
await expect(
resolveGatewayRuntimeConfig({
cfg,
port: 18789,
}),
).rejects.toThrow("refusing to bind gateway");
});
it("should reject loopback mode if host resolves to non-loopback", async () => {
const cfg = {
gateway: {
bind: "loopback" as const,
auth: {
mode: "none" as const,
},
},
};
await expect(
resolveGatewayRuntimeConfig({
cfg,
port: 18789,
host: "0.0.0.0", host: "0.0.0.0",
}), expectedMessage: "gateway bind=loopback resolved to non-loopback host",
).rejects.toThrow("gateway bind=loopback resolved to non-loopback host");
});
it("should reject custom bind without customBindHost", async () => {
const cfg = {
gateway: {
bind: "custom" as const,
auth: {
mode: "token" as const,
token: "test-token-123",
}, },
{
name: "custom bind without customBindHost",
cfg: { gateway: { bind: "custom" as const, auth: TOKEN_AUTH } },
expectedMessage: "gateway.bind=custom requires gateway.customBindHost",
}, },
}; {
name: "custom bind with invalid customBindHost",
await expect( cfg: {
resolveGatewayRuntimeConfig({
cfg,
port: 18789,
}),
).rejects.toThrow("gateway.bind=custom requires gateway.customBindHost");
});
it("should reject custom bind with invalid customBindHost", async () => {
const cfg = {
gateway: { gateway: {
bind: "custom" as const, bind: "custom" as const,
customBindHost: "192.168.001.100", customBindHost: "192.168.001.100",
auth: { auth: TOKEN_AUTH,
mode: "token" as const,
token: "test-token-123",
}, },
}, },
}; expectedMessage: "gateway.bind=custom requires a valid IPv4 customBindHost",
},
await expect( {
resolveGatewayRuntimeConfig({ name: "custom bind with mismatched resolved host",
cfg, cfg: {
port: 18789,
}),
).rejects.toThrow("gateway.bind=custom requires a valid IPv4 customBindHost");
});
it("should reject custom bind if resolved host differs from configured host", async () => {
const cfg = {
gateway: { gateway: {
bind: "custom" as const, bind: "custom" as const,
customBindHost: "192.168.1.100", customBindHost: "192.168.1.100",
auth: { auth: TOKEN_AUTH,
mode: "token" as const,
token: "test-token-123",
}, },
}, },
};
await expect(
resolveGatewayRuntimeConfig({
cfg,
port: 18789,
host: "0.0.0.0", host: "0.0.0.0",
}), expectedMessage: "gateway bind=custom requested 192.168.1.100 but resolved 0.0.0.0",
).rejects.toThrow("gateway bind=custom requested 192.168.1.100 but resolved 0.0.0.0"); },
])("rejects $name", async ({ cfg, host, expectedMessage }) => {
await expect(resolveGatewayRuntimeConfig({ cfg, port: 18789, host })).rejects.toThrow(
expectedMessage,
);
}); });
}); });
}); });