Merged via /review-pr -> /prepare-pr -> /merge-pr. Prepared head SHA: 6ff3ca9b5db530c2ea4abbd027ee98a9c4a1be67 Co-authored-by: mbelinky <132747814+mbelinky@users.noreply.github.com> Co-authored-by: mbelinky <132747814+mbelinky@users.noreply.github.com> Reviewed-by: @mbelinky
304 lines
7.8 KiB
TypeScript
304 lines
7.8 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
import { resolveGatewayRuntimeConfig } from "./server-runtime-config.js";
|
|
|
|
describe("resolveGatewayRuntimeConfig", () => {
|
|
describe("trusted-proxy auth mode", () => {
|
|
// This test validates BOTH validation layers:
|
|
// 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)
|
|
// Both must allow lan binding when authMode === "trusted-proxy"
|
|
it("should allow lan binding with trusted-proxy auth mode", async () => {
|
|
const cfg = {
|
|
gateway: {
|
|
bind: "lan" as const,
|
|
auth: {
|
|
mode: "trusted-proxy" as const,
|
|
trustedProxy: {
|
|
userHeader: "x-forwarded-user",
|
|
},
|
|
},
|
|
trustedProxies: ["192.168.1.1"],
|
|
},
|
|
};
|
|
|
|
const result = await resolveGatewayRuntimeConfig({
|
|
cfg,
|
|
port: 18789,
|
|
});
|
|
|
|
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: {
|
|
bind: "loopback" as const,
|
|
auth: {
|
|
mode: "trusted-proxy" as const,
|
|
trustedProxy: {
|
|
userHeader: "x-forwarded-user",
|
|
},
|
|
},
|
|
trustedProxies: ["127.0.0.1"],
|
|
},
|
|
};
|
|
|
|
const result = await resolveGatewayRuntimeConfig({
|
|
cfg,
|
|
port: 18789,
|
|
});
|
|
expect(result.bindHost).toBe("127.0.0.1");
|
|
});
|
|
|
|
it("should allow loopback trusted-proxy when trustedProxies includes ::1", async () => {
|
|
const cfg = {
|
|
gateway: {
|
|
bind: "loopback" as const,
|
|
auth: {
|
|
mode: "trusted-proxy" as const,
|
|
trustedProxy: {
|
|
userHeader: "x-forwarded-user",
|
|
},
|
|
},
|
|
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",
|
|
);
|
|
});
|
|
|
|
it("should reject loopback trusted-proxy when trustedProxies has no loopback address", async () => {
|
|
const cfg = {
|
|
gateway: {
|
|
bind: "loopback" as const,
|
|
auth: {
|
|
mode: "trusted-proxy" as const,
|
|
trustedProxy: {
|
|
userHeader: "x-forwarded-user",
|
|
},
|
|
},
|
|
trustedProxies: ["10.0.0.1"],
|
|
},
|
|
};
|
|
|
|
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",
|
|
);
|
|
});
|
|
|
|
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",
|
|
},
|
|
},
|
|
trustedProxies: [],
|
|
},
|
|
};
|
|
|
|
await expect(
|
|
resolveGatewayRuntimeConfig({
|
|
cfg,
|
|
port: 18789,
|
|
}),
|
|
).rejects.toThrow(
|
|
"gateway auth mode=trusted-proxy requires gateway.trustedProxies to be configured",
|
|
);
|
|
});
|
|
});
|
|
|
|
describe("token/password auth modes", () => {
|
|
it("should reject token mode without token configured", async () => {
|
|
const cfg = {
|
|
gateway: {
|
|
bind: "lan" as const,
|
|
auth: {
|
|
mode: "token" as const,
|
|
},
|
|
},
|
|
};
|
|
|
|
await expect(
|
|
resolveGatewayRuntimeConfig({
|
|
cfg,
|
|
port: 18789,
|
|
}),
|
|
).rejects.toThrow("gateway auth mode is token, but no token was configured");
|
|
});
|
|
|
|
it("should allow lan binding with token", async () => {
|
|
const cfg = {
|
|
gateway: {
|
|
bind: "lan" as const,
|
|
auth: {
|
|
mode: "token" as const,
|
|
token: "test-token-123",
|
|
},
|
|
},
|
|
};
|
|
|
|
const result = await resolveGatewayRuntimeConfig({
|
|
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",
|
|
}),
|
|
).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",
|
|
},
|
|
},
|
|
};
|
|
|
|
await expect(
|
|
resolveGatewayRuntimeConfig({
|
|
cfg,
|
|
port: 18789,
|
|
}),
|
|
).rejects.toThrow("gateway.bind=custom requires gateway.customBindHost");
|
|
});
|
|
|
|
it("should reject custom bind with invalid customBindHost", async () => {
|
|
const cfg = {
|
|
gateway: {
|
|
bind: "custom" as const,
|
|
customBindHost: "192.168.001.100",
|
|
auth: {
|
|
mode: "token" as const,
|
|
token: "test-token-123",
|
|
},
|
|
},
|
|
};
|
|
|
|
await expect(
|
|
resolveGatewayRuntimeConfig({
|
|
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: {
|
|
bind: "custom" as const,
|
|
customBindHost: "192.168.1.100",
|
|
auth: {
|
|
mode: "token" as const,
|
|
token: "test-token-123",
|
|
},
|
|
},
|
|
};
|
|
|
|
await expect(
|
|
resolveGatewayRuntimeConfig({
|
|
cfg,
|
|
port: 18789,
|
|
host: "0.0.0.0",
|
|
}),
|
|
).rejects.toThrow("gateway bind=custom requested 192.168.1.100 but resolved 0.0.0.0");
|
|
});
|
|
});
|
|
});
|