143 lines
4.1 KiB
TypeScript
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));
|
|
});
|
|
});
|