@@ -56,7 +56,7 @@ class URLSessionTransportConverterTests: XCTestCase {
56
56
57
57
// swift-format-ignore: AllPublicDeclarationsHaveDocumentation
58
58
class URLSessionTransportBufferedTests : XCTestCase {
59
- var transport : ( any ClientTransport ) !
59
+ var transport : URLSessionTransport !
60
60
61
61
static override func setUp( ) { OpenAPIURLSession . debugLoggingEnabled = false }
62
62
@@ -66,7 +66,31 @@ class URLSessionTransportBufferedTests: XCTestCase {
66
66
67
67
func testBasicGet( ) async throws { try await testHTTPBasicGet ( transport: transport) }
68
68
69
- func testBasicPost( ) async throws { try await testHTTPBasicGet ( transport: transport) }
69
+ func testBasicPost( ) async throws { try await testHTTPBasicPost ( transport: transport) }
70
+
71
+ func testCancellation_beforeSendingHead( ) async throws {
72
+ try await testTaskCancelled ( . beforeSendingHead, transport: transport)
73
+ }
74
+
75
+ func testCancellation_beforeSendingRequestBody( ) async throws {
76
+ try await testTaskCancelled ( . beforeSendingRequestBody, transport: transport)
77
+ }
78
+
79
+ func testCancellation_partwayThroughSendingRequestBody( ) async throws {
80
+ try await testTaskCancelled ( . partwayThroughSendingRequestBody, transport: transport)
81
+ }
82
+
83
+ func testCancellation_beforeConsumingResponseBody( ) async throws {
84
+ try await testTaskCancelled ( . beforeConsumingResponseBody, transport: transport)
85
+ }
86
+
87
+ func testCancellation_partwayThroughConsumingResponseBody( ) async throws {
88
+ try await testTaskCancelled ( . partwayThroughConsumingResponseBody, transport: transport)
89
+ }
90
+
91
+ func testCancellation_afterConsumingResponseBody( ) async throws {
92
+ try await testTaskCancelled ( . afterConsumingResponseBody, transport: transport)
93
+ }
70
94
71
95
#if canImport(Darwin) // Only passes on Darwin because Linux doesn't replay the request body on 307.
72
96
func testHTTPRedirect_multipleIterationBehavior_succeeds( ) async throws {
@@ -89,7 +113,7 @@ class URLSessionTransportBufferedTests: XCTestCase {
89
113
90
114
// swift-format-ignore: AllPublicDeclarationsHaveDocumentation
91
115
class URLSessionTransportStreamingTests : XCTestCase {
92
- var transport : ( any ClientTransport ) !
116
+ var transport : URLSessionTransport !
93
117
94
118
static override func setUp( ) { OpenAPIURLSession . debugLoggingEnabled = false }
95
119
@@ -107,7 +131,31 @@ class URLSessionTransportStreamingTests: XCTestCase {
107
131
108
132
func testBasicGet( ) async throws { try await testHTTPBasicGet ( transport: transport) }
109
133
110
- func testBasicPost( ) async throws { try await testHTTPBasicGet ( transport: transport) }
134
+ func testBasicPost( ) async throws { try await testHTTPBasicPost ( transport: transport) }
135
+
136
+ func testCancellation_beforeSendingHead( ) async throws {
137
+ try await testTaskCancelled ( . beforeSendingHead, transport: transport)
138
+ }
139
+
140
+ func testCancellation_beforeSendingRequestBody( ) async throws {
141
+ try await testTaskCancelled ( . beforeSendingRequestBody, transport: transport)
142
+ }
143
+
144
+ func testCancellation_partwayThroughSendingRequestBody( ) async throws {
145
+ try await testTaskCancelled ( . partwayThroughSendingRequestBody, transport: transport)
146
+ }
147
+
148
+ func testCancellation_beforeConsumingResponseBody( ) async throws {
149
+ try await testTaskCancelled ( . beforeConsumingResponseBody, transport: transport)
150
+ }
151
+
152
+ func testCancellation_partwayThroughConsumingResponseBody( ) async throws {
153
+ try await testTaskCancelled ( . partwayThroughConsumingResponseBody, transport: transport)
154
+ }
155
+
156
+ func testCancellation_afterConsumingResponseBody( ) async throws {
157
+ try await testTaskCancelled ( . afterConsumingResponseBody, transport: transport)
158
+ }
111
159
112
160
#if canImport(Darwin) // Only passes on Darwin because Linux doesn't replay the request body on 307.
113
161
func testHTTPRedirect_multipleIterationBehavior_succeeds( ) async throws {
@@ -311,6 +359,171 @@ func testHTTPBasicPost(transport: any ClientTransport) async throws {
311
359
}
312
360
}
313
361
362
+ enum CancellationPoint: CaseIterable {
363
+ case beforeSendingHead
364
+ case beforeSendingRequestBody
365
+ case partwayThroughSendingRequestBody
366
+ case beforeConsumingResponseBody
367
+ case partwayThroughConsumingResponseBody
368
+ case afterConsumingResponseBody
369
+ }
370
+
371
+ func testTaskCancelled( _ cancellationPoint: CancellationPoint, transport: URLSessionTransport) async throws {
372
+ let requestPath = " /hello/world "
373
+ let requestBodyElements = [ " Hello, " , " world! " ]
374
+ let requestBodySequence = MockAsyncSequence ( elementsToVend: requestBodyElements, gatingProduction: true )
375
+ let requestBody = HTTPBody (
376
+ requestBodySequence,
377
+ length: . known( Int64 ( requestBodyElements. joined ( ) . lengthOfBytes ( using: . utf8) ) ) ,
378
+ iterationBehavior: . single
379
+ )
380
+
381
+ let responseBodyMessage = " Hey! "
382
+
383
+ let taskShouldCancel = XCTestExpectation ( description: " Concurrency task cancelled " )
384
+ let taskCancelled = XCTestExpectation ( description: " Concurrency task cancelled " )
385
+
386
+ try await withThrowingTaskGroup( of: Void . self) { group in
387
+ let serverPort = try await AsyncTestHTTP1Server . start ( connectionTaskGroup: & group) { connectionChannel in
388
+ try await connectionChannel . executeThenClose { inbound, outbound in
389
+ var requestPartIterator = inbound. makeAsyncIterator ( )
390
+ var accumulatedBody = ByteBuffer ( )
391
+ while let requestPart = try await requestPartIterator. next ( ) {
392
+ switch requestPart {
393
+ case . head( let head) :
394
+ XCTAssertEqual ( head. uri, requestPath)
395
+ XCTAssertEqual ( head. method, . POST)
396
+ case . body( let buffer) : accumulatedBody. writeImmutableBuffer ( buffer)
397
+ case . end:
398
+ switch cancellationPoint {
399
+ case . beforeConsumingResponseBody, . partwayThroughConsumingResponseBody,
400
+ . afterConsumingResponseBody:
401
+ XCTAssertEqual (
402
+ String ( decoding: accumulatedBody. readableBytesView, as: UTF8 . self) ,
403
+ requestBodyElements. joined ( )
404
+ )
405
+ case . beforeSendingHead, . beforeSendingRequestBody, . partwayThroughSendingRequestBody: break
406
+ }
407
+ try await outbound. write ( . head( . init( version: . http1_1, status: . ok) ) )
408
+ try await outbound. write ( . body( ByteBuffer ( string: responseBodyMessage) ) )
409
+ try await outbound. write ( . end( nil ) )
410
+ }
411
+ }
412
+ }
413
+ }
414
+ debug( " Server running on 127.0.0.1: \( serverPort) " )
415
+
416
+ let task = Task {
417
+ if case . beforeSendingHead = cancellationPoint {
418
+ taskShouldCancel. fulfill ( )
419
+ await fulfillment ( of: [ taskCancelled] )
420
+ }
421
+ debug ( " Client starting request " )
422
+ async let ( asyncResponse, asyncResponseBody) = try await transport. send (
423
+ HTTPRequest ( method: . post, scheme: nil , authority: nil , path: requestPath) ,
424
+ body: requestBody,
425
+ baseURL: URL ( string: " http://127.0.0.1: \( serverPort) " ) !,
426
+ operationID: " unused "
427
+ )
428
+
429
+ if case . beforeSendingRequestBody = cancellationPoint {
430
+ taskShouldCancel. fulfill ( )
431
+ await fulfillment ( of: [ taskCancelled] )
432
+ }
433
+
434
+ requestBodySequence. openGate ( for: 1 )
435
+
436
+ if case . partwayThroughSendingRequestBody = cancellationPoint {
437
+ taskShouldCancel. fulfill ( )
438
+ await fulfillment ( of: [ taskCancelled] )
439
+ }
440
+
441
+ requestBodySequence. openGate ( )
442
+
443
+ let ( response, maybeResponseBody) = try await ( asyncResponse, asyncResponseBody)
444
+
445
+ debug ( " Client received response head: \( response) " )
446
+ XCTAssertEqual ( response. status, . ok)
447
+ let responseBody = try XCTUnwrap ( maybeResponseBody)
448
+
449
+ if case . beforeConsumingResponseBody = cancellationPoint {
450
+ taskShouldCancel. fulfill ( )
451
+ await fulfillment ( of: [ taskCancelled] )
452
+ }
453
+
454
+ var iterator = responseBody. makeAsyncIterator ( )
455
+
456
+ _ = try await iterator. next ( )
457
+
458
+ if case . partwayThroughConsumingResponseBody = cancellationPoint {
459
+ taskShouldCancel. fulfill ( )
460
+ await fulfillment ( of: [ taskCancelled] )
461
+ }
462
+
463
+ while try await iterator. next ( ) != nil {
464
+
465
+ }
466
+
467
+ if case . afterConsumingResponseBody = cancellationPoint {
468
+ taskShouldCancel. fulfill ( )
469
+ await fulfillment ( of: [ taskCancelled] )
470
+ }
471
+
472
+ }
473
+
474
+ await fulfillment( of: [ taskShouldCancel] )
475
+ task. cancel ( )
476
+ taskCancelled. fulfill ( )
477
+
478
+ switch transport. configuration. implementation {
479
+ case . buffering:
480
+ switch cancellationPoint {
481
+ case . beforeSendingHead, . beforeSendingRequestBody, . partwayThroughSendingRequestBody:
482
+ await XCTAssertThrowsError ( try await task. value) { error in XCTAssertTrue ( error is CancellationError ) }
483
+ case . beforeConsumingResponseBody, . partwayThroughConsumingResponseBody, . afterConsumingResponseBody:
484
+ try await task. value
485
+ }
486
+ case . streaming:
487
+ switch cancellationPoint {
488
+ case . beforeSendingHead:
489
+ await XCTAssertThrowsError ( try await task. value) { error in XCTAssertTrue ( error is CancellationError ) }
490
+ case . beforeSendingRequestBody, . partwayThroughSendingRequestBody:
491
+ await XCTAssertThrowsError ( try await task. value) { error in
492
+ guard let urlError = error as? URLError else {
493
+ XCTFail ( )
494
+ return
495
+ }
496
+ XCTAssertEqual ( urlError. code, . cancelled)
497
+ }
498
+ case . beforeConsumingResponseBody, . partwayThroughConsumingResponseBody, . afterConsumingResponseBody:
499
+ try await task. value
500
+ }
501
+ }
502
+
503
+ group. cancelAll ( )
504
+ }
505
+
506
+ }
507
+
508
+ func fulfillment(
509
+ of expectations: [ XCTestExpectation] ,
510
+ timeout seconds: TimeInterval = . infinity,
511
+ enforceOrder enforceOrderOfFulfillment: Bool = false ,
512
+ file: StaticString = #file,
513
+ line: UInt = #line
514
+ ) async {
515
+ guard
516
+ case . completed = await XCTWaiter . fulfillment (
517
+ of: expectations,
518
+ timeout: seconds,
519
+ enforceOrder: enforceOrderOfFulfillment
520
+ )
521
+ else {
522
+ XCTFail ( " Expectation was not fulfilled " , file: file, line: line)
523
+ return
524
+ }
525
+ }
526
+
314
527
class URLSessionTransportDebugLoggingTests: XCTestCase {
315
528
func testDebugLoggingEnabled( ) {
316
529
let expectation = expectation ( description: " message autoclosure evaluated " )
0 commit comments