mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-29 21:08:43 +00:00
346 lines
11 KiB
TypeScript
346 lines
11 KiB
TypeScript
import { describe, expect, it, vi } from "vitest";
|
|
import {
|
|
AUTH_TOKEN,
|
|
AUTH_NONE,
|
|
createRequest,
|
|
createResponse,
|
|
dispatchRequest,
|
|
withGatewayServer,
|
|
} from "./server-http.test-harness.js";
|
|
import type { ReadinessChecker } from "./server/readiness.js";
|
|
import { withTempConfig } from "./test-temp-config.js";
|
|
|
|
describe("gateway OpenAI-compatible disabled HTTP routes", () => {
|
|
it("returns 404 when compat endpoints are disabled", async () => {
|
|
await withGatewayServer({
|
|
prefix: "openai-compat-disabled",
|
|
resolvedAuth: AUTH_NONE,
|
|
run: async (server) => {
|
|
for (const path of ["/v1/chat/completions", "/v1/responses"]) {
|
|
const req = createRequest({
|
|
path,
|
|
method: "POST",
|
|
headers: { "content-type": "application/json" },
|
|
});
|
|
const { res, getBody } = createResponse();
|
|
await dispatchRequest(server, req, res);
|
|
|
|
expect(res.statusCode, path).toBe(404);
|
|
expect(getBody(), path).toBe("Not Found");
|
|
}
|
|
},
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("gateway probe endpoints", () => {
|
|
it("returns detailed readiness payload for local /ready requests", async () => {
|
|
const getReadiness: ReadinessChecker = () => ({
|
|
ready: true,
|
|
failing: [],
|
|
uptimeMs: 45_000,
|
|
});
|
|
|
|
await withGatewayServer({
|
|
prefix: "probe-ready",
|
|
resolvedAuth: AUTH_NONE,
|
|
overrides: { getReadiness },
|
|
run: async (server) => {
|
|
const req = createRequest({ path: "/ready" });
|
|
const { res, getBody } = createResponse();
|
|
await dispatchRequest(server, req, res);
|
|
|
|
expect(res.statusCode).toBe(200);
|
|
expect(JSON.parse(getBody())).toEqual({ ready: true, failing: [], uptimeMs: 45_000 });
|
|
},
|
|
});
|
|
});
|
|
|
|
it("returns only readiness state for unauthenticated remote /ready requests", async () => {
|
|
const getReadiness: ReadinessChecker = () => ({
|
|
ready: false,
|
|
failing: ["discord", "telegram"],
|
|
uptimeMs: 8_000,
|
|
});
|
|
|
|
await withGatewayServer({
|
|
prefix: "probe-not-ready",
|
|
resolvedAuth: AUTH_NONE,
|
|
overrides: { getReadiness },
|
|
run: async (server) => {
|
|
const req = createRequest({
|
|
path: "/ready",
|
|
remoteAddress: "10.0.0.8",
|
|
host: "gateway.test",
|
|
});
|
|
const { res, getBody } = createResponse();
|
|
await dispatchRequest(server, req, res);
|
|
|
|
expect(res.statusCode).toBe(503);
|
|
expect(JSON.parse(getBody())).toEqual({ ready: false });
|
|
},
|
|
});
|
|
});
|
|
|
|
it("returns detailed readiness payload for authenticated remote /ready requests", async () => {
|
|
const getReadiness: ReadinessChecker = () => ({
|
|
ready: false,
|
|
failing: ["discord", "telegram"],
|
|
uptimeMs: 8_000,
|
|
});
|
|
|
|
await withGatewayServer({
|
|
prefix: "probe-remote-authenticated",
|
|
resolvedAuth: AUTH_TOKEN,
|
|
overrides: { getReadiness },
|
|
run: async (server) => {
|
|
const req = createRequest({
|
|
path: "/ready",
|
|
remoteAddress: "10.0.0.8",
|
|
host: "gateway.test",
|
|
authorization: "Bearer test-token",
|
|
});
|
|
const { res, getBody } = createResponse();
|
|
await dispatchRequest(server, req, res);
|
|
|
|
expect(res.statusCode).toBe(503);
|
|
expect(JSON.parse(getBody())).toEqual({
|
|
ready: false,
|
|
failing: ["discord", "telegram"],
|
|
uptimeMs: 8_000,
|
|
});
|
|
},
|
|
});
|
|
});
|
|
|
|
it("re-resolves auth for remote /ready requests after shared auth rotation", async () => {
|
|
const getReadiness: ReadinessChecker = () => ({
|
|
ready: false,
|
|
failing: ["discord", "telegram"],
|
|
uptimeMs: 8_000,
|
|
});
|
|
let currentAuth = AUTH_TOKEN;
|
|
|
|
await withGatewayServer({
|
|
prefix: "probe-remote-rotated-auth",
|
|
// `resolvedAuth` remains the static fallback; `getResolvedAuth` drives the rotated value.
|
|
resolvedAuth: AUTH_TOKEN,
|
|
overrides: {
|
|
getReadiness,
|
|
getResolvedAuth: () => currentAuth,
|
|
},
|
|
run: async (server) => {
|
|
const sendReady = async (authorization: string) => {
|
|
const req = createRequest({
|
|
path: "/ready",
|
|
remoteAddress: "10.0.0.8",
|
|
host: "gateway.test",
|
|
authorization,
|
|
});
|
|
const { res, getBody } = createResponse();
|
|
await dispatchRequest(server, req, res);
|
|
return { statusCode: res.statusCode, body: JSON.parse(getBody()) };
|
|
};
|
|
|
|
await expect(sendReady("Bearer test-token")).resolves.toEqual({
|
|
statusCode: 503,
|
|
body: {
|
|
ready: false,
|
|
failing: ["discord", "telegram"],
|
|
uptimeMs: 8_000,
|
|
},
|
|
});
|
|
|
|
currentAuth = {
|
|
...AUTH_TOKEN,
|
|
token: "rotated-token",
|
|
};
|
|
|
|
await expect(sendReady("Bearer test-token")).resolves.toEqual({
|
|
statusCode: 503,
|
|
body: { ready: false },
|
|
});
|
|
await expect(sendReady("Bearer rotated-token")).resolves.toEqual({
|
|
statusCode: 503,
|
|
body: {
|
|
ready: false,
|
|
failing: ["discord", "telegram"],
|
|
uptimeMs: 8_000,
|
|
},
|
|
});
|
|
},
|
|
});
|
|
});
|
|
|
|
it("hides readiness details when trusted-proxy auth violates browser origin policy", async () => {
|
|
const getReadiness: ReadinessChecker = () => ({
|
|
ready: false,
|
|
failing: ["discord", "telegram"],
|
|
uptimeMs: 8_000,
|
|
});
|
|
|
|
await withTempConfig({
|
|
prefix: "probe-remote-origin-rejected",
|
|
cfg: {
|
|
gateway: {
|
|
trustedProxies: ["10.0.0.1"],
|
|
controlUi: {
|
|
allowedOrigins: ["https://control.example"],
|
|
},
|
|
},
|
|
},
|
|
run: async () => {
|
|
await withGatewayServer({
|
|
prefix: "probe-remote-origin-rejected-server",
|
|
resolvedAuth: {
|
|
mode: "trusted-proxy",
|
|
allowTailscale: false,
|
|
trustedProxy: { userHeader: "x-forwarded-user" },
|
|
},
|
|
overrides: {
|
|
getReadiness,
|
|
},
|
|
run: async (server) => {
|
|
const req = createRequest({
|
|
path: "/ready",
|
|
remoteAddress: "10.0.0.1",
|
|
host: "gateway.test",
|
|
headers: {
|
|
origin: "https://evil.example",
|
|
forwarded: "for=203.0.113.10;proto=https;host=gateway.test",
|
|
"x-forwarded-user": "user@example.com",
|
|
"x-forwarded-proto": "https",
|
|
},
|
|
});
|
|
const { res, getBody } = createResponse();
|
|
await dispatchRequest(server, req, res);
|
|
|
|
expect(res.statusCode).toBe(503);
|
|
expect(JSON.parse(getBody())).toEqual({ ready: false });
|
|
},
|
|
});
|
|
},
|
|
});
|
|
});
|
|
|
|
it("returns typed internal error payload when readiness evaluation throws", async () => {
|
|
const getReadiness: ReadinessChecker = () => {
|
|
throw new Error("boom");
|
|
};
|
|
|
|
await withGatewayServer({
|
|
prefix: "probe-throws",
|
|
resolvedAuth: AUTH_NONE,
|
|
overrides: { getReadiness },
|
|
run: async (server) => {
|
|
const req = createRequest({ path: "/ready" });
|
|
const { res, getBody } = createResponse();
|
|
await dispatchRequest(server, req, res);
|
|
|
|
expect(res.statusCode).toBe(503);
|
|
expect(JSON.parse(getBody())).toEqual({ ready: false, failing: ["internal"], uptimeMs: 0 });
|
|
},
|
|
});
|
|
});
|
|
|
|
it("keeps /healthz shallow even when readiness checker reports failing channels", async () => {
|
|
const getReadiness: ReadinessChecker = () => ({
|
|
ready: false,
|
|
failing: ["discord"],
|
|
uptimeMs: 999,
|
|
});
|
|
|
|
await withGatewayServer({
|
|
prefix: "probe-healthz-unaffected",
|
|
resolvedAuth: AUTH_NONE,
|
|
overrides: { getReadiness },
|
|
run: async (server) => {
|
|
const req = createRequest({ path: "/healthz" });
|
|
const { res, getBody } = createResponse();
|
|
await dispatchRequest(server, req, res);
|
|
|
|
expect(res.statusCode).toBe(200);
|
|
expect(getBody()).toBe(JSON.stringify({ ok: true, status: "live" }));
|
|
},
|
|
});
|
|
});
|
|
|
|
it("serves /healthz before loading gateway config", async () => {
|
|
const getRuntimeConfig = vi.fn(() => {
|
|
throw new Error("config load blocked");
|
|
});
|
|
|
|
await withGatewayServer({
|
|
prefix: "probe-healthz-before-config",
|
|
resolvedAuth: AUTH_NONE,
|
|
overrides: { getRuntimeConfig },
|
|
run: async (server) => {
|
|
const req = createRequest({ path: "/healthz" });
|
|
const { res, getBody } = createResponse();
|
|
await dispatchRequest(server, req, res);
|
|
|
|
expect(res.statusCode).toBe(200);
|
|
expect(getBody()).toBe(JSON.stringify({ ok: true, status: "live" }));
|
|
expect(getRuntimeConfig).not.toHaveBeenCalled();
|
|
},
|
|
});
|
|
});
|
|
|
|
it("serves probes before stalled request stages", async () => {
|
|
const handleHooksRequest = vi.fn((): Promise<boolean> => new Promise(() => {}));
|
|
const getReadiness = vi.fn(() => ({
|
|
ready: true,
|
|
failing: [],
|
|
uptimeMs: 123,
|
|
}));
|
|
|
|
await withGatewayServer({
|
|
prefix: "probe-before-stalled-stages",
|
|
resolvedAuth: AUTH_NONE,
|
|
overrides: { getReadiness, handleHooksRequest },
|
|
run: async (server) => {
|
|
const healthReq = createRequest({ path: "/healthz" });
|
|
const healthResponse = createResponse();
|
|
await dispatchRequest(server, healthReq, healthResponse.res);
|
|
|
|
expect(healthResponse.res.statusCode).toBe(200);
|
|
expect(healthResponse.getBody()).toBe(JSON.stringify({ ok: true, status: "live" }));
|
|
|
|
const readyReq = createRequest({ path: "/readyz" });
|
|
const readyResponse = createResponse();
|
|
await dispatchRequest(server, readyReq, readyResponse.res);
|
|
|
|
expect(readyResponse.res.statusCode).toBe(200);
|
|
expect(JSON.parse(readyResponse.getBody())).toEqual({
|
|
ready: true,
|
|
failing: [],
|
|
uptimeMs: 123,
|
|
});
|
|
expect(handleHooksRequest).not.toHaveBeenCalled();
|
|
},
|
|
});
|
|
});
|
|
|
|
it("reflects readiness status on HEAD /readyz without a response body", async () => {
|
|
const getReadiness: ReadinessChecker = () => ({
|
|
ready: false,
|
|
failing: ["discord"],
|
|
uptimeMs: 5_000,
|
|
});
|
|
|
|
await withGatewayServer({
|
|
prefix: "probe-readyz-head",
|
|
resolvedAuth: AUTH_NONE,
|
|
overrides: { getReadiness },
|
|
run: async (server) => {
|
|
const req = createRequest({ path: "/readyz", method: "HEAD" });
|
|
const { res, getBody } = createResponse();
|
|
await dispatchRequest(server, req, res);
|
|
|
|
expect(res.statusCode).toBe(503);
|
|
expect(getBody()).toBe("");
|
|
},
|
|
});
|
|
});
|
|
});
|