




















@@ -1,17 +1,25 @@
11import { afterEach, describe, expect, it, vi } from "vitest";
22import { TwilioApiError, twilioApiRequest } from "./api.js";
334-const originalFetch = globalThis.fetch;
4+const apiMocks = vi.hoisted(() => ({
5+fetchWithSsrFGuard: vi.fn(),
6+}));
7+8+vi.mock("openclaw/plugin-sdk/ssrf-runtime", () => ({
9+fetchWithSsrFGuard: apiMocks.fetchWithSsrFGuard,
10+}));
511612describe("twilioApiRequest", () => {
713afterEach(() => {
8-globalThis.fetch = originalFetch;
14+apiMocks.fetchWithSsrFGuard.mockReset();
915});
10161117it("posts form bodies with basic auth and parses json", async () => {
12-globalThis.fetch = vi.fn(async () => {
13-return new Response(JSON.stringify({ sid: "CA123" }), { status: 200 });
14-}) as unknown as typeof fetch;
18+const release = vi.fn(async () => {});
19+apiMocks.fetchWithSsrFGuard.mockResolvedValue({
20+response: new Response(JSON.stringify({ sid: "CA123" }), { status: 200 }),
21+ release,
22+});
15231624await expect(
1725twilioApiRequest({
@@ -26,8 +34,10 @@ describe("twilioApiRequest", () => {
2634}),
2735).resolves.toEqual({ sid: "CA123" });
283629-const [url, init] = vi.mocked(globalThis.fetch).mock.calls[0] ?? [];
37+const [{ url, init, auditContext, policy }] = apiMocks.fetchWithSsrFGuard.mock.calls[0] ?? [];
3038expect(url).toBe("https://api.twilio.com/Calls.json");
39+expect(auditContext).toBe("voice-call.twilio.api");
40+expect(policy).toEqual({ allowedHostnames: ["api.twilio.com"] });
3141expect(init).toEqual(
3242expect.objectContaining({
3343method: "POST",
@@ -44,14 +54,19 @@ describe("twilioApiRequest", () => {
4454expect(requestBody.toString()).toBe(
4555"To=%2B14155550123&StatusCallbackEvent=initiated&StatusCallbackEvent=completed",
4656);
57+expect(release).toHaveBeenCalledTimes(1);
4758});
48594960it("passes through URLSearchParams, allows 404s, and returns undefined for empty bodies", async () => {
5061const responses = [
5162new Response(null, { status: 204 }),
5263new Response("missing", { status: 404 }),
5364];
54-globalThis.fetch = vi.fn(async () => responses.shift()!) as unknown as typeof fetch;
65+const release = vi.fn(async () => {});
66+apiMocks.fetchWithSsrFGuard.mockImplementation(async () => ({
67+response: responses.shift()!,
68+ release,
69+}));
55705671await expect(
5772twilioApiRequest({
@@ -73,12 +88,15 @@ describe("twilioApiRequest", () => {
7388allowNotFound: true,
7489}),
7590).resolves.toBeUndefined();
91+expect(release).toHaveBeenCalledTimes(2);
7692});
77937894it("throws twilio api errors for non-ok responses", async () => {
79-globalThis.fetch = vi.fn(
80-async () => new Response("bad request", { status: 400 }),
81-) as unknown as typeof fetch;
95+const release = vi.fn(async () => {});
96+apiMocks.fetchWithSsrFGuard.mockResolvedValue({
97+response: new Response("bad request", { status: 400 }),
98+ release,
99+});
8210083101await expect(
84102twilioApiRequest({
@@ -89,19 +107,21 @@ describe("twilioApiRequest", () => {
89107body: {},
90108}),
91109).rejects.toThrow("Twilio API error: 400 bad request");
110+expect(release).toHaveBeenCalledTimes(1);
92111});
9311294113it("exposes structured Twilio error codes from json error bodies", async () => {
95-globalThis.fetch = vi.fn(
96-async () =>
97-new Response(
98-JSON.stringify({
99-code: 21220,
100-message: "Call is not in-progress. Cannot redirect.",
101-}),
102-{ status: 400 },
103-),
104-) as unknown as typeof fetch;
114+const release = vi.fn(async () => {});
115+apiMocks.fetchWithSsrFGuard.mockResolvedValue({
116+response: new Response(
117+JSON.stringify({
118+code: 21220,
119+message: "Call is not in-progress. Cannot redirect.",
120+}),
121+{ status: 400 },
122+),
123+ release,
124+});
105125106126await expect(
107127twilioApiRequest({
@@ -117,5 +137,6 @@ describe("twilioApiRequest", () => {
117137twilioCode: 21220,
118138message: "Twilio API error: 400 Call is not in-progress. Cannot redirect.",
119139} satisfies Partial<TwilioApiError>);
140+expect(release).toHaveBeenCalledTimes(1);
120141});
121142});
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。