@@ -51,7 +51,11 @@ import Testing
51
51
52
52
func executeHandler( for request: URLRequest ) async throws -> ( HTTPURLResponse , Data ) {
53
53
guard let handler = requestHandler else {
54
- throw MockURLProtocolError . noRequestHandler
54
+ throw NSError (
55
+ domain: " MockURLProtocolError " , code: 0 ,
56
+ userInfo: [
57
+ NSLocalizedDescriptionKey: " No request handler set "
58
+ ] )
55
59
}
56
60
return try await handler ( request)
57
61
}
@@ -123,11 +127,6 @@ import Testing
123
127
override func stopLoading( ) { }
124
128
}
125
129
126
- enum MockURLProtocolError : Swift . Error {
127
- case noRequestHandler
128
- case invalidURL
129
- }
130
-
131
130
// MARK: -
132
131
133
132
@Suite ( " HTTP Client Transport Tests " , . serialized)
@@ -140,7 +139,11 @@ import Testing
140
139
configuration. protocolClasses = [ MockURLProtocol . self]
141
140
142
141
let transport = HTTPClientTransport (
143
- endpoint: testEndpoint, configuration: configuration, streaming: false , logger: nil )
142
+ endpoint: testEndpoint,
143
+ configuration: configuration,
144
+ streaming: false ,
145
+ logger: nil
146
+ )
144
147
145
148
try await transport. connect ( )
146
149
await transport. disconnect ( )
@@ -152,7 +155,11 @@ import Testing
152
155
configuration. protocolClasses = [ MockURLProtocol . self]
153
156
154
157
let transport = HTTPClientTransport (
155
- endpoint: testEndpoint, configuration: configuration, streaming: false , logger: nil )
158
+ endpoint: testEndpoint,
159
+ configuration: configuration,
160
+ streaming: false ,
161
+ logger: nil
162
+ )
156
163
try await transport. connect ( )
157
164
158
165
let messageData = #"{"jsonrpc":"2.0","method":"initialize","id":1}"# . data ( using: . utf8) !
@@ -190,7 +197,11 @@ import Testing
190
197
configuration. protocolClasses = [ MockURLProtocol . self]
191
198
192
199
let transport = HTTPClientTransport (
193
- endpoint: testEndpoint, configuration: configuration, streaming: false , logger: nil )
200
+ endpoint: testEndpoint,
201
+ configuration: configuration,
202
+ streaming: false ,
203
+ logger: nil
204
+ )
194
205
try await transport. connect ( )
195
206
196
207
let messageData = #"{"jsonrpc":"2.0","method":"initialize","id":1}"# . data ( using: . utf8) !
@@ -220,7 +231,11 @@ import Testing
220
231
configuration. protocolClasses = [ MockURLProtocol . self]
221
232
222
233
let transport = HTTPClientTransport (
223
- endpoint: testEndpoint, configuration: configuration, streaming: false , logger: nil )
234
+ endpoint: testEndpoint,
235
+ configuration: configuration,
236
+ streaming: false ,
237
+ logger: nil
238
+ )
224
239
try await transport. connect ( )
225
240
226
241
let initialSessionID = " existing-session-abc "
@@ -265,7 +280,11 @@ import Testing
265
280
configuration. protocolClasses = [ MockURLProtocol . self]
266
281
267
282
let transport = HTTPClientTransport (
268
- endpoint: testEndpoint, configuration: configuration)
283
+ endpoint: testEndpoint,
284
+ configuration: configuration,
285
+ streaming: false ,
286
+ logger: nil
287
+ )
269
288
try await transport. connect ( )
270
289
271
290
let messageData = #"{"jsonrpc":"2.0","method":"test","id":3}"# . data ( using: . utf8) !
@@ -298,7 +317,11 @@ import Testing
298
317
configuration. protocolClasses = [ MockURLProtocol . self]
299
318
300
319
let transport = HTTPClientTransport (
301
- endpoint: testEndpoint, configuration: configuration)
320
+ endpoint: testEndpoint,
321
+ configuration: configuration,
322
+ streaming: false ,
323
+ logger: nil
324
+ )
302
325
try await transport. connect ( )
303
326
304
327
let messageData = #"{"jsonrpc":"2.0","method":"test","id":4}"# . data ( using: . utf8) !
@@ -331,7 +354,11 @@ import Testing
331
354
configuration. protocolClasses = [ MockURLProtocol . self]
332
355
333
356
let transport = HTTPClientTransport (
334
- endpoint: testEndpoint, configuration: configuration)
357
+ endpoint: testEndpoint,
358
+ configuration: configuration,
359
+ streaming: false ,
360
+ logger: nil
361
+ )
335
362
try await transport. connect ( )
336
363
337
364
let initialSessionID = " expired-session-xyz "
@@ -385,8 +412,11 @@ import Testing
385
412
configuration. protocolClasses = [ MockURLProtocol . self]
386
413
387
414
let transport = HTTPClientTransport (
388
- endpoint: testEndpoint, configuration: configuration, streaming: true ,
389
- logger: nil )
415
+ endpoint: testEndpoint,
416
+ configuration: configuration,
417
+ streaming: true ,
418
+ logger: nil
419
+ )
390
420
391
421
let eventString = " id: event1 \n data: { \" key \" : \" value \" } \n \n "
392
422
let sseEventData = eventString. data ( using: . utf8) !
@@ -419,8 +449,11 @@ import Testing
419
449
configuration. protocolClasses = [ MockURLProtocol . self]
420
450
421
451
let transport = HTTPClientTransport (
422
- endpoint: testEndpoint, configuration: configuration, streaming: true ,
423
- logger: nil )
452
+ endpoint: testEndpoint,
453
+ configuration: configuration,
454
+ streaming: true ,
455
+ logger: nil
456
+ )
424
457
425
458
let eventString = " id: event1 \r \n data: { \" key \" : \" value \" } \r \n \n "
426
459
let sseEventData = eventString. data ( using: . utf8) !
@@ -448,6 +481,152 @@ import Testing
448
481
#expect( receivedData == expectedData)
449
482
}
450
483
#endif // !canImport(FoundationNetworking)
451
- }
452
484
485
+ @Test (
486
+ " Client with HTTP Transport complete flow " , . httpClientTransportSetup,
487
+ . timeLimit( . minutes( 1 ) ) )
488
+ func testClientFlow( ) async throws {
489
+ let configuration = URLSessionConfiguration . ephemeral
490
+ configuration. protocolClasses = [ MockURLProtocol . self]
491
+
492
+ let transport = HTTPClientTransport (
493
+ endpoint: testEndpoint,
494
+ configuration: configuration,
495
+ streaming: false ,
496
+ logger: nil
497
+ )
498
+
499
+ let client = Client ( name: " TestClient " , version: " 1.0.0 " )
500
+
501
+ // Use an actor to track request sequence
502
+ actor RequestTracker {
503
+ enum RequestType {
504
+ case initialize
505
+ case callTool
506
+ }
507
+
508
+ private( set) var lastRequest : RequestType ?
509
+
510
+ func setRequest( _ type: RequestType ) {
511
+ lastRequest = type
512
+ }
513
+
514
+ func getLastRequest( ) -> RequestType ? {
515
+ return lastRequest
516
+ }
517
+ }
518
+
519
+ let tracker = RequestTracker ( )
520
+
521
+ // Setup mock responses
522
+ await MockURLProtocol . requestHandlerStorage. setHandler {
523
+ [ testEndpoint, tracker] ( request: URLRequest ) in
524
+ switch request. httpMethod {
525
+ case " GET " :
526
+ #expect(
527
+ request. allHTTPHeaderFields ? [ " Accept " ] ? . contains ( " text/event-stream " )
528
+ == true )
529
+ case " POST " :
530
+ #expect(
531
+ request. allHTTPHeaderFields ? [ " Accept " ] ? . contains ( " application/json " ) == true
532
+ )
533
+ default :
534
+ Issue . record (
535
+ " Unsupported HTTP method \( String ( describing: request. httpMethod) ) " )
536
+ }
537
+
538
+ #expect( request. url == testEndpoint)
539
+
540
+ let bodyData = request. readBody ( )
541
+
542
+ guard let bodyData = bodyData,
543
+ let json = try JSONSerialization . jsonObject ( with: bodyData) as? [ String : Any ] ,
544
+ let method = json [ " method " ] as? String
545
+ else {
546
+ throw NSError (
547
+ domain: " MockURLProtocolError " , code: 0 ,
548
+ userInfo: [
549
+ NSLocalizedDescriptionKey: " Invalid JSON-RPC message \( #file) : \( #line) "
550
+ ] )
551
+ }
552
+
553
+ if method == " initialize " {
554
+ await tracker. setRequest ( . initialize)
555
+
556
+ let requestID = json [ " id " ] as! String
557
+ let result = Initialize . Result (
558
+ protocolVersion: Version . latest,
559
+ capabilities: . init( tools: . init( ) ) ,
560
+ serverInfo: . init( name: " Mock Server " , version: " 0.0.1 " ) ,
561
+ instructions: nil
562
+ )
563
+ let response = Initialize . response ( id: . string( requestID) , result: result)
564
+ let responseData = try JSONEncoder ( ) . encode ( response)
565
+
566
+ let httpResponse = HTTPURLResponse (
567
+ url: testEndpoint, statusCode: 200 , httpVersion: " HTTP/1.1 " ,
568
+ headerFields: [ " Content-Type " : " application/json " ] ) !
569
+ return ( httpResponse, responseData)
570
+ } else if method == " tools/call " {
571
+ // Verify initialize was called first
572
+ if let lastRequest = await tracker. getLastRequest ( ) , lastRequest != . initialize
573
+ {
574
+ #expect( Bool ( false ) , " Initialize should be called before callTool " )
575
+ }
576
+
577
+ await tracker. setRequest ( . callTool)
578
+
579
+ let params = json [ " params " ] as? [ String : Any ]
580
+ let toolName = params ? [ " name " ] as? String
581
+ #expect( toolName == " calculator " )
582
+
583
+ let requestID = json [ " id " ] as! String
584
+ let result = CallTool . Result ( content: [ . text( " 42 " ) ] )
585
+ let response = CallTool . response ( id: . string( requestID) , result: result)
586
+ let responseData = try JSONEncoder ( ) . encode ( response)
587
+
588
+ let httpResponse = HTTPURLResponse (
589
+ url: testEndpoint, statusCode: 200 , httpVersion: " HTTP/1.1 " ,
590
+ headerFields: [ " Content-Type " : " application/json " ] ) !
591
+ return ( httpResponse, responseData)
592
+ } else if method == " notifications/initialized " {
593
+ // Ignore initialized notifications
594
+ let httpResponse = HTTPURLResponse (
595
+ url: testEndpoint, statusCode: 200 , httpVersion: " HTTP/1.1 " ,
596
+ headerFields: [ " Content-Type " : " application/json " ] ) !
597
+ return ( httpResponse, Data ( ) )
598
+ } else {
599
+ throw NSError (
600
+ domain: " MockURLProtocolError " , code: 0 ,
601
+ userInfo: [
602
+ NSLocalizedDescriptionKey:
603
+ " Unexpected request method: \( method) \( #file) : \( #line) "
604
+ ] )
605
+ }
606
+ }
607
+
608
+ // Execute the complete flow
609
+ try await client. connect ( transport: transport)
610
+
611
+ // Step 1: Initialize client
612
+ let initResult = try await client. initialize ( )
613
+ #expect( initResult. protocolVersion == Version . latest)
614
+ #expect( initResult. capabilities. tools != nil )
615
+
616
+ // Step 2: Call a tool
617
+ let toolResult = try await client. callTool ( name: " calculator " )
618
+ #expect( toolResult. content. count == 1 )
619
+ if case let . text( text) = toolResult. content [ 0 ] {
620
+ #expect( text == " 42 " )
621
+ } else {
622
+ #expect( Bool ( false ) , " Expected text content " )
623
+ }
624
+
625
+ // Step 3: Verify request sequence
626
+ #expect( await tracker. getLastRequest ( ) == . callTool)
627
+
628
+ // Step 4: Disconnect
629
+ await client. disconnect ( )
630
+ }
631
+ }
453
632
#endif // swift(>=6.1)
0 commit comments