11import { afterEach , describe , expect , it , vi } from "vitest" ;
22import { TwilioApiError , twilioApiRequest } from "./api.js" ;
33
4- 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+ } ) ) ;
511
612describe ( "twilioApiRequest" , ( ) => {
713 afterEach ( ( ) => {
8- globalThis . fetch = originalFetch ;
14+ apiMocks . fetchWithSsrFGuard . mockReset ( ) ;
915 } ) ;
1016
1117 it ( "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+ } ) ;
1523
1624 await expect (
1725 twilioApiRequest ( {
@@ -26,8 +34,10 @@ describe("twilioApiRequest", () => {
2634 } ) ,
2735 ) . resolves . toEqual ( { sid : "CA123" } ) ;
2836
29- const [ url , init ] = vi . mocked ( globalThis . fetch ) . mock . calls [ 0 ] ?? [ ] ;
37+ const [ { url, init, auditContext , policy } ] = apiMocks . fetchWithSsrFGuard . mock . calls [ 0 ] ?? [ ] ;
3038 expect ( url ) . toBe ( "https://api.twilio.com/Calls.json" ) ;
39+ expect ( auditContext ) . toBe ( "voice-call.twilio.api" ) ;
40+ expect ( policy ) . toEqual ( { allowedHostnames : [ "api.twilio.com" ] } ) ;
3141 expect ( init ) . toEqual (
3242 expect . objectContaining ( {
3343 method : "POST" ,
@@ -44,14 +54,19 @@ describe("twilioApiRequest", () => {
4454 expect ( requestBody . toString ( ) ) . toBe (
4555 "To=%2B14155550123&StatusCallbackEvent=initiated&StatusCallbackEvent=completed" ,
4656 ) ;
57+ expect ( release ) . toHaveBeenCalledTimes ( 1 ) ;
4758 } ) ;
4859
4960 it ( "passes through URLSearchParams, allows 404s, and returns undefined for empty bodies" , async ( ) => {
5061 const responses = [
5162 new Response ( null , { status : 204 } ) ,
5263 new 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+ } ) ) ;
5570
5671 await expect (
5772 twilioApiRequest ( {
@@ -73,12 +88,15 @@ describe("twilioApiRequest", () => {
7388 allowNotFound : true ,
7489 } ) ,
7590 ) . resolves . toBeUndefined ( ) ;
91+ expect ( release ) . toHaveBeenCalledTimes ( 2 ) ;
7692 } ) ;
7793
7894 it ( "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+ } ) ;
82100
83101 await expect (
84102 twilioApiRequest ( {
@@ -89,19 +107,21 @@ describe("twilioApiRequest", () => {
89107 body : { } ,
90108 } ) ,
91109 ) . rejects . toThrow ( "Twilio API error: 400 bad request" ) ;
110+ expect ( release ) . toHaveBeenCalledTimes ( 1 ) ;
92111 } ) ;
93112
94113 it ( "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+ } ) ;
105125
106126 await expect (
107127 twilioApiRequest ( {
@@ -117,5 +137,6 @@ describe("twilioApiRequest", () => {
117137 twilioCode : 21220 ,
118138 message : "Twilio API error: 400 Call is not in-progress. Cannot redirect." ,
119139 } satisfies Partial < TwilioApiError > ) ;
140+ expect ( release ) . toHaveBeenCalledTimes ( 1 ) ;
120141 } ) ;
121142} ) ;
0 commit comments