@@ -440,6 +440,100 @@ private struct SuccessOnlyRequest: HTTPRequest {
440440 }
441441}
442442
443+ // MARK: - Decoding Error Tests
444+
445+ @Suite ( " HTTPResponse Decoding Error Tests " )
446+ struct HTTPResponseDecodingErrorTests {
447+ @Test
448+ func testDecodeSuccessBody_throwsDecodingError_withResponseBody( ) throws {
449+ // Arrange — response body is valid JSON but wrong shape for SuccessModel
450+ let mismatchedBody = Data ( " { \" unexpected \" : \" field \" } " . utf8)
451+ let response = HTTPResponse < SuccessModel , ErrorModel > (
452+ statusCode: . ok,
453+ url: URL ( string: " https://example.com/test " ) !,
454+ headers: [ : ] ,
455+ httpBody: mismatchedBody,
456+ successBodyDecoder: { data in
457+ try JSONDecoder ( ) . decode ( SuccessModel . self, from: data)
458+ } ,
459+ failureBodyDecoder: { data in
460+ try JSONDecoder ( ) . decode ( ErrorModel . self, from: data)
461+ }
462+ )
463+
464+ // Act & Assert
465+ do {
466+ _ = try response. decodeSuccessBody ( )
467+ Issue . record ( " Expected decodingError to be thrown " )
468+ } catch let error as ClientError {
469+ guard case let . decodingError( type, responseBody, underlyingError) = error else {
470+ Issue . record ( " Expected ClientError.decodingError, got: \( error) " )
471+ return
472+ }
473+ #expect( type == " SuccessModel " )
474+ #expect( responseBody == mismatchedBody)
475+ #expect( underlyingError is DecodingError )
476+ }
477+ }
478+
479+ @Test
480+ func testDecodeFailureBody_throwsDecodingError_withResponseBody( ) throws {
481+ // Arrange — response body is not valid JSON at all
482+ let invalidBody = Data ( " not json " . utf8)
483+ let response = HTTPResponse < SuccessModel , ErrorModel > (
484+ statusCode: . badRequest,
485+ url: URL ( string: " https://example.com/test " ) !,
486+ headers: [ : ] ,
487+ httpBody: invalidBody,
488+ successBodyDecoder: { data in
489+ try JSONDecoder ( ) . decode ( SuccessModel . self, from: data)
490+ } ,
491+ failureBodyDecoder: { data in
492+ try JSONDecoder ( ) . decode ( ErrorModel . self, from: data)
493+ }
494+ )
495+
496+ // Act & Assert
497+ do {
498+ _ = try response. decodeFailureBody ( )
499+ Issue . record ( " Expected decodingError to be thrown " )
500+ } catch let error as ClientError {
501+ guard case let . decodingError( type, responseBody, underlyingError) = error else {
502+ Issue . record ( " Expected ClientError.decodingError, got: \( error) " )
503+ return
504+ }
505+ #expect( type == " ErrorModel " )
506+ #expect( responseBody == invalidBody)
507+ #expect( underlyingError is DecodingError )
508+ }
509+ }
510+
511+ @Test
512+ func testDecodingError_errorDescription_includesResponseBody( ) throws {
513+ let body = Data ( " { \" wrong \" :true} " . utf8)
514+ let error = ClientError . decodingError (
515+ type: " SuccessModel " ,
516+ responseBody: body,
517+ underlyingError: DecodingError . keyNotFound (
518+ AnyCodingKey ( stringValue: " value " ) ,
519+ . init( codingPath: [ ] , debugDescription: " No value for key 'value' " )
520+ )
521+ )
522+
523+ let description = error. errorDescription ?? " "
524+ #expect( description. contains ( " Failed to decode SuccessModel " ) )
525+ #expect( description. contains ( " { \" wrong \" :true} " ) )
526+ }
527+ }
528+
529+ /// A type-erased CodingKey for test assertions.
530+ private struct AnyCodingKey : CodingKey {
531+ var stringValue : String
532+ var intValue : Int ?
533+ init ( stringValue: String ) { self . stringValue = stringValue; self . intValue = nil }
534+ init ? ( intValue: Int ) { self . stringValue = " \( intValue) " ; self . intValue = intValue }
535+ }
536+
443537// MARK: - Cache Tests
444538
445539@Suite ( " URLSessionHTTPClient Cache Tests " )
0 commit comments