Files
Moltbot/src/gateway/server-http.hooks-request-timeout.test.ts
2026-02-23 05:45:54 +00:00

143 lines
4.1 KiB
TypeScript

import type { IncomingMessage, ServerResponse } from "node:http";
import { beforeEach, describe, expect, test, vi } from "vitest";
import type { createSubsystemLogger } from "../logging/subsystem.js";
import type { HooksConfigResolved } from "./hooks.js";
const { readJsonBodyMock } = vi.hoisted(() => ({
readJsonBodyMock: vi.fn(),
}));
vi.mock("./hooks.js", async (importOriginal) => {
const actual = await importOriginal<typeof import("./hooks.js")>();
return {
...actual,
readJsonBody: readJsonBodyMock,
};
});
import { createHooksRequestHandler } from "./server-http.js";
type HooksHandlerDeps = Parameters<typeof createHooksRequestHandler>[0];
function createHooksConfig(): HooksConfigResolved {
return {
basePath: "/hooks",
token: "hook-secret",
maxBodyBytes: 1024,
mappings: [],
agentPolicy: {
defaultAgentId: "main",
knownAgentIds: new Set(["main"]),
allowedAgentIds: undefined,
},
sessionPolicy: {
allowRequestSessionKey: false,
defaultSessionKey: undefined,
allowedSessionKeyPrefixes: undefined,
},
};
}
function createRequest(params?: {
authorization?: string;
remoteAddress?: string;
}): IncomingMessage {
return {
method: "POST",
url: "/hooks/wake",
headers: {
host: "127.0.0.1:18789",
authorization: params?.authorization ?? "Bearer hook-secret",
},
socket: { remoteAddress: params?.remoteAddress ?? "127.0.0.1" },
} as IncomingMessage;
}
function createResponse(): {
res: ServerResponse;
end: ReturnType<typeof vi.fn>;
setHeader: ReturnType<typeof vi.fn>;
} {
const setHeader = vi.fn();
const end = vi.fn();
const res = {
statusCode: 200,
setHeader,
end,
} as unknown as ServerResponse;
return { res, end, setHeader };
}
function createHandler(params?: {
dispatchWakeHook?: HooksHandlerDeps["dispatchWakeHook"];
dispatchAgentHook?: HooksHandlerDeps["dispatchAgentHook"];
}) {
return createHooksRequestHandler({
getHooksConfig: () => createHooksConfig(),
bindHost: "127.0.0.1",
port: 18789,
logHooks: {
warn: vi.fn(),
debug: vi.fn(),
info: vi.fn(),
error: vi.fn(),
} as unknown as ReturnType<typeof createSubsystemLogger>,
dispatchWakeHook:
params?.dispatchWakeHook ??
((() => {
return;
}) as HooksHandlerDeps["dispatchWakeHook"]),
dispatchAgentHook:
params?.dispatchAgentHook ?? ((() => "run-1") as HooksHandlerDeps["dispatchAgentHook"]),
});
}
describe("createHooksRequestHandler timeout status mapping", () => {
beforeEach(() => {
readJsonBodyMock.mockClear();
});
test("returns 408 for request body timeout", async () => {
readJsonBodyMock.mockResolvedValue({ ok: false, error: "request body timeout" });
const dispatchWakeHook = vi.fn();
const dispatchAgentHook = vi.fn(() => "run-1");
const handler = createHandler({ dispatchWakeHook, dispatchAgentHook });
const req = createRequest();
const { res, end } = createResponse();
const handled = await handler(req, res);
expect(handled).toBe(true);
expect(res.statusCode).toBe(408);
expect(end).toHaveBeenCalledWith(JSON.stringify({ ok: false, error: "request body timeout" }));
expect(dispatchWakeHook).not.toHaveBeenCalled();
expect(dispatchAgentHook).not.toHaveBeenCalled();
});
test("shares hook auth rate-limit bucket across ipv4 and ipv4-mapped ipv6 forms", async () => {
const handler = createHandler();
for (let i = 0; i < 20; i++) {
const req = createRequest({
authorization: "Bearer wrong",
remoteAddress: "1.2.3.4",
});
const { res } = createResponse();
const handled = await handler(req, res);
expect(handled).toBe(true);
expect(res.statusCode).toBe(401);
}
const mappedReq = createRequest({
authorization: "Bearer wrong",
remoteAddress: "::ffff:1.2.3.4",
});
const { res: mappedRes, setHeader } = createResponse();
const handled = await handler(mappedReq, mappedRes);
expect(handled).toBe(true);
expect(mappedRes.statusCode).toBe(429);
expect(setHeader).toHaveBeenCalledWith("Retry-After", expect.any(String));
});
});