
























@@ -0,0 +1,160 @@
1+import { Readable } from "node:stream";
2+import { beforeEach, describe, expect, it, vi } from "vitest";
3+import { handleAdminHttpRpcRequest } from "./handler.js";
4+import { listAdminHttpRpcAllowedMethods } from "./methods.js";
5+6+const { dispatchGatewayMethod } = vi.hoisted(() => ({
7+dispatchGatewayMethod: vi.fn(),
8+}));
9+10+vi.mock("openclaw/plugin-sdk/gateway-method-runtime", () => ({
11+ dispatchGatewayMethod,
12+}));
13+14+type CapturedResponse = {
15+statusCode: number;
16+headers: Record<string, string | number | readonly string[]>;
17+body: string;
18+};
19+20+function createRequest(body: unknown, method = "POST") {
21+const req = Readable.from([typeof body === "string" ? body : JSON.stringify(body)]);
22+Object.assign(req, {
23+ method,
24+url: "/api/v1/admin/rpc",
25+headers: {
26+"content-type": "application/json",
27+},
28+});
29+return req as import("node:http").IncomingMessage;
30+}
31+32+function createResponse() {
33+const captured: CapturedResponse = {
34+statusCode: 200,
35+headers: {},
36+body: "",
37+};
38+const res = {
39+get statusCode() {
40+return captured.statusCode;
41+},
42+set statusCode(value: number) {
43+captured.statusCode = value;
44+},
45+setHeader(name: string, value: string | number | readonly string[]) {
46+captured.headers[name.toLowerCase()] = value;
47+},
48+end(chunk?: string | Buffer) {
49+captured.body = Buffer.isBuffer(chunk) ? chunk.toString("utf8") : (chunk ?? "");
50+},
51+} as import("node:http").ServerResponse;
52+return { res, captured };
53+}
54+55+async function invoke(body: unknown, method = "POST") {
56+const { res, captured } = createResponse();
57+const handled = await handleAdminHttpRpcRequest(createRequest(body, method), res);
58+return {
59+ handled,
60+ captured,
61+json: captured.body ? (JSON.parse(captured.body) as unknown) : undefined,
62+};
63+}
64+65+describe("admin-http-rpc plugin handler", () => {
66+beforeEach(() => {
67+dispatchGatewayMethod.mockReset();
68+});
69+70+it("returns the allowlist without dispatching through the Gateway", async () => {
71+const result = await invoke({ id: "1", method: "commands.list" });
72+73+expect(result.handled).toBe(true);
74+expect(result.captured.statusCode).toBe(200);
75+expect(result.json).toEqual({
76+id: "1",
77+ok: true,
78+payload: {
79+methods: listAdminHttpRpcAllowedMethods(),
80+},
81+});
82+expect(dispatchGatewayMethod).not.toHaveBeenCalled();
83+});
84+85+it("dispatches allowed methods through the authenticated plugin request scope", async () => {
86+dispatchGatewayMethod.mockResolvedValueOnce({
87+ok: true,
88+payload: { status: "ok" },
89+meta: { requestId: "abc" },
90+});
91+92+const result = await invoke({
93+id: "cfg",
94+method: "config.get",
95+params: { path: "gateway" },
96+});
97+98+expect(dispatchGatewayMethod).toHaveBeenCalledWith("config.get", { path: "gateway" });
99+expect(result.captured.statusCode).toBe(200);
100+expect(result.json).toEqual({
101+id: "cfg",
102+ok: true,
103+payload: { status: "ok" },
104+meta: { requestId: "abc" },
105+});
106+});
107+108+it("rejects methods outside the admin HTTP RPC allowlist", async () => {
109+const result = await invoke({ id: "bad", method: "sessions.send" });
110+111+expect(dispatchGatewayMethod).not.toHaveBeenCalled();
112+expect(result.captured.statusCode).toBe(400);
113+expect(result.json).toEqual({
114+id: "bad",
115+ok: false,
116+error: {
117+code: "INVALID_REQUEST",
118+message: "admin HTTP RPC method is not supported: sessions.send",
119+},
120+});
121+});
122+123+it("maps Gateway errors to HTTP status codes", async () => {
124+dispatchGatewayMethod.mockResolvedValueOnce({
125+ok: false,
126+error: { code: "NOT_PAIRED", message: "pair first" },
127+});
128+129+const result = await invoke({ id: "node", method: "node.list" });
130+131+expect(result.captured.statusCode).toBe(409);
132+expect(result.json).toEqual({
133+id: "node",
134+ok: false,
135+error: { code: "NOT_PAIRED", message: "pair first" },
136+});
137+});
138+139+it("rejects invalid request bodies before dispatch", async () => {
140+const result = await invoke({ id: "missing" });
141+142+expect(result.captured.statusCode).toBe(400);
143+expect(result.json).toEqual({
144+ok: false,
145+error: {
146+type: "invalid_request",
147+message: "method must be a non-empty string",
148+},
149+});
150+expect(dispatchGatewayMethod).not.toHaveBeenCalled();
151+});
152+153+it("only accepts POST", async () => {
154+const result = await invoke({ method: "status" }, "GET");
155+156+expect(result.captured.statusCode).toBe(405);
157+expect(result.captured.headers.allow).toBe("POST");
158+expect(dispatchGatewayMethod).not.toHaveBeenCalled();
159+});
160+});
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。