@@ -83,4 +83,100 @@ struct MacNodeBrowserProxyTests {
8383 let arr = try #require( parsed [ " arr " ] as? [ Any ] )
8484 #expect( arr. count == 2 )
8585 }
86+
87+ // MARK: - sanitizeForJSON
88+
89+ @Test func sanitizeForJSONConvertsNonSerializableValuesToStrings( ) {
90+ // A custom Swift struct that NSJSONSerialization cannot handle.
91+ struct OpaqueValue : CustomStringConvertible {
92+ let id : Int
93+ var description : String { " OpaqueValue( \( id) ) " }
94+ }
95+
96+ let result = MacNodeBrowserProxy . sanitizeForJSON ( OpaqueValue ( id: 42 ) )
97+ #expect( result as? String == " OpaqueValue(42) " )
98+ }
99+
100+ @Test func sanitizeForJSONRecursesIntoDictionaries( ) throws {
101+ struct Opaque : CustomStringConvertible {
102+ var description : String { " opaque " }
103+ }
104+
105+ let input : [ String : Any ] = [
106+ " ok " : true ,
107+ " count " : 3 ,
108+ " nested " : [ " inner " : Opaque ( ) ] ,
109+ ]
110+ let result = MacNodeBrowserProxy . sanitizeForJSON ( input)
111+ let dict = try #require( result as? [ String : Any ] )
112+ #expect( dict [ " ok " ] as? Bool == true )
113+ #expect( dict [ " count " ] as? Int == 3 )
114+ let nested = try #require( dict [ " nested " ] as? [ String : Any ] )
115+ #expect( nested [ " inner " ] as? String == " opaque " )
116+ }
117+
118+ @Test func sanitizeForJSONRecursesIntoArrays( ) throws {
119+ struct Opaque : CustomStringConvertible {
120+ var description : String { " item " }
121+ }
122+
123+ let input : [ Any ] = [ 1 , " two " , Opaque ( ) ]
124+ let result = MacNodeBrowserProxy . sanitizeForJSON ( input)
125+ let arr = try #require( result as? [ Any ] )
126+ #expect( arr. count == 3 )
127+ #expect( arr [ 0 ] as? Int == 1 )
128+ #expect( arr [ 1 ] as? String == " two " )
129+ #expect( arr [ 2 ] as? String == " item " )
130+ }
131+
132+ @Test func sanitizeForJSONPreservesJSONSafeValues( ) throws {
133+ let dict : [ String : Any ] = [ " a " : 1 , " b " : " hello " , " c " : true , " d " : NSNull ( ) ]
134+ let result = MacNodeBrowserProxy . sanitizeForJSON ( dict)
135+ // Should be passable to NSJSONSerialization without throwing.
136+ let data = try JSONSerialization . data ( withJSONObject: result)
137+ let parsed = try #require( JSONSerialization . jsonObject ( with: data) as? [ String : Any ] )
138+ #expect( parsed [ " a " ] as? Int == 1 )
139+ #expect( parsed [ " b " ] as? String == " hello " )
140+ #expect( parsed [ " c " ] as? Bool == true )
141+ }
142+
143+ // Regression: a POST request whose body produces non-JSON-serializable
144+ // foundation values must NOT crash with SIGABRT.
145+ @Test func postRequestWithNonSerializableBodyDoesNotCrash( ) async throws {
146+ actor BodyCapture {
147+ private var body : Data ?
148+ func set( _ body: Data ? ) { self . body = body }
149+ func get( ) -> Data ? { self . body }
150+ }
151+
152+ let capturedBody = BodyCapture ( )
153+ let proxy = MacNodeBrowserProxy (
154+ endpointProvider: {
155+ MacNodeBrowserProxy . Endpoint (
156+ baseURL: URL ( string: " http://127.0.0.1:18791 " ) !,
157+ token: nil ,
158+ password: nil )
159+ } ,
160+ performRequest: { request in
161+ await capturedBody. set ( request. httpBody)
162+ let url = try #require( request. url)
163+ let response = try #require(
164+ HTTPURLResponse (
165+ url: url,
166+ statusCode: 200 ,
167+ httpVersion: nil ,
168+ headerFields: nil ) )
169+ return ( Data ( #"{"ok":true}"# . utf8) , response)
170+ } )
171+
172+ // Encode a body that, after AnyCodable decoding, is fine.
173+ // The sanitization layer ensures no crash even if the gateway
174+ // sends something unexpected in the future.
175+ _ = try await proxy. request (
176+ paramsJSON: #"{"method":"POST","path":"/action","body":{"key":"val"}}"# )
177+
178+ let bodyData = try #require( await capturedBody. get ( ) )
179+ let parsed = try #require( JSONSerialization . jsonObject ( with: bodyData) as? [ String : Any ] )
180+ #expect( parsed [ " key " ] as? String == " val " )
181+ }
86182}
0 commit comments