diff --git a/.swift-version b/.swift-version index a3ec5a4bd..bf77d5496 100644 --- a/.swift-version +++ b/.swift-version @@ -1 +1 @@ -3.2 +4.2 diff --git a/.travis.yml b/.travis.yml index 52a62aa1a..f6972ac35 100644 --- a/.travis.yml +++ b/.travis.yml @@ -18,18 +18,14 @@ env: - DESTINATION="OS=5.1,name=Apple Watch Series 4 - 44mm" SCHEME="$WATCHOS_FRAMEWORK_SCHEME" RUN_TESTS="NO" BUILD_EXAMPLE="NO" POD_LINT="NO" - DESTINATION="OS=4.2,name=Apple Watch Series 3 - 42mm" SCHEME="$WATCHOS_FRAMEWORK_SCHEME" RUN_TESTS="NO" BUILD_EXAMPLE="NO" POD_LINT="NO" - DESTINATION="OS=3.2,name=Apple Watch Series 2 - 42mm" SCHEME="$WATCHOS_FRAMEWORK_SCHEME" RUN_TESTS="NO" BUILD_EXAMPLE="NO" POD_LINT="NO" - - DESTINATION="OS=2.2,name=Apple Watch - 42mm" SCHEME="$WATCHOS_FRAMEWORK_SCHEME" RUN_TESTS="NO" BUILD_EXAMPLE="NO" POD_LINT="NO" - DESTINATION="OS=12.1,name=iPhone XS" SCHEME="$IOS_FRAMEWORK_SCHEME" RUN_TESTS="YES" BUILD_EXAMPLE="YES" POD_LINT="NO" - DESTINATION="OS=11.4,name=iPhone X" SCHEME="$IOS_FRAMEWORK_SCHEME" RUN_TESTS="YES" BUILD_EXAMPLE="YES" POD_LINT="NO" - DESTINATION="OS=10.3.1,name=iPhone 7 Plus" SCHEME="$IOS_FRAMEWORK_SCHEME" RUN_TESTS="YES" BUILD_EXAMPLE="YES" POD_LINT="NO" - - DESTINATION="OS=9.3,name=iPhone 6" SCHEME="$IOS_FRAMEWORK_SCHEME" RUN_TESTS="YES" BUILD_EXAMPLE="YES" POD_LINT="NO" - - DESTINATION="OS=8.4,name=iPhone 4S" SCHEME="$IOS_FRAMEWORK_SCHEME" RUN_TESTS="YES" BUILD_EXAMPLE="YES" POD_LINT="NO" - DESTINATION="OS=12.1,name=Apple TV 4K" SCHEME="$TVOS_FRAMEWORK_SCHEME" RUN_TESTS="YES" BUILD_EXAMPLE="NO" POD_LINT="NO" - DESTINATION="OS=11.4,name=Apple TV 4K" SCHEME="$TVOS_FRAMEWORK_SCHEME" RUN_TESTS="YES" BUILD_EXAMPLE="NO" POD_LINT="NO" - DESTINATION="OS=10.2,name=Apple TV 1080p" SCHEME="$TVOS_FRAMEWORK_SCHEME" RUN_TESTS="YES" BUILD_EXAMPLE="NO" POD_LINT="NO" - - DESTINATION="OS=9.2,name=Apple TV 1080p" SCHEME="$TVOS_FRAMEWORK_SCHEME" RUN_TESTS="YES" BUILD_EXAMPLE="NO" POD_LINT="NO" - DESTINATION="arch=x86_64" SCHEME="$MACOS_FRAMEWORK_SCHEME" RUN_TESTS="YES" BUILD_EXAMPLE="NO" POD_LINT="YES" script: @@ -37,13 +33,6 @@ script: - xcodebuild -version - xcodebuild -showsdks - # Build Framework in Debug and Run Tests if specified - - if [ $RUN_TESTS == "YES" ]; then - xcodebuild -workspace "$WORKSPACE" -scheme "$SCHEME" -destination "$DESTINATION" -configuration Debug ONLY_ACTIVE_ARCH=NO ENABLE_TESTABILITY=YES test | xcpretty; - else - xcodebuild -workspace "$WORKSPACE" -scheme "$SCHEME" -destination "$DESTINATION" -configuration Debug ONLY_ACTIVE_ARCH=NO build | xcpretty; - fi - # Build Framework in Release and Run Tests if specified - if [ $RUN_TESTS == "YES" ]; then xcodebuild -workspace "$WORKSPACE" -scheme "$SCHEME" -destination "$DESTINATION" -configuration Release ONLY_ACTIVE_ARCH=NO ENABLE_TESTABILITY=YES test | xcpretty; diff --git a/Alamofire.podspec b/Alamofire.podspec index 318bf1ccb..d9168a52c 100644 --- a/Alamofire.podspec +++ b/Alamofire.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'Alamofire' - s.version = '4.8.0' + s.version = '5.0.0.beta.1' s.license = 'MIT' s.summary = 'Elegant HTTP Networking in Swift' s.homepage = 'https://github.com/Alamofire/Alamofire' @@ -9,10 +9,12 @@ Pod::Spec.new do |s| s.source = { :git => 'https://github.com/Alamofire/Alamofire.git', :tag => s.version } s.documentation_url = 'https://alamofire.github.io/Alamofire/' - s.ios.deployment_target = '8.0' - s.osx.deployment_target = '10.10' - s.tvos.deployment_target = '9.0' - s.watchos.deployment_target = '2.0' + s.ios.deployment_target = '10.0' + s.osx.deployment_target = '10.12' + s.tvos.deployment_target = '10.0' + s.watchos.deployment_target = '3.0' s.source_files = 'Source/*.swift' + + s.frameworks = 'CFNetwork' end diff --git a/Alamofire.xcodeproj/project.pbxproj b/Alamofire.xcodeproj/project.pbxproj index d36a671fe..855bc7733 100644 --- a/Alamofire.xcodeproj/project.pbxproj +++ b/Alamofire.xcodeproj/project.pbxproj @@ -21,14 +21,130 @@ /* End PBXAggregateTarget section */ /* Begin PBXBuildFile section */ + 3107EA3520A11AE100445260 /* AuthenticationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8E6024419CB46A800A3E7F1 /* AuthenticationTests.swift */; }; + 3107EA3620A11AE100445260 /* AuthenticationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8E6024419CB46A800A3E7F1 /* AuthenticationTests.swift */; }; + 3107EA3720A11AE200445260 /* AuthenticationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8E6024419CB46A800A3E7F1 /* AuthenticationTests.swift */; }; + 3107EA3820A11F9600445260 /* ResponseTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8111E5E19A9674D0040E7D1 /* ResponseTests.swift */; }; + 3107EA3920A11F9600445260 /* ResponseTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8111E5E19A9674D0040E7D1 /* ResponseTests.swift */; }; + 3107EA3A20A11F9700445260 /* ResponseTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8111E5E19A9674D0040E7D1 /* ResponseTests.swift */; }; + 3107EA3C20A124E900445260 /* ResultTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CA028C41B7466C500C84163 /* ResultTests.swift */; }; + 3107EA3D20A124EA00445260 /* ResultTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CA028C41B7466C500C84163 /* ResultTests.swift */; }; + 3107EA3E20A124EB00445260 /* ResultTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CA028C41B7466C500C84163 /* ResultTests.swift */; }; + 3107EA3F20A1267C00445260 /* SessionDelegateTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C9DCE771CB1BCE2003E6463 /* SessionDelegateTests.swift */; }; + 3107EA4020A1267C00445260 /* SessionDelegateTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C9DCE771CB1BCE2003E6463 /* SessionDelegateTests.swift */; }; + 3107EA4120A1267D00445260 /* SessionDelegateTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C9DCE771CB1BCE2003E6463 /* SessionDelegateTests.swift */; }; + 3111CE8420A7636E008315E2 /* SessionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8D1C6F419D52968002E74FE /* SessionTests.swift */; }; + 3111CE8520A7636F008315E2 /* SessionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8D1C6F419D52968002E74FE /* SessionTests.swift */; }; + 3111CE8620A76370008315E2 /* SessionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8D1C6F419D52968002E74FE /* SessionTests.swift */; }; + 3111CE8820A77843008315E2 /* EventMonitor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3111CE8720A77843008315E2 /* EventMonitor.swift */; }; + 3111CE8920A77944008315E2 /* EventMonitor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3111CE8720A77843008315E2 /* EventMonitor.swift */; }; + 3111CE8A20A77945008315E2 /* EventMonitor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3111CE8720A77843008315E2 /* EventMonitor.swift */; }; + 3111CE8B20A77945008315E2 /* EventMonitor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3111CE8720A77843008315E2 /* EventMonitor.swift */; }; + 3111CE8C20A7EBE6008315E2 /* MultipartFormDataTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C3238E61B3604DB00FE04AE /* MultipartFormDataTests.swift */; }; + 3111CE8D20A7EBE7008315E2 /* MultipartFormDataTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C3238E61B3604DB00FE04AE /* MultipartFormDataTests.swift */; }; + 3111CE8E20A7EBE7008315E2 /* MultipartFormDataTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C3238E61B3604DB00FE04AE /* MultipartFormDataTests.swift */; }; + 3111CE8F20A7EC26008315E2 /* NetworkReachabilityManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C3D00571C66A8B900D1F709 /* NetworkReachabilityManagerTests.swift */; }; + 3111CE9020A7EC27008315E2 /* NetworkReachabilityManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C3D00571C66A8B900D1F709 /* NetworkReachabilityManagerTests.swift */; }; + 3111CE9120A7EC27008315E2 /* NetworkReachabilityManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C3D00571C66A8B900D1F709 /* NetworkReachabilityManagerTests.swift */; }; + 3111CE9220A7EC30008315E2 /* ResponseSerializationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C0B58381B747A4400C0B99C /* ResponseSerializationTests.swift */; }; + 3111CE9320A7EC31008315E2 /* ResponseSerializationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C0B58381B747A4400C0B99C /* ResponseSerializationTests.swift */; }; + 3111CE9420A7EC32008315E2 /* ResponseSerializationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C0B58381B747A4400C0B99C /* ResponseSerializationTests.swift */; }; + 3111CE9520A7EC39008315E2 /* ServerTrustEvaluatorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C33A1421B52089C00873DFF /* ServerTrustEvaluatorTests.swift */; }; + 3111CE9620A7EC3A008315E2 /* ServerTrustEvaluatorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C33A1421B52089C00873DFF /* ServerTrustEvaluatorTests.swift */; }; + 3111CE9720A7EC3A008315E2 /* ServerTrustEvaluatorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C33A1421B52089C00873DFF /* ServerTrustEvaluatorTests.swift */; }; + 3111CE9B20A7EC57008315E2 /* URLProtocolTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CCFA7991B2BE71600B6F460 /* URLProtocolTests.swift */; }; + 3111CE9C20A7EC58008315E2 /* URLProtocolTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CCFA7991B2BE71600B6F460 /* URLProtocolTests.swift */; }; + 3111CE9D20A7EC58008315E2 /* URLProtocolTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CCFA7991B2BE71600B6F460 /* URLProtocolTests.swift */; }; + 3113D46B21878227001CCD21 /* HTTPHeadersTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3113D46A21878227001CCD21 /* HTTPHeadersTests.swift */; }; + 3113D46C21878227001CCD21 /* HTTPHeadersTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3113D46A21878227001CCD21 /* HTTPHeadersTests.swift */; }; + 3113D46D21878227001CCD21 /* HTTPHeadersTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3113D46A21878227001CCD21 /* HTTPHeadersTests.swift */; }; + 311B199020B0D3B40036823B /* MultipartUpload.swift in Sources */ = {isa = PBXBuildFile; fileRef = 311B198F20B0D3B40036823B /* MultipartUpload.swift */; }; + 311B199120B0E3470036823B /* MultipartUpload.swift in Sources */ = {isa = PBXBuildFile; fileRef = 311B198F20B0D3B40036823B /* MultipartUpload.swift */; }; + 311B199220B0E3480036823B /* MultipartUpload.swift in Sources */ = {isa = PBXBuildFile; fileRef = 311B198F20B0D3B40036823B /* MultipartUpload.swift */; }; + 311B199320B0E3480036823B /* MultipartUpload.swift in Sources */ = {isa = PBXBuildFile; fileRef = 311B198F20B0D3B40036823B /* MultipartUpload.swift */; }; + 311B199420B0ED980036823B /* UploadTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8111E5F19A9674D0040E7D1 /* UploadTests.swift */; }; + 311B199520B0ED980036823B /* UploadTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8111E5F19A9674D0040E7D1 /* UploadTests.swift */; }; + 311B199620B0ED990036823B /* UploadTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8111E5F19A9674D0040E7D1 /* UploadTests.swift */; }; + 31501E882196962A005829F2 /* ParameterEncoderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31501E872196962A005829F2 /* ParameterEncoderTests.swift */; }; + 31501E892196962A005829F2 /* ParameterEncoderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31501E872196962A005829F2 /* ParameterEncoderTests.swift */; }; + 31501E8A2196962A005829F2 /* ParameterEncoderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31501E872196962A005829F2 /* ParameterEncoderTests.swift */; }; + 31727418218BAEC90039FFCC /* HTTPMethod.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31727417218BAEC90039FFCC /* HTTPMethod.swift */; }; + 31727419218BAEC90039FFCC /* HTTPMethod.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31727417218BAEC90039FFCC /* HTTPMethod.swift */; }; + 3172741A218BAEC90039FFCC /* HTTPMethod.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31727417218BAEC90039FFCC /* HTTPMethod.swift */; }; + 3172741B218BAEC90039FFCC /* HTTPMethod.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31727417218BAEC90039FFCC /* HTTPMethod.swift */; }; + 3172741D218BB1790039FFCC /* ParameterEncoder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3172741C218BB1790039FFCC /* ParameterEncoder.swift */; }; + 3172741E218BB1790039FFCC /* ParameterEncoder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3172741C218BB1790039FFCC /* ParameterEncoder.swift */; }; + 3172741F218BB1790039FFCC /* ParameterEncoder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3172741C218BB1790039FFCC /* ParameterEncoder.swift */; }; + 31727420218BB1790039FFCC /* ParameterEncoder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3172741C218BB1790039FFCC /* ParameterEncoder.swift */; }; + 31727422218BB9A50039FFCC /* HTTPBin.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31727421218BB9A50039FFCC /* HTTPBin.swift */; }; + 31727423218BB9A50039FFCC /* HTTPBin.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31727421218BB9A50039FFCC /* HTTPBin.swift */; }; + 31727424218BB9A50039FFCC /* HTTPBin.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31727421218BB9A50039FFCC /* HTTPBin.swift */; }; + 317A6A7620B2207F00A9FEC5 /* DownloadTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8111E5B19A9674D0040E7D1 /* DownloadTests.swift */; }; + 317A6A7720B2208000A9FEC5 /* DownloadTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8111E5B19A9674D0040E7D1 /* DownloadTests.swift */; }; + 317A6A7820B2208000A9FEC5 /* DownloadTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8111E5B19A9674D0040E7D1 /* DownloadTests.swift */; }; + 3191B5751F5F53A6003960A8 /* Protector.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3191B5741F5F53A6003960A8 /* Protector.swift */; }; + 3191B5761F5F53A6003960A8 /* Protector.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3191B5741F5F53A6003960A8 /* Protector.swift */; }; + 3191B5771F5F53A6003960A8 /* Protector.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3191B5741F5F53A6003960A8 /* Protector.swift */; }; + 3191B5781F5F53A6003960A8 /* Protector.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3191B5741F5F53A6003960A8 /* Protector.swift */; }; + 31991794209CDA7F00103A19 /* Request.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31991790209CDA7F00103A19 /* Request.swift */; }; + 31991795209CDA7F00103A19 /* Request.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31991790209CDA7F00103A19 /* Request.swift */; }; + 31991796209CDA7F00103A19 /* Request.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31991790209CDA7F00103A19 /* Request.swift */; }; + 31991797209CDA7F00103A19 /* Request.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31991790209CDA7F00103A19 /* Request.swift */; }; + 31991798209CDA7F00103A19 /* Response.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31991791209CDA7F00103A19 /* Response.swift */; }; + 31991799209CDA7F00103A19 /* Response.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31991791209CDA7F00103A19 /* Response.swift */; }; + 3199179A209CDA7F00103A19 /* Response.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31991791209CDA7F00103A19 /* Response.swift */; }; + 3199179B209CDA7F00103A19 /* Response.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31991791209CDA7F00103A19 /* Response.swift */; }; + 3199179C209CDA7F00103A19 /* Session.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31991792209CDA7F00103A19 /* Session.swift */; }; + 3199179D209CDA7F00103A19 /* Session.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31991792209CDA7F00103A19 /* Session.swift */; }; + 3199179E209CDA7F00103A19 /* Session.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31991792209CDA7F00103A19 /* Session.swift */; }; + 3199179F209CDA7F00103A19 /* Session.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31991792209CDA7F00103A19 /* Session.swift */; }; + 319917A0209CDA7F00103A19 /* SessionStateProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31991793209CDA7F00103A19 /* SessionStateProvider.swift */; }; + 319917A1209CDA7F00103A19 /* SessionStateProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31991793209CDA7F00103A19 /* SessionStateProvider.swift */; }; + 319917A2209CDA7F00103A19 /* SessionStateProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31991793209CDA7F00103A19 /* SessionStateProvider.swift */; }; + 319917A3209CDA7F00103A19 /* SessionStateProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31991793209CDA7F00103A19 /* SessionStateProvider.swift */; }; + 319917A5209CDAC400103A19 /* RequestTaskMap.swift in Sources */ = {isa = PBXBuildFile; fileRef = 319917A4209CDAC400103A19 /* RequestTaskMap.swift */; }; + 319917A6209CDAC400103A19 /* RequestTaskMap.swift in Sources */ = {isa = PBXBuildFile; fileRef = 319917A4209CDAC400103A19 /* RequestTaskMap.swift */; }; + 319917A7209CDAC400103A19 /* RequestTaskMap.swift in Sources */ = {isa = PBXBuildFile; fileRef = 319917A4209CDAC400103A19 /* RequestTaskMap.swift */; }; + 319917A8209CDAC400103A19 /* RequestTaskMap.swift in Sources */ = {isa = PBXBuildFile; fileRef = 319917A4209CDAC400103A19 /* RequestTaskMap.swift */; }; + 319917AA209CDCB000103A19 /* HTTPHeaders.swift in Sources */ = {isa = PBXBuildFile; fileRef = 319917A9209CDCB000103A19 /* HTTPHeaders.swift */; }; + 319917AB209CDCB000103A19 /* HTTPHeaders.swift in Sources */ = {isa = PBXBuildFile; fileRef = 319917A9209CDCB000103A19 /* HTTPHeaders.swift */; }; + 319917AC209CDCB000103A19 /* HTTPHeaders.swift in Sources */ = {isa = PBXBuildFile; fileRef = 319917A9209CDCB000103A19 /* HTTPHeaders.swift */; }; + 319917AD209CDCB000103A19 /* HTTPHeaders.swift in Sources */ = {isa = PBXBuildFile; fileRef = 319917A9209CDCB000103A19 /* HTTPHeaders.swift */; }; + 319917AF209CE34F00103A19 /* RequestAdapter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 319917AE209CE34E00103A19 /* RequestAdapter.swift */; }; + 319917B0209CE34F00103A19 /* RequestAdapter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 319917AE209CE34E00103A19 /* RequestAdapter.swift */; }; + 319917B1209CE34F00103A19 /* RequestAdapter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 319917AE209CE34E00103A19 /* RequestAdapter.swift */; }; + 319917B2209CE34F00103A19 /* RequestAdapter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 319917AE209CE34E00103A19 /* RequestAdapter.swift */; }; + 319917B4209CE36E00103A19 /* RequestRetrier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 319917B3209CE36E00103A19 /* RequestRetrier.swift */; }; + 319917B5209CE36E00103A19 /* RequestRetrier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 319917B3209CE36E00103A19 /* RequestRetrier.swift */; }; + 319917B6209CE36E00103A19 /* RequestRetrier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 319917B3209CE36E00103A19 /* RequestRetrier.swift */; }; + 319917B7209CE36E00103A19 /* RequestRetrier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 319917B3209CE36E00103A19 /* RequestRetrier.swift */; }; + 319917B9209CE53A00103A19 /* OperationQueue+Alamofire.swift in Sources */ = {isa = PBXBuildFile; fileRef = 319917B8209CE53A00103A19 /* OperationQueue+Alamofire.swift */; }; + 319917BA209CE53A00103A19 /* OperationQueue+Alamofire.swift in Sources */ = {isa = PBXBuildFile; fileRef = 319917B8209CE53A00103A19 /* OperationQueue+Alamofire.swift */; }; + 319917BB209CE53A00103A19 /* OperationQueue+Alamofire.swift in Sources */ = {isa = PBXBuildFile; fileRef = 319917B8209CE53A00103A19 /* OperationQueue+Alamofire.swift */; }; + 319917BC209CE53A00103A19 /* OperationQueue+Alamofire.swift in Sources */ = {isa = PBXBuildFile; fileRef = 319917B8209CE53A00103A19 /* OperationQueue+Alamofire.swift */; }; + 31C2B0EA20B271040089BA7C /* CacheTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C341BB91B1A865A00C1B34D /* CacheTests.swift */; }; + 31C2B0EB20B271050089BA7C /* CacheTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C341BB91B1A865A00C1B34D /* CacheTests.swift */; }; + 31C2B0EC20B271060089BA7C /* CacheTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C341BB91B1A865A00C1B34D /* CacheTests.swift */; }; + 31C2B0F020B271370089BA7C /* TLSEvaluationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F86AEFE51AE6A282007D9C76 /* TLSEvaluationTests.swift */; }; + 31C2B0F120B271370089BA7C /* TLSEvaluationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F86AEFE51AE6A282007D9C76 /* TLSEvaluationTests.swift */; }; + 31C2B0F220B271380089BA7C /* TLSEvaluationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F86AEFE51AE6A282007D9C76 /* TLSEvaluationTests.swift */; }; + 31D83FCE20D5C29300D93E47 /* URLConvertible+URLRequestConvertible.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31D83FCD20D5C29300D93E47 /* URLConvertible+URLRequestConvertible.swift */; }; + 31D83FCF20D5C29300D93E47 /* URLConvertible+URLRequestConvertible.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31D83FCD20D5C29300D93E47 /* URLConvertible+URLRequestConvertible.swift */; }; + 31D83FD020D5C29300D93E47 /* URLConvertible+URLRequestConvertible.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31D83FCD20D5C29300D93E47 /* URLConvertible+URLRequestConvertible.swift */; }; + 31D83FD120D5C29300D93E47 /* URLConvertible+URLRequestConvertible.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31D83FCD20D5C29300D93E47 /* URLConvertible+URLRequestConvertible.swift */; }; + 31EBD9C120D1D89C00D1FF34 /* ValidationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8AE910119D28DCC0078C7B2 /* ValidationTests.swift */; }; + 31EBD9C220D1D89C00D1FF34 /* ValidationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8AE910119D28DCC0078C7B2 /* ValidationTests.swift */; }; + 31EBD9C320D1D89D00D1FF34 /* ValidationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8AE910119D28DCC0078C7B2 /* ValidationTests.swift */; }; 31ED52E81D73891B00199085 /* AFError+AlamofireTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31ED52E61D73889D00199085 /* AFError+AlamofireTests.swift */; }; 31ED52E91D73891C00199085 /* AFError+AlamofireTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31ED52E61D73889D00199085 /* AFError+AlamofireTests.swift */; }; 31ED52EA1D73891C00199085 /* AFError+AlamofireTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31ED52E61D73889D00199085 /* AFError+AlamofireTests.swift */; }; - 4C0B58391B747A4400C0B99C /* ResponseSerializationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C0B58381B747A4400C0B99C /* ResponseSerializationTests.swift */; }; - 4C0B583A1B747A4400C0B99C /* ResponseSerializationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C0B58381B747A4400C0B99C /* ResponseSerializationTests.swift */; }; - 4C0B62511BB1001C009302D3 /* Response.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C0B62501BB1001C009302D3 /* Response.swift */; }; - 4C0B62521BB1001C009302D3 /* Response.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C0B62501BB1001C009302D3 /* Response.swift */; }; - 4C0B62531BB1001C009302D3 /* Response.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C0B62501BB1001C009302D3 /* Response.swift */; }; + 31F5085D20B50DC400FE2A0C /* URLSessionConfiguration+Alamofire.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31F5085C20B50DC400FE2A0C /* URLSessionConfiguration+Alamofire.swift */; }; + 31F5085E20B50DC400FE2A0C /* URLSessionConfiguration+Alamofire.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31F5085C20B50DC400FE2A0C /* URLSessionConfiguration+Alamofire.swift */; }; + 31F5085F20B50DC400FE2A0C /* URLSessionConfiguration+Alamofire.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31F5085C20B50DC400FE2A0C /* URLSessionConfiguration+Alamofire.swift */; }; + 31F5086020B50DC400FE2A0C /* URLSessionConfiguration+Alamofire.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31F5085C20B50DC400FE2A0C /* URLSessionConfiguration+Alamofire.swift */; }; + 31F9683C20BB70290009606F /* NSLoggingEventMonitor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31F9683B20BB70290009606F /* NSLoggingEventMonitor.swift */; }; + 31F9683D20BB70290009606F /* NSLoggingEventMonitor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31F9683B20BB70290009606F /* NSLoggingEventMonitor.swift */; }; + 31F9683E20BB70290009606F /* NSLoggingEventMonitor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31F9683B20BB70290009606F /* NSLoggingEventMonitor.swift */; }; 4C0E5BF81B673D3400816CCC /* Result.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C0E5BF71B673D3400816CCC /* Result.swift */; }; 4C0E5BF91B673D3400816CCC /* Result.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C0E5BF71B673D3400816CCC /* Result.swift */; }; 4C1DC8541B68908E00476DE3 /* AFError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C1DC8531B68908E00476DE3 /* AFError.swift */; }; @@ -37,30 +153,17 @@ 4C23EB441B327C5B0090E0BC /* MultipartFormData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C23EB421B327C5B0090E0BC /* MultipartFormData.swift */; }; 4C256A531B096C770065714F /* BaseTestCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C256A501B096C2C0065714F /* BaseTestCase.swift */; }; 4C256A541B096C770065714F /* BaseTestCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C256A501B096C2C0065714F /* BaseTestCase.swift */; }; - 4C3238E71B3604DB00FE04AE /* MultipartFormDataTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C3238E61B3604DB00FE04AE /* MultipartFormDataTests.swift */; }; - 4C3238E81B3604DB00FE04AE /* MultipartFormDataTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C3238E61B3604DB00FE04AE /* MultipartFormDataTests.swift */; }; 4C33A1391B5207DB00873DFF /* rainbow.jpg in Resources */ = {isa = PBXBuildFile; fileRef = 4C33A1231B5207DB00873DFF /* rainbow.jpg */; }; 4C33A13A1B5207DB00873DFF /* rainbow.jpg in Resources */ = {isa = PBXBuildFile; fileRef = 4C33A1231B5207DB00873DFF /* rainbow.jpg */; }; 4C33A13B1B5207DB00873DFF /* unicorn.png in Resources */ = {isa = PBXBuildFile; fileRef = 4C33A1241B5207DB00873DFF /* unicorn.png */; }; 4C33A13C1B5207DB00873DFF /* unicorn.png in Resources */ = {isa = PBXBuildFile; fileRef = 4C33A1241B5207DB00873DFF /* unicorn.png */; }; - 4C33A1431B52089C00873DFF /* ServerTrustPolicyTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C33A1421B52089C00873DFF /* ServerTrustPolicyTests.swift */; }; - 4C33A1441B52089C00873DFF /* ServerTrustPolicyTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C33A1421B52089C00873DFF /* ServerTrustPolicyTests.swift */; }; - 4C341BBA1B1A865A00C1B34D /* CacheTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C341BB91B1A865A00C1B34D /* CacheTests.swift */; }; - 4C341BBB1B1A865A00C1B34D /* CacheTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C341BB91B1A865A00C1B34D /* CacheTests.swift */; }; 4C3D00541C66A63000D1F709 /* NetworkReachabilityManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C3D00531C66A63000D1F709 /* NetworkReachabilityManager.swift */; }; 4C3D00551C66A63000D1F709 /* NetworkReachabilityManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C3D00531C66A63000D1F709 /* NetworkReachabilityManager.swift */; }; 4C3D00561C66A63000D1F709 /* NetworkReachabilityManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C3D00531C66A63000D1F709 /* NetworkReachabilityManager.swift */; }; - 4C3D00581C66A8B900D1F709 /* NetworkReachabilityManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C3D00571C66A8B900D1F709 /* NetworkReachabilityManagerTests.swift */; }; - 4C3D00591C66A8B900D1F709 /* NetworkReachabilityManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C3D00571C66A8B900D1F709 /* NetworkReachabilityManagerTests.swift */; }; - 4C3D005A1C66A8B900D1F709 /* NetworkReachabilityManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C3D00571C66A8B900D1F709 /* NetworkReachabilityManagerTests.swift */; }; 4C43669B1D7BB93D00C38AAD /* DispatchQueue+Alamofire.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C43669A1D7BB93D00C38AAD /* DispatchQueue+Alamofire.swift */; }; 4C43669C1D7BB93D00C38AAD /* DispatchQueue+Alamofire.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C43669A1D7BB93D00C38AAD /* DispatchQueue+Alamofire.swift */; }; 4C43669D1D7BB93D00C38AAD /* DispatchQueue+Alamofire.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C43669A1D7BB93D00C38AAD /* DispatchQueue+Alamofire.swift */; }; 4C43669E1D7BB93D00C38AAD /* DispatchQueue+Alamofire.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C43669A1D7BB93D00C38AAD /* DispatchQueue+Alamofire.swift */; }; - 4C574E6A1C67D207000B3128 /* Timeline.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C574E691C67D207000B3128 /* Timeline.swift */; }; - 4C574E6B1C67D207000B3128 /* Timeline.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C574E691C67D207000B3128 /* Timeline.swift */; }; - 4C574E6C1C67D207000B3128 /* Timeline.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C574E691C67D207000B3128 /* Timeline.swift */; }; - 4C574E6D1C67D207000B3128 /* Timeline.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C574E691C67D207000B3128 /* Timeline.swift */; }; 4C743CF61C22772D00BCB23E /* certDER.cer in Resources */ = {isa = PBXBuildFile; fileRef = B39E2F831C1A72F8002DA1A9 /* certDER.cer */; }; 4C743CF71C22772D00BCB23E /* certDER.crt in Resources */ = {isa = PBXBuildFile; fileRef = B39E2F841C1A72F8002DA1A9 /* certDER.crt */; }; 4C743CF81C22772D00BCB23E /* certDER.der in Resources */ = {isa = PBXBuildFile; fileRef = B39E2F851C1A72F8002DA1A9 /* certDER.der */; }; @@ -118,14 +221,8 @@ 4C743D321C22772F00BCB23E /* signed-by-ca2.cer in Resources */ = {isa = PBXBuildFile; fileRef = 4C812C511B535F540017E0BF /* signed-by-ca2.cer */; }; 4C743D331C22772F00BCB23E /* valid-dns-name.cer in Resources */ = {isa = PBXBuildFile; fileRef = 4C812C521B535F540017E0BF /* valid-dns-name.cer */; }; 4C743D341C22772F00BCB23E /* valid-uri.cer in Resources */ = {isa = PBXBuildFile; fileRef = 4C812C531B535F540017E0BF /* valid-uri.cer */; }; - 4C80F9F81BB730EF001B46D2 /* Response.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C0B62501BB1001C009302D3 /* Response.swift */; }; - 4C811F8D1B51856D00E0F59A /* ServerTrustPolicy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C811F8C1B51856D00E0F59A /* ServerTrustPolicy.swift */; }; - 4C811F8E1B51856D00E0F59A /* ServerTrustPolicy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C811F8C1B51856D00E0F59A /* ServerTrustPolicy.swift */; }; - 4C9DCE781CB1BCE2003E6463 /* SessionDelegateTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C9DCE771CB1BCE2003E6463 /* SessionDelegateTests.swift */; }; - 4C9DCE791CB1BCE2003E6463 /* SessionDelegateTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C9DCE771CB1BCE2003E6463 /* SessionDelegateTests.swift */; }; - 4C9DCE7A1CB1BCE2003E6463 /* SessionDelegateTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C9DCE771CB1BCE2003E6463 /* SessionDelegateTests.swift */; }; - 4CA028C51B7466C500C84163 /* ResultTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CA028C41B7466C500C84163 /* ResultTests.swift */; }; - 4CA028C61B7466C500C84163 /* ResultTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CA028C41B7466C500C84163 /* ResultTests.swift */; }; + 4C811F8D1B51856D00E0F59A /* ServerTrustEvaluation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C811F8C1B51856D00E0F59A /* ServerTrustEvaluation.swift */; }; + 4C811F8E1B51856D00E0F59A /* ServerTrustEvaluation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C811F8C1B51856D00E0F59A /* ServerTrustEvaluation.swift */; }; 4CB928291C66BFBC00CE5F08 /* Notifications.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CB928281C66BFBC00CE5F08 /* Notifications.swift */; }; 4CB9282A1C66BFBC00CE5F08 /* Notifications.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CB928281C66BFBC00CE5F08 /* Notifications.swift */; }; 4CB9282B1C66BFBC00CE5F08 /* Notifications.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CB928281C66BFBC00CE5F08 /* Notifications.swift */; }; @@ -142,12 +239,6 @@ 4CCB20751D4549E000C64D5B /* expired.badssl.com-intermediate-ca-2.cer in Resources */ = {isa = PBXBuildFile; fileRef = 4CCB206B1D4549E000C64D5B /* expired.badssl.com-intermediate-ca-2.cer */; }; 4CCB20761D4549E000C64D5B /* expired.badssl.com-intermediate-ca-2.cer in Resources */ = {isa = PBXBuildFile; fileRef = 4CCB206B1D4549E000C64D5B /* expired.badssl.com-intermediate-ca-2.cer */; }; 4CCB20771D4549E000C64D5B /* expired.badssl.com-intermediate-ca-2.cer in Resources */ = {isa = PBXBuildFile; fileRef = 4CCB206B1D4549E000C64D5B /* expired.badssl.com-intermediate-ca-2.cer */; }; - 4CCFA79A1B2BE71600B6F460 /* URLProtocolTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CCFA7991B2BE71600B6F460 /* URLProtocolTests.swift */; }; - 4CCFA79B1B2BE71600B6F460 /* URLProtocolTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CCFA7991B2BE71600B6F460 /* URLProtocolTests.swift */; }; - 4CDE2C371AF8932A00BABAE5 /* SessionManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CDE2C361AF8932A00BABAE5 /* SessionManager.swift */; }; - 4CDE2C381AF8932A00BABAE5 /* SessionManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CDE2C361AF8932A00BABAE5 /* SessionManager.swift */; }; - 4CDE2C3A1AF899EC00BABAE5 /* Request.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CDE2C391AF899EC00BABAE5 /* Request.swift */; }; - 4CDE2C3B1AF899EC00BABAE5 /* Request.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CDE2C391AF899EC00BABAE5 /* Request.swift */; }; 4CDE2C431AF89F0900BABAE5 /* Validation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CDE2C421AF89F0900BABAE5 /* Validation.swift */; }; 4CDE2C441AF89F0900BABAE5 /* Validation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CDE2C421AF89F0900BABAE5 /* Validation.swift */; }; 4CDE2C461AF89FF300BABAE5 /* ResponseSerialization.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CDE2C451AF89FF300BABAE5 /* ResponseSerialization.swift */; }; @@ -162,30 +253,15 @@ 4CF627061BA7CBE30011A099 /* Alamofire.h in Headers */ = {isa = PBXBuildFile; fileRef = F8111E3819A95C8B0040E7D1 /* Alamofire.h */; settings = {ATTRIBUTES = (Public, ); }; }; 4CF627071BA7CBF60011A099 /* Alamofire.swift in Sources */ = {isa = PBXBuildFile; fileRef = F897FF4019AA800700AB5182 /* Alamofire.swift */; }; 4CF627081BA7CBF60011A099 /* AFError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C1DC8531B68908E00476DE3 /* AFError.swift */; }; - 4CF627091BA7CBF60011A099 /* SessionManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CDE2C361AF8932A00BABAE5 /* SessionManager.swift */; }; 4CF6270A1BA7CBF60011A099 /* ParameterEncoding.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CE2724E1AF88FB500F1D59A /* ParameterEncoding.swift */; }; - 4CF6270B1BA7CBF60011A099 /* Request.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CDE2C391AF899EC00BABAE5 /* Request.swift */; }; 4CF6270C1BA7CBF60011A099 /* Result.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C0E5BF71B673D3400816CCC /* Result.swift */; }; 4CF6270E1BA7CBF60011A099 /* MultipartFormData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C23EB421B327C5B0090E0BC /* MultipartFormData.swift */; }; 4CF6270F1BA7CBF60011A099 /* ResponseSerialization.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CDE2C451AF89FF300BABAE5 /* ResponseSerialization.swift */; }; - 4CF627101BA7CBF60011A099 /* ServerTrustPolicy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C811F8C1B51856D00E0F59A /* ServerTrustPolicy.swift */; }; + 4CF627101BA7CBF60011A099 /* ServerTrustEvaluation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C811F8C1B51856D00E0F59A /* ServerTrustEvaluation.swift */; }; 4CF627131BA7CBF60011A099 /* Validation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CDE2C421AF89F0900BABAE5 /* Validation.swift */; }; 4CF627141BA7CC240011A099 /* BaseTestCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C256A501B096C2C0065714F /* BaseTestCase.swift */; }; - 4CF627151BA7CC240011A099 /* AuthenticationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8E6024419CB46A800A3E7F1 /* AuthenticationTests.swift */; }; - 4CF627161BA7CC240011A099 /* SessionManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8D1C6F419D52968002E74FE /* SessionManagerTests.swift */; }; 4CF627171BA7CC240011A099 /* ParameterEncodingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8111E5C19A9674D0040E7D1 /* ParameterEncodingTests.swift */; }; 4CF627181BA7CC240011A099 /* RequestTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8111E5D19A9674D0040E7D1 /* RequestTests.swift */; }; - 4CF627191BA7CC240011A099 /* ResponseTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8111E5E19A9674D0040E7D1 /* ResponseTests.swift */; }; - 4CF6271A1BA7CC240011A099 /* ResultTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CA028C41B7466C500C84163 /* ResultTests.swift */; }; - 4CF6271C1BA7CC240011A099 /* CacheTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C341BB91B1A865A00C1B34D /* CacheTests.swift */; }; - 4CF6271D1BA7CC240011A099 /* DownloadTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8111E5B19A9674D0040E7D1 /* DownloadTests.swift */; }; - 4CF6271E1BA7CC240011A099 /* MultipartFormDataTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C3238E61B3604DB00FE04AE /* MultipartFormDataTests.swift */; }; - 4CF6271F1BA7CC240011A099 /* ResponseSerializationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C0B58381B747A4400C0B99C /* ResponseSerializationTests.swift */; }; - 4CF627201BA7CC240011A099 /* ServerTrustPolicyTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C33A1421B52089C00873DFF /* ServerTrustPolicyTests.swift */; }; - 4CF627211BA7CC240011A099 /* TLSEvaluationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F86AEFE51AE6A282007D9C76 /* TLSEvaluationTests.swift */; }; - 4CF627221BA7CC240011A099 /* UploadTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8111E5F19A9674D0040E7D1 /* UploadTests.swift */; }; - 4CF627231BA7CC240011A099 /* URLProtocolTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CCFA7991B2BE71600B6F460 /* URLProtocolTests.swift */; }; - 4CF627241BA7CC240011A099 /* ValidationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8AE910119D28DCC0078C7B2 /* ValidationTests.swift */; }; 4CF627341BA7CC300011A099 /* rainbow.jpg in Resources */ = {isa = PBXBuildFile; fileRef = 4C33A1231B5207DB00873DFF /* rainbow.jpg */; }; 4CF627351BA7CC300011A099 /* unicorn.png in Resources */ = {isa = PBXBuildFile; fileRef = 4C33A1241B5207DB00873DFF /* unicorn.png */; }; 4CFB02901D7CF28F0056F249 /* FileManager+AlamofireTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CFB028F1D7CF28F0056F249 /* FileManager+AlamofireTests.swift */; }; @@ -218,46 +294,22 @@ 4CFB030D1D7D2FA20056F249 /* utf8_string.txt in Resources */ = {isa = PBXBuildFile; fileRef = 4CFB02F41D7D2FA20056F249 /* utf8_string.txt */; }; 4CFB030E1D7D2FA20056F249 /* utf8_string.txt in Resources */ = {isa = PBXBuildFile; fileRef = 4CFB02F41D7D2FA20056F249 /* utf8_string.txt */; }; 4CFB030F1D7D2FA20056F249 /* utf8_string.txt in Resources */ = {isa = PBXBuildFile; fileRef = 4CFB02F41D7D2FA20056F249 /* utf8_string.txt */; }; - 4CFCFE2E1D56D31700A76388 /* SessionDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CFCFE2D1D56D31700A76388 /* SessionDelegate.swift */; }; - 4CFCFE2F1D56D31700A76388 /* SessionDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CFCFE2D1D56D31700A76388 /* SessionDelegate.swift */; }; - 4CFCFE301D56D31700A76388 /* SessionDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CFCFE2D1D56D31700A76388 /* SessionDelegate.swift */; }; - 4CFCFE311D56D31700A76388 /* SessionDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CFCFE2D1D56D31700A76388 /* SessionDelegate.swift */; }; - 4CFCFE391D56E8D900A76388 /* TaskDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CFCFE381D56E8D900A76388 /* TaskDelegate.swift */; }; - 4CFCFE3A1D56E8D900A76388 /* TaskDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CFCFE381D56E8D900A76388 /* TaskDelegate.swift */; }; - 4CFCFE3B1D56E8D900A76388 /* TaskDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CFCFE381D56E8D900A76388 /* TaskDelegate.swift */; }; - 4CFCFE3C1D56E8D900A76388 /* TaskDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CFCFE381D56E8D900A76388 /* TaskDelegate.swift */; }; 4DD67C241A5C58FB00ED2280 /* Alamofire.h in Headers */ = {isa = PBXBuildFile; fileRef = F8111E3819A95C8B0040E7D1 /* Alamofire.h */; settings = {ATTRIBUTES = (Public, ); }; }; 4DD67C251A5C590000ED2280 /* Alamofire.swift in Sources */ = {isa = PBXBuildFile; fileRef = F897FF4019AA800700AB5182 /* Alamofire.swift */; }; 8035DB621BAB492500466CB3 /* Alamofire.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F8111E3319A95C8B0040E7D1 /* Alamofire.framework */; }; E4202FD01B667AA100C997FB /* ParameterEncoding.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CE2724E1AF88FB500F1D59A /* ParameterEncoding.swift */; }; - E4202FD11B667AA100C997FB /* Request.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CDE2C391AF899EC00BABAE5 /* Request.swift */; }; E4202FD21B667AA100C997FB /* ResponseSerialization.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CDE2C451AF89FF300BABAE5 /* ResponseSerialization.swift */; }; - E4202FD31B667AA100C997FB /* SessionManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CDE2C361AF8932A00BABAE5 /* SessionManager.swift */; }; E4202FD41B667AA100C997FB /* Alamofire.swift in Sources */ = {isa = PBXBuildFile; fileRef = F897FF4019AA800700AB5182 /* Alamofire.swift */; }; E4202FD51B667AA100C997FB /* MultipartFormData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C23EB421B327C5B0090E0BC /* MultipartFormData.swift */; }; - E4202FD61B667AA100C997FB /* ServerTrustPolicy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C811F8C1B51856D00E0F59A /* ServerTrustPolicy.swift */; }; + E4202FD61B667AA100C997FB /* ServerTrustEvaluation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C811F8C1B51856D00E0F59A /* ServerTrustEvaluation.swift */; }; E4202FD81B667AA100C997FB /* Validation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CDE2C421AF89F0900BABAE5 /* Validation.swift */; }; F8111E3919A95C8B0040E7D1 /* Alamofire.h in Headers */ = {isa = PBXBuildFile; fileRef = F8111E3819A95C8B0040E7D1 /* Alamofire.h */; settings = {ATTRIBUTES = (Public, ); }; }; - F8111E6019A9674D0040E7D1 /* DownloadTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8111E5B19A9674D0040E7D1 /* DownloadTests.swift */; }; F8111E6119A9674D0040E7D1 /* ParameterEncodingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8111E5C19A9674D0040E7D1 /* ParameterEncodingTests.swift */; }; - F8111E6419A9674D0040E7D1 /* UploadTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8111E5F19A9674D0040E7D1 /* UploadTests.swift */; }; F829C6B81A7A94F100A2CD59 /* Alamofire.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4DD67C0B1A5C55C900ED2280 /* Alamofire.framework */; }; F829C6BE1A7A950600A2CD59 /* ParameterEncodingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8111E5C19A9674D0040E7D1 /* ParameterEncodingTests.swift */; }; F829C6BF1A7A950600A2CD59 /* RequestTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8111E5D19A9674D0040E7D1 /* RequestTests.swift */; }; - F829C6C01A7A950600A2CD59 /* SessionManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8D1C6F419D52968002E74FE /* SessionManagerTests.swift */; }; - F829C6C11A7A950600A2CD59 /* ResponseTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8111E5E19A9674D0040E7D1 /* ResponseTests.swift */; }; - F829C6C21A7A950600A2CD59 /* UploadTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8111E5F19A9674D0040E7D1 /* UploadTests.swift */; }; - F829C6C31A7A950600A2CD59 /* DownloadTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8111E5B19A9674D0040E7D1 /* DownloadTests.swift */; }; - F829C6C41A7A950600A2CD59 /* AuthenticationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8E6024419CB46A800A3E7F1 /* AuthenticationTests.swift */; }; - F829C6C51A7A950600A2CD59 /* ValidationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8AE910119D28DCC0078C7B2 /* ValidationTests.swift */; }; - F86AEFE71AE6A312007D9C76 /* TLSEvaluationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F86AEFE51AE6A282007D9C76 /* TLSEvaluationTests.swift */; }; - F86AEFE81AE6A315007D9C76 /* TLSEvaluationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F86AEFE51AE6A282007D9C76 /* TLSEvaluationTests.swift */; }; F8858DDD19A96B4300F55F93 /* RequestTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8111E5D19A9674D0040E7D1 /* RequestTests.swift */; }; - F8858DDE19A96B4400F55F93 /* ResponseTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8111E5E19A9674D0040E7D1 /* ResponseTests.swift */; }; F897FF4119AA800700AB5182 /* Alamofire.swift in Sources */ = {isa = PBXBuildFile; fileRef = F897FF4019AA800700AB5182 /* Alamofire.swift */; }; - F8AE910219D28DCC0078C7B2 /* ValidationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8AE910119D28DCC0078C7B2 /* ValidationTests.swift */; }; - F8D1C6F519D52968002E74FE /* SessionManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8D1C6F419D52968002E74FE /* SessionManagerTests.swift */; }; - F8E6024519CB46A800A3E7F1 /* AuthenticationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8E6024419CB46A800A3E7F1 /* AuthenticationTests.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -285,15 +337,34 @@ /* End PBXContainerItemProxy section */ /* Begin PBXFileReference section */ + 3111CE8720A77843008315E2 /* EventMonitor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventMonitor.swift; sourceTree = ""; }; + 3113D46A21878227001CCD21 /* HTTPHeadersTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HTTPHeadersTests.swift; sourceTree = ""; }; + 311B198F20B0D3B40036823B /* MultipartUpload.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MultipartUpload.swift; sourceTree = ""; }; 312D1E0B1FC2551400E51FF1 /* Usage.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; name = Usage.md; path = Documentation/Usage.md; sourceTree = ""; }; 312D1E0C1FC2551400E51FF1 /* AdvancedUsage.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; name = AdvancedUsage.md; path = Documentation/AdvancedUsage.md; sourceTree = ""; }; + 31501E872196962A005829F2 /* ParameterEncoderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParameterEncoderTests.swift; sourceTree = ""; }; 316250E41F00ABE900E207A6 /* ISSUE_TEMPLATE.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; name = ISSUE_TEMPLATE.md; path = .github/ISSUE_TEMPLATE.md; sourceTree = ""; }; 316250E51F00ACD000E207A6 /* PULL_REQUEST_TEMPLATE.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; name = PULL_REQUEST_TEMPLATE.md; path = .github/PULL_REQUEST_TEMPLATE.md; sourceTree = ""; }; + 31727417218BAEC90039FFCC /* HTTPMethod.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HTTPMethod.swift; sourceTree = ""; }; + 3172741C218BB1790039FFCC /* ParameterEncoder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParameterEncoder.swift; sourceTree = ""; }; + 31727421218BB9A50039FFCC /* HTTPBin.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HTTPBin.swift; sourceTree = ""; }; + 3191B5741F5F53A6003960A8 /* Protector.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Protector.swift; sourceTree = ""; }; + 31991790209CDA7F00103A19 /* Request.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Request.swift; sourceTree = ""; }; + 31991791209CDA7F00103A19 /* Response.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Response.swift; sourceTree = ""; }; + 31991792209CDA7F00103A19 /* Session.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Session.swift; sourceTree = ""; }; + 31991793209CDA7F00103A19 /* SessionStateProvider.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SessionStateProvider.swift; sourceTree = ""; }; + 319917A4209CDAC400103A19 /* RequestTaskMap.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RequestTaskMap.swift; sourceTree = ""; }; + 319917A9209CDCB000103A19 /* HTTPHeaders.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HTTPHeaders.swift; sourceTree = ""; }; + 319917AE209CE34E00103A19 /* RequestAdapter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RequestAdapter.swift; sourceTree = ""; }; + 319917B3209CE36E00103A19 /* RequestRetrier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RequestRetrier.swift; sourceTree = ""; }; + 319917B8209CE53A00103A19 /* OperationQueue+Alamofire.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "OperationQueue+Alamofire.swift"; sourceTree = ""; }; 31B2CA9421AA24F5005B371A /* Package@swift-4.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Package@swift-4.swift"; sourceTree = ""; }; 31B2CA9521AA25CD005B371A /* Package.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Package.swift; sourceTree = ""; }; + 31D83FCD20D5C29300D93E47 /* URLConvertible+URLRequestConvertible.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "URLConvertible+URLRequestConvertible.swift"; sourceTree = ""; }; 31ED52E61D73889D00199085 /* AFError+AlamofireTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "AFError+AlamofireTests.swift"; sourceTree = ""; }; + 31F5085C20B50DC400FE2A0C /* URLSessionConfiguration+Alamofire.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "URLSessionConfiguration+Alamofire.swift"; sourceTree = ""; }; + 31F9683B20BB70290009606F /* NSLoggingEventMonitor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NSLoggingEventMonitor.swift; sourceTree = ""; }; 4C0B58381B747A4400C0B99C /* ResponseSerializationTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ResponseSerializationTests.swift; sourceTree = ""; }; - 4C0B62501BB1001C009302D3 /* Response.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Response.swift; sourceTree = ""; }; 4C0E02681BF99A18004E7F18 /* Info-tvOS.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; name = "Info-tvOS.plist"; path = "Source/Info-tvOS.plist"; sourceTree = SOURCE_ROOT; }; 4C0E5BF71B673D3400816CCC /* Result.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Result.swift; sourceTree = ""; }; 4C1DC8531B68908E00476DE3 /* AFError.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AFError.swift; sourceTree = ""; }; @@ -302,13 +373,12 @@ 4C3238E61B3604DB00FE04AE /* MultipartFormDataTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MultipartFormDataTests.swift; sourceTree = ""; }; 4C33A1231B5207DB00873DFF /* rainbow.jpg */ = {isa = PBXFileReference; lastKnownFileType = image.jpeg; path = rainbow.jpg; sourceTree = ""; }; 4C33A1241B5207DB00873DFF /* unicorn.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = unicorn.png; sourceTree = ""; }; - 4C33A1421B52089C00873DFF /* ServerTrustPolicyTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ServerTrustPolicyTests.swift; sourceTree = ""; }; + 4C33A1421B52089C00873DFF /* ServerTrustEvaluatorTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ServerTrustEvaluatorTests.swift; sourceTree = ""; }; 4C341BB91B1A865A00C1B34D /* CacheTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CacheTests.swift; sourceTree = ""; }; 4C3D00531C66A63000D1F709 /* NetworkReachabilityManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NetworkReachabilityManager.swift; sourceTree = ""; }; 4C3D00571C66A8B900D1F709 /* NetworkReachabilityManagerTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NetworkReachabilityManagerTests.swift; sourceTree = ""; }; 4C43669A1D7BB93D00C38AAD /* DispatchQueue+Alamofire.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "DispatchQueue+Alamofire.swift"; sourceTree = ""; }; - 4C574E691C67D207000B3128 /* Timeline.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Timeline.swift; sourceTree = ""; }; - 4C811F8C1B51856D00E0F59A /* ServerTrustPolicy.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ServerTrustPolicy.swift; sourceTree = ""; }; + 4C811F8C1B51856D00E0F59A /* ServerTrustEvaluation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ServerTrustEvaluation.swift; sourceTree = ""; }; 4C812C3A1B535F220017E0BF /* alamofire-root-ca.cer */ = {isa = PBXFileReference; lastKnownFileType = file; name = "alamofire-root-ca.cer"; path = "alamofire.org/alamofire-root-ca.cer"; sourceTree = ""; }; 4C812C3D1B535F2E0017E0BF /* alamofire-signing-ca1.cer */ = {isa = PBXFileReference; lastKnownFileType = file; name = "alamofire-signing-ca1.cer"; path = "alamofire.org/alamofire-signing-ca1.cer"; sourceTree = ""; }; 4C812C3E1B535F2E0017E0BF /* alamofire-signing-ca2.cer */ = {isa = PBXFileReference; lastKnownFileType = file; name = "alamofire-signing-ca2.cer"; path = "alamofire.org/alamofire-signing-ca2.cer"; sourceTree = ""; }; @@ -332,8 +402,6 @@ 4CCB206A1D4549E000C64D5B /* expired.badssl.com-intermediate-ca-1.cer */ = {isa = PBXFileReference; lastKnownFileType = file; path = "expired.badssl.com-intermediate-ca-1.cer"; sourceTree = ""; }; 4CCB206B1D4549E000C64D5B /* expired.badssl.com-intermediate-ca-2.cer */ = {isa = PBXFileReference; lastKnownFileType = file; path = "expired.badssl.com-intermediate-ca-2.cer"; sourceTree = ""; }; 4CCFA7991B2BE71600B6F460 /* URLProtocolTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = URLProtocolTests.swift; sourceTree = ""; }; - 4CDE2C361AF8932A00BABAE5 /* SessionManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SessionManager.swift; sourceTree = ""; }; - 4CDE2C391AF899EC00BABAE5 /* Request.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Request.swift; sourceTree = ""; }; 4CDE2C421AF89F0900BABAE5 /* Validation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Validation.swift; sourceTree = ""; }; 4CDE2C451AF89FF300BABAE5 /* ResponseSerialization.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ResponseSerialization.swift; sourceTree = ""; }; 4CE2724E1AF88FB500F1D59A /* ParameterEncoding.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ParameterEncoding.swift; sourceTree = ""; }; @@ -355,10 +423,7 @@ 4CFB02F21D7D2FA20056F249 /* empty_string.txt */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = empty_string.txt; sourceTree = ""; }; 4CFB02F31D7D2FA20056F249 /* utf32_string.txt */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = utf32_string.txt; sourceTree = ""; }; 4CFB02F41D7D2FA20056F249 /* utf8_string.txt */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = utf8_string.txt; sourceTree = ""; }; - 4CFCFE2D1D56D31700A76388 /* SessionDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SessionDelegate.swift; sourceTree = ""; }; - 4CFCFE381D56E8D900A76388 /* TaskDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TaskDelegate.swift; sourceTree = ""; }; 4DD67C0B1A5C55C900ED2280 /* Alamofire.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Alamofire.framework; sourceTree = BUILT_PRODUCTS_DIR; }; - 72998D721BF26173006D3F69 /* Info-tvOS.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = "Info-tvOS.plist"; sourceTree = ""; }; B39E2F831C1A72F8002DA1A9 /* certDER.cer */ = {isa = PBXFileReference; lastKnownFileType = file; name = certDER.cer; path = selfSignedAndMalformedCerts/certDER.cer; sourceTree = ""; }; B39E2F841C1A72F8002DA1A9 /* certDER.crt */ = {isa = PBXFileReference; lastKnownFileType = file; name = certDER.crt; path = selfSignedAndMalformedCerts/certDER.crt; sourceTree = ""; }; B39E2F851C1A72F8002DA1A9 /* certDER.der */ = {isa = PBXFileReference; lastKnownFileType = file; name = certDER.der; path = selfSignedAndMalformedCerts/certDER.der; sourceTree = ""; }; @@ -381,7 +446,7 @@ F86AEFE51AE6A282007D9C76 /* TLSEvaluationTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TLSEvaluationTests.swift; sourceTree = ""; }; F897FF4019AA800700AB5182 /* Alamofire.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Alamofire.swift; sourceTree = ""; }; F8AE910119D28DCC0078C7B2 /* ValidationTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ValidationTests.swift; sourceTree = ""; }; - F8D1C6F419D52968002E74FE /* SessionManagerTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SessionManagerTests.swift; sourceTree = ""; }; + F8D1C6F419D52968002E74FE /* SessionTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SessionTests.swift; sourceTree = ""; }; F8E6024419CB46A800A3E7F1 /* AuthenticationTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AuthenticationTests.swift; sourceTree = ""; }; /* End PBXFileReference section */ @@ -446,12 +511,13 @@ children = ( F8E6024419CB46A800A3E7F1 /* AuthenticationTests.swift */, F8111E5B19A9674D0040E7D1 /* DownloadTests.swift */, + 31501E872196962A005829F2 /* ParameterEncoderTests.swift */, F8111E5C19A9674D0040E7D1 /* ParameterEncodingTests.swift */, F8111E5D19A9674D0040E7D1 /* RequestTests.swift */, F8111E5E19A9674D0040E7D1 /* ResponseTests.swift */, 4CA028C41B7466C500C84163 /* ResultTests.swift */, 4C9DCE771CB1BCE2003E6463 /* SessionDelegateTests.swift */, - F8D1C6F419D52968002E74FE /* SessionManagerTests.swift */, + F8D1C6F419D52968002E74FE /* SessionTests.swift */, F8111E5F19A9674D0040E7D1 /* UploadTests.swift */, ); name = Core; @@ -461,10 +527,11 @@ isa = PBXGroup; children = ( 4C341BB91B1A865A00C1B34D /* CacheTests.swift */, + 3113D46A21878227001CCD21 /* HTTPHeadersTests.swift */, 4C3238E61B3604DB00FE04AE /* MultipartFormDataTests.swift */, 4C3D00571C66A8B900D1F709 /* NetworkReachabilityManagerTests.swift */, 4C0B58381B747A4400C0B99C /* ResponseSerializationTests.swift */, - 4C33A1421B52089C00873DFF /* ServerTrustPolicyTests.swift */, + 4C33A1421B52089C00873DFF /* ServerTrustEvaluatorTests.swift */, F86AEFE51AE6A282007D9C76 /* TLSEvaluationTests.swift */, 4CCFA7991B2BE71600B6F460 /* URLProtocolTests.swift */, F8AE910119D28DCC0078C7B2 /* ValidationTests.swift */, @@ -556,6 +623,8 @@ isa = PBXGroup; children = ( 4C43669A1D7BB93D00C38AAD /* DispatchQueue+Alamofire.swift */, + 319917B8209CE53A00103A19 /* OperationQueue+Alamofire.swift */, + 31F5085C20B50DC400FE2A0C /* URLSessionConfiguration+Alamofire.swift */, ); name = Extensions; sourceTree = ""; @@ -604,14 +673,19 @@ isa = PBXGroup; children = ( 4C1DC8531B68908E00476DE3 /* AFError.swift */, + 319917A9209CDCB000103A19 /* HTTPHeaders.swift */, + 31727417218BAEC90039FFCC /* HTTPMethod.swift */, 4CB928281C66BFBC00CE5F08 /* Notifications.swift */, 4CE2724E1AF88FB500F1D59A /* ParameterEncoding.swift */, - 4CDE2C391AF899EC00BABAE5 /* Request.swift */, - 4C0B62501BB1001C009302D3 /* Response.swift */, + 3172741C218BB1790039FFCC /* ParameterEncoder.swift */, + 3191B5741F5F53A6003960A8 /* Protector.swift */, + 31991790209CDA7F00103A19 /* Request.swift */, + 319917A4209CDAC400103A19 /* RequestTaskMap.swift */, + 31991791209CDA7F00103A19 /* Response.swift */, 4C0E5BF71B673D3400816CCC /* Result.swift */, - 4CFCFE2D1D56D31700A76388 /* SessionDelegate.swift */, - 4CDE2C361AF8932A00BABAE5 /* SessionManager.swift */, - 4CFCFE381D56E8D900A76388 /* TaskDelegate.swift */, + 31991792209CDA7F00103A19 /* Session.swift */, + 31991793209CDA7F00103A19 /* SessionStateProvider.swift */, + 31D83FCD20D5C29300D93E47 /* URLConvertible+URLRequestConvertible.swift */, ); name = Core; sourceTree = ""; @@ -619,11 +693,14 @@ 4CDE2C491AF8A14E00BABAE5 /* Features */ = { isa = PBXGroup; children = ( + 3111CE8720A77843008315E2 /* EventMonitor.swift */, 4C23EB421B327C5B0090E0BC /* MultipartFormData.swift */, + 311B198F20B0D3B40036823B /* MultipartUpload.swift */, 4C3D00531C66A63000D1F709 /* NetworkReachabilityManager.swift */, + 319917AE209CE34E00103A19 /* RequestAdapter.swift */, + 319917B3209CE36E00103A19 /* RequestRetrier.swift */, 4CDE2C451AF89FF300BABAE5 /* ResponseSerialization.swift */, - 4C811F8C1B51856D00E0F59A /* ServerTrustPolicy.swift */, - 4C574E691C67D207000B3128 /* Timeline.swift */, + 4C811F8C1B51856D00E0F59A /* ServerTrustEvaluation.swift */, 4CDE2C421AF89F0900BABAE5 /* Validation.swift */, ); name = Features; @@ -756,7 +833,6 @@ children = ( F8111E3819A95C8B0040E7D1 /* Alamofire.h */, F8111E3719A95C8B0040E7D1 /* Info.plist */, - 72998D721BF26173006D3F69 /* Info-tvOS.plist */, ); name = "Supporting Files"; sourceTree = ""; @@ -765,6 +841,8 @@ isa = PBXGroup; children = ( 4C256A501B096C2C0065714F /* BaseTestCase.swift */, + 31727421218BB9A50039FFCC /* HTTPBin.swift */, + 31F9683B20BB70290009606F /* NSLoggingEventMonitor.swift */, 4C256A4E1B09656A0065714F /* Core */, 4C7C8D201B9D0D7300948136 /* Extensions */, 4C256A4F1B09656E0065714F /* Features */, @@ -954,19 +1032,22 @@ isa = PBXProject; attributes = { LastSwiftUpdateCheck = 0700; - LastUpgradeCheck = 0930; + LastUpgradeCheck = 1010; ORGANIZATIONNAME = Alamofire; TargetAttributes = { 4CCB207C1D45563900C64D5B = { CreatedOnToolsVersion = 7.3.1; + ProvisioningStyle = Manual; }; 4CF626EE1BA7CB3E0011A099 = { CreatedOnToolsVersion = 7.1; LastSwiftMigration = 0900; + ProvisioningStyle = Manual; }; 4CF626F71BA7CB3E0011A099 = { CreatedOnToolsVersion = 7.1; LastSwiftMigration = 0900; + ProvisioningStyle = Manual; }; 4DD67C0A1A5C55C900ED2280 = { CreatedOnToolsVersion = 6.1.1; @@ -975,18 +1056,22 @@ }; E4202FCD1B667AA100C997FB = { LastSwiftMigration = 0900; + ProvisioningStyle = Manual; }; F8111E3219A95C8B0040E7D1 = { CreatedOnToolsVersion = 6.0; LastSwiftMigration = 0900; + ProvisioningStyle = Manual; }; F8111E3D19A95C8B0040E7D1 = { CreatedOnToolsVersion = 6.0; LastSwiftMigration = 0900; + ProvisioningStyle = Manual; }; F829C6B11A7A94F100A2CD59 = { CreatedOnToolsVersion = 6.1.1; LastSwiftMigration = 0900; + ProvisioningStyle = Manual; }; }; }; @@ -1190,23 +1275,33 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - 4C574E6C1C67D207000B3128 /* Timeline.swift in Sources */, - 4CFCFE301D56D31700A76388 /* SessionDelegate.swift in Sources */, 4CF6270C1BA7CBF60011A099 /* Result.swift in Sources */, 4CF627081BA7CBF60011A099 /* AFError.swift in Sources */, + 3191B5771F5F53A6003960A8 /* Protector.swift in Sources */, + 3199179A209CDA7F00103A19 /* Response.swift in Sources */, + 31D83FD020D5C29300D93E47 /* URLConvertible+URLRequestConvertible.swift in Sources */, + 319917A7209CDAC400103A19 /* RequestTaskMap.swift in Sources */, 4CF627131BA7CBF60011A099 /* Validation.swift in Sources */, + 31F5085F20B50DC400FE2A0C /* URLSessionConfiguration+Alamofire.swift in Sources */, + 3172741F218BB1790039FFCC /* ParameterEncoder.swift in Sources */, + 319917BB209CE53A00103A19 /* OperationQueue+Alamofire.swift in Sources */, + 319917AC209CDCB000103A19 /* HTTPHeaders.swift in Sources */, + 3172741A218BAEC90039FFCC /* HTTPMethod.swift in Sources */, 4CF6270E1BA7CBF60011A099 /* MultipartFormData.swift in Sources */, - 4C80F9F81BB730EF001B46D2 /* Response.swift in Sources */, 4CB9282B1C66BFBC00CE5F08 /* Notifications.swift in Sources */, - 4CF627091BA7CBF60011A099 /* SessionManager.swift in Sources */, 4CF6270F1BA7CBF60011A099 /* ResponseSerialization.swift in Sources */, - 4CF6270B1BA7CBF60011A099 /* Request.swift in Sources */, + 319917B6209CE36E00103A19 /* RequestRetrier.swift in Sources */, 4C43669D1D7BB93D00C38AAD /* DispatchQueue+Alamofire.swift in Sources */, 4C3D00561C66A63000D1F709 /* NetworkReachabilityManager.swift in Sources */, - 4CFCFE3B1D56E8D900A76388 /* TaskDelegate.swift in Sources */, + 311B199220B0E3480036823B /* MultipartUpload.swift in Sources */, + 319917A2209CDA7F00103A19 /* SessionStateProvider.swift in Sources */, 4CF6270A1BA7CBF60011A099 /* ParameterEncoding.swift in Sources */, - 4CF627101BA7CBF60011A099 /* ServerTrustPolicy.swift in Sources */, + 31991796209CDA7F00103A19 /* Request.swift in Sources */, + 4CF627101BA7CBF60011A099 /* ServerTrustEvaluation.swift in Sources */, + 319917B1209CE34F00103A19 /* RequestAdapter.swift in Sources */, + 3199179E209CDA7F00103A19 /* Session.swift in Sources */, 4CF627071BA7CBF60011A099 /* Alamofire.swift in Sources */, + 3111CE8A20A77945008315E2 /* EventMonitor.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -1215,25 +1310,29 @@ buildActionMask = 2147483647; files = ( 4CF627181BA7CC240011A099 /* RequestTests.swift in Sources */, - 4CF627211BA7CC240011A099 /* TLSEvaluationTests.swift in Sources */, - 4CF627221BA7CC240011A099 /* UploadTests.swift in Sources */, - 4C9DCE7A1CB1BCE2003E6463 /* SessionDelegateTests.swift in Sources */, - 4CF6271E1BA7CC240011A099 /* MultipartFormDataTests.swift in Sources */, + 3111CE9720A7EC3A008315E2 /* ServerTrustEvaluatorTests.swift in Sources */, + 3111CE9420A7EC32008315E2 /* ResponseSerializationTests.swift in Sources */, + 3111CE8E20A7EBE7008315E2 /* MultipartFormDataTests.swift in Sources */, + 311B199620B0ED990036823B /* UploadTests.swift in Sources */, + 3107EA3720A11AE200445260 /* AuthenticationTests.swift in Sources */, 31ED52EA1D73891C00199085 /* AFError+AlamofireTests.swift in Sources */, - 4CF627201BA7CC240011A099 /* ServerTrustPolicyTests.swift in Sources */, + 3107EA3A20A11F9700445260 /* ResponseTests.swift in Sources */, 4CFB02921D7CF28F0056F249 /* FileManager+AlamofireTests.swift in Sources */, - 4CF627241BA7CC240011A099 /* ValidationTests.swift in Sources */, 4CF627141BA7CC240011A099 /* BaseTestCase.swift in Sources */, - 4CF627151BA7CC240011A099 /* AuthenticationTests.swift in Sources */, + 31727424218BB9A50039FFCC /* HTTPBin.swift in Sources */, + 31EBD9C320D1D89D00D1FF34 /* ValidationTests.swift in Sources */, + 3111CE8620A76370008315E2 /* SessionTests.swift in Sources */, + 31C2B0F220B271380089BA7C /* TLSEvaluationTests.swift in Sources */, + 3111CE9D20A7EC58008315E2 /* URLProtocolTests.swift in Sources */, + 317A6A7820B2208000A9FEC5 /* DownloadTests.swift in Sources */, + 31F9683E20BB70290009606F /* NSLoggingEventMonitor.swift in Sources */, + 3113D46D21878227001CCD21 /* HTTPHeadersTests.swift in Sources */, + 31501E8A2196962A005829F2 /* ParameterEncoderTests.swift in Sources */, + 3107EA4120A1267D00445260 /* SessionDelegateTests.swift in Sources */, + 31C2B0EC20B271060089BA7C /* CacheTests.swift in Sources */, + 3111CE9120A7EC27008315E2 /* NetworkReachabilityManagerTests.swift in Sources */, + 3107EA3E20A124EB00445260 /* ResultTests.swift in Sources */, 4CF627171BA7CC240011A099 /* ParameterEncodingTests.swift in Sources */, - 4CF627191BA7CC240011A099 /* ResponseTests.swift in Sources */, - 4CF627231BA7CC240011A099 /* URLProtocolTests.swift in Sources */, - 4CF6271C1BA7CC240011A099 /* CacheTests.swift in Sources */, - 4CF627161BA7CC240011A099 /* SessionManagerTests.swift in Sources */, - 4CF6271A1BA7CC240011A099 /* ResultTests.swift in Sources */, - 4C3D005A1C66A8B900D1F709 /* NetworkReachabilityManagerTests.swift in Sources */, - 4CF6271F1BA7CC240011A099 /* ResponseSerializationTests.swift in Sources */, - 4CF6271D1BA7CC240011A099 /* DownloadTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -1241,23 +1340,33 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - 4C574E6B1C67D207000B3128 /* Timeline.swift in Sources */, - 4CFCFE2F1D56D31700A76388 /* SessionDelegate.swift in Sources */, 4CE272501AF88FB500F1D59A /* ParameterEncoding.swift in Sources */, - 4CDE2C3B1AF899EC00BABAE5 /* Request.swift in Sources */, + 3191B5761F5F53A6003960A8 /* Protector.swift in Sources */, 4CDE2C471AF89FF300BABAE5 /* ResponseSerialization.swift in Sources */, + 31991799209CDA7F00103A19 /* Response.swift in Sources */, + 31D83FCF20D5C29300D93E47 /* URLConvertible+URLRequestConvertible.swift in Sources */, + 319917A6209CDAC400103A19 /* RequestTaskMap.swift in Sources */, 4C1DC8551B68908E00476DE3 /* AFError.swift in Sources */, - 4CDE2C381AF8932A00BABAE5 /* SessionManager.swift in Sources */, - 4C0B62521BB1001C009302D3 /* Response.swift in Sources */, + 31F5085E20B50DC400FE2A0C /* URLSessionConfiguration+Alamofire.swift in Sources */, + 3172741E218BB1790039FFCC /* ParameterEncoder.swift in Sources */, + 319917BA209CE53A00103A19 /* OperationQueue+Alamofire.swift in Sources */, + 319917AB209CDCB000103A19 /* HTTPHeaders.swift in Sources */, + 31727419218BAEC90039FFCC /* HTTPMethod.swift in Sources */, 4CB9282A1C66BFBC00CE5F08 /* Notifications.swift in Sources */, 4DD67C251A5C590000ED2280 /* Alamofire.swift in Sources */, 4C23EB441B327C5B0090E0BC /* MultipartFormData.swift in Sources */, + 319917B5209CE36E00103A19 /* RequestRetrier.swift in Sources */, 4C43669C1D7BB93D00C38AAD /* DispatchQueue+Alamofire.swift in Sources */, - 4C811F8E1B51856D00E0F59A /* ServerTrustPolicy.swift in Sources */, - 4CFCFE3A1D56E8D900A76388 /* TaskDelegate.swift in Sources */, + 4C811F8E1B51856D00E0F59A /* ServerTrustEvaluation.swift in Sources */, + 311B199120B0E3470036823B /* MultipartUpload.swift in Sources */, + 319917A1209CDA7F00103A19 /* SessionStateProvider.swift in Sources */, 4C3D00551C66A63000D1F709 /* NetworkReachabilityManager.swift in Sources */, + 31991795209CDA7F00103A19 /* Request.swift in Sources */, 4CDE2C441AF89F0900BABAE5 /* Validation.swift in Sources */, + 319917B0209CE34F00103A19 /* RequestAdapter.swift in Sources */, + 3199179D209CDA7F00103A19 /* Session.swift in Sources */, 4C0E5BF91B673D3400816CCC /* Result.swift in Sources */, + 3111CE8920A77944008315E2 /* EventMonitor.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -1265,23 +1374,33 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - 4C574E6D1C67D207000B3128 /* Timeline.swift in Sources */, 4CEE82AD1C6813CF00E9C9F0 /* NetworkReachabilityManager.swift in Sources */, - 4CFCFE311D56D31700A76388 /* SessionDelegate.swift in Sources */, E4202FD01B667AA100C997FB /* ParameterEncoding.swift in Sources */, - E4202FD11B667AA100C997FB /* Request.swift in Sources */, + 3191B5781F5F53A6003960A8 /* Protector.swift in Sources */, + 3199179B209CDA7F00103A19 /* Response.swift in Sources */, + 31D83FD120D5C29300D93E47 /* URLConvertible+URLRequestConvertible.swift in Sources */, + 319917A8209CDAC400103A19 /* RequestTaskMap.swift in Sources */, 4CEC605A1B745C9100E684F4 /* AFError.swift in Sources */, + 31F5086020B50DC400FE2A0C /* URLSessionConfiguration+Alamofire.swift in Sources */, + 31727420218BB1790039FFCC /* ParameterEncoder.swift in Sources */, + 319917BC209CE53A00103A19 /* OperationQueue+Alamofire.swift in Sources */, + 319917AD209CDCB000103A19 /* HTTPHeaders.swift in Sources */, + 3172741B218BAEC90039FFCC /* HTTPMethod.swift in Sources */, E4202FD21B667AA100C997FB /* ResponseSerialization.swift in Sources */, - E4202FD31B667AA100C997FB /* SessionManager.swift in Sources */, - 4C0B62531BB1001C009302D3 /* Response.swift in Sources */, 4CB9282C1C66BFBC00CE5F08 /* Notifications.swift in Sources */, 4CEC605B1B745C9100E684F4 /* Result.swift in Sources */, + 319917B7209CE36E00103A19 /* RequestRetrier.swift in Sources */, 4C43669E1D7BB93D00C38AAD /* DispatchQueue+Alamofire.swift in Sources */, E4202FD41B667AA100C997FB /* Alamofire.swift in Sources */, - 4CFCFE3C1D56E8D900A76388 /* TaskDelegate.swift in Sources */, + 311B199320B0E3480036823B /* MultipartUpload.swift in Sources */, + 319917A3209CDA7F00103A19 /* SessionStateProvider.swift in Sources */, E4202FD51B667AA100C997FB /* MultipartFormData.swift in Sources */, - E4202FD61B667AA100C997FB /* ServerTrustPolicy.swift in Sources */, + 31991797209CDA7F00103A19 /* Request.swift in Sources */, + E4202FD61B667AA100C997FB /* ServerTrustEvaluation.swift in Sources */, + 319917B2209CE34F00103A19 /* RequestAdapter.swift in Sources */, + 3199179F209CDA7F00103A19 /* Session.swift in Sources */, E4202FD81B667AA100C997FB /* Validation.swift in Sources */, + 3111CE8B20A77945008315E2 /* EventMonitor.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -1289,23 +1408,33 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - 4C574E6A1C67D207000B3128 /* Timeline.swift in Sources */, - 4CFCFE2E1D56D31700A76388 /* SessionDelegate.swift in Sources */, 4CE2724F1AF88FB500F1D59A /* ParameterEncoding.swift in Sources */, - 4CDE2C3A1AF899EC00BABAE5 /* Request.swift in Sources */, + 3191B5751F5F53A6003960A8 /* Protector.swift in Sources */, 4CDE2C461AF89FF300BABAE5 /* ResponseSerialization.swift in Sources */, + 31991798209CDA7F00103A19 /* Response.swift in Sources */, + 31D83FCE20D5C29300D93E47 /* URLConvertible+URLRequestConvertible.swift in Sources */, + 319917A5209CDAC400103A19 /* RequestTaskMap.swift in Sources */, 4C1DC8541B68908E00476DE3 /* AFError.swift in Sources */, - 4CDE2C371AF8932A00BABAE5 /* SessionManager.swift in Sources */, - 4C0B62511BB1001C009302D3 /* Response.swift in Sources */, + 31F5085D20B50DC400FE2A0C /* URLSessionConfiguration+Alamofire.swift in Sources */, + 3172741D218BB1790039FFCC /* ParameterEncoder.swift in Sources */, + 319917B9209CE53A00103A19 /* OperationQueue+Alamofire.swift in Sources */, + 319917AA209CDCB000103A19 /* HTTPHeaders.swift in Sources */, + 31727418218BAEC90039FFCC /* HTTPMethod.swift in Sources */, F897FF4119AA800700AB5182 /* Alamofire.swift in Sources */, 4C23EB431B327C5B0090E0BC /* MultipartFormData.swift in Sources */, - 4C811F8D1B51856D00E0F59A /* ServerTrustPolicy.swift in Sources */, + 4C811F8D1B51856D00E0F59A /* ServerTrustEvaluation.swift in Sources */, + 319917B4209CE36E00103A19 /* RequestRetrier.swift in Sources */, 4C43669B1D7BB93D00C38AAD /* DispatchQueue+Alamofire.swift in Sources */, 4C3D00541C66A63000D1F709 /* NetworkReachabilityManager.swift in Sources */, - 4CFCFE391D56E8D900A76388 /* TaskDelegate.swift in Sources */, + 311B199020B0D3B40036823B /* MultipartUpload.swift in Sources */, + 319917A0209CDA7F00103A19 /* SessionStateProvider.swift in Sources */, 4CDE2C431AF89F0900BABAE5 /* Validation.swift in Sources */, + 31991794209CDA7F00103A19 /* Request.swift in Sources */, 4CB928291C66BFBC00CE5F08 /* Notifications.swift in Sources */, + 319917AF209CE34F00103A19 /* RequestAdapter.swift in Sources */, + 3199179C209CDA7F00103A19 /* Session.swift in Sources */, 4C0E5BF81B673D3400816CCC /* Result.swift in Sources */, + 3111CE8820A77843008315E2 /* EventMonitor.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -1313,26 +1442,30 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - 4C3238E71B3604DB00FE04AE /* MultipartFormDataTests.swift in Sources */, - 4C33A1431B52089C00873DFF /* ServerTrustPolicyTests.swift in Sources */, - 4C341BBA1B1A865A00C1B34D /* CacheTests.swift in Sources */, - 4C9DCE781CB1BCE2003E6463 /* SessionDelegateTests.swift in Sources */, - 4CA028C51B7466C500C84163 /* ResultTests.swift in Sources */, 31ED52E81D73891B00199085 /* AFError+AlamofireTests.swift in Sources */, - 4CCFA79A1B2BE71600B6F460 /* URLProtocolTests.swift in Sources */, + 3111CE9520A7EC39008315E2 /* ServerTrustEvaluatorTests.swift in Sources */, + 3111CE9220A7EC30008315E2 /* ResponseSerializationTests.swift in Sources */, + 3111CE8C20A7EBE6008315E2 /* MultipartFormDataTests.swift in Sources */, + 311B199420B0ED980036823B /* UploadTests.swift in Sources */, + 3107EA3520A11AE100445260 /* AuthenticationTests.swift in Sources */, 4CFB02901D7CF28F0056F249 /* FileManager+AlamofireTests.swift in Sources */, - F86AEFE71AE6A312007D9C76 /* TLSEvaluationTests.swift in Sources */, - 4C0B58391B747A4400C0B99C /* ResponseSerializationTests.swift in Sources */, + 3107EA3820A11F9600445260 /* ResponseTests.swift in Sources */, F8858DDD19A96B4300F55F93 /* RequestTests.swift in Sources */, 4C256A531B096C770065714F /* BaseTestCase.swift in Sources */, - F8E6024519CB46A800A3E7F1 /* AuthenticationTests.swift in Sources */, - F8858DDE19A96B4400F55F93 /* ResponseTests.swift in Sources */, - F8D1C6F519D52968002E74FE /* SessionManagerTests.swift in Sources */, - F8AE910219D28DCC0078C7B2 /* ValidationTests.swift in Sources */, + 31727422218BB9A50039FFCC /* HTTPBin.swift in Sources */, + 31EBD9C120D1D89C00D1FF34 /* ValidationTests.swift in Sources */, + 3111CE8420A7636E008315E2 /* SessionTests.swift in Sources */, + 31C2B0F020B271370089BA7C /* TLSEvaluationTests.swift in Sources */, + 3111CE9B20A7EC57008315E2 /* URLProtocolTests.swift in Sources */, + 317A6A7620B2207F00A9FEC5 /* DownloadTests.swift in Sources */, + 31F9683C20BB70290009606F /* NSLoggingEventMonitor.swift in Sources */, + 3113D46B21878227001CCD21 /* HTTPHeadersTests.swift in Sources */, + 31501E882196962A005829F2 /* ParameterEncoderTests.swift in Sources */, + 3107EA3F20A1267C00445260 /* SessionDelegateTests.swift in Sources */, + 31C2B0EA20B271040089BA7C /* CacheTests.swift in Sources */, + 3111CE8F20A7EC26008315E2 /* NetworkReachabilityManagerTests.swift in Sources */, + 3107EA3C20A124E900445260 /* ResultTests.swift in Sources */, F8111E6119A9674D0040E7D1 /* ParameterEncodingTests.swift in Sources */, - F8111E6419A9674D0040E7D1 /* UploadTests.swift in Sources */, - 4C3D00581C66A8B900D1F709 /* NetworkReachabilityManagerTests.swift in Sources */, - F8111E6019A9674D0040E7D1 /* DownloadTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -1340,26 +1473,30 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - 4C3238E81B3604DB00FE04AE /* MultipartFormDataTests.swift in Sources */, - 4C33A1441B52089C00873DFF /* ServerTrustPolicyTests.swift in Sources */, - 4C341BBB1B1A865A00C1B34D /* CacheTests.swift in Sources */, - 4C9DCE791CB1BCE2003E6463 /* SessionDelegateTests.swift in Sources */, - 4CA028C61B7466C500C84163 /* ResultTests.swift in Sources */, 31ED52E91D73891C00199085 /* AFError+AlamofireTests.swift in Sources */, - 4CCFA79B1B2BE71600B6F460 /* URLProtocolTests.swift in Sources */, + 3111CE9620A7EC3A008315E2 /* ServerTrustEvaluatorTests.swift in Sources */, + 3111CE9320A7EC31008315E2 /* ResponseSerializationTests.swift in Sources */, + 3111CE8D20A7EBE7008315E2 /* MultipartFormDataTests.swift in Sources */, + 311B199520B0ED980036823B /* UploadTests.swift in Sources */, + 3107EA3620A11AE100445260 /* AuthenticationTests.swift in Sources */, 4CFB02911D7CF28F0056F249 /* FileManager+AlamofireTests.swift in Sources */, + 3107EA3920A11F9600445260 /* ResponseTests.swift in Sources */, F829C6BE1A7A950600A2CD59 /* ParameterEncodingTests.swift in Sources */, - 4C0B583A1B747A4400C0B99C /* ResponseSerializationTests.swift in Sources */, F829C6BF1A7A950600A2CD59 /* RequestTests.swift in Sources */, + 31727423218BB9A50039FFCC /* HTTPBin.swift in Sources */, + 31EBD9C220D1D89C00D1FF34 /* ValidationTests.swift in Sources */, + 3111CE8520A7636F008315E2 /* SessionTests.swift in Sources */, + 31C2B0F120B271370089BA7C /* TLSEvaluationTests.swift in Sources */, + 3111CE9C20A7EC58008315E2 /* URLProtocolTests.swift in Sources */, + 317A6A7720B2208000A9FEC5 /* DownloadTests.swift in Sources */, + 31F9683D20BB70290009606F /* NSLoggingEventMonitor.swift in Sources */, + 3113D46C21878227001CCD21 /* HTTPHeadersTests.swift in Sources */, + 31501E892196962A005829F2 /* ParameterEncoderTests.swift in Sources */, + 3107EA4020A1267C00445260 /* SessionDelegateTests.swift in Sources */, + 31C2B0EB20B271050089BA7C /* CacheTests.swift in Sources */, + 3111CE9020A7EC27008315E2 /* NetworkReachabilityManagerTests.swift in Sources */, + 3107EA3D20A124EA00445260 /* ResultTests.swift in Sources */, 4C256A541B096C770065714F /* BaseTestCase.swift in Sources */, - F829C6C01A7A950600A2CD59 /* SessionManagerTests.swift in Sources */, - F829C6C11A7A950600A2CD59 /* ResponseTests.swift in Sources */, - F829C6C21A7A950600A2CD59 /* UploadTests.swift in Sources */, - F829C6C31A7A950600A2CD59 /* DownloadTests.swift in Sources */, - F829C6C41A7A950600A2CD59 /* AuthenticationTests.swift in Sources */, - F829C6C51A7A950600A2CD59 /* ValidationTests.swift in Sources */, - 4C3D00591C66A8B900D1F709 /* NetworkReachabilityManagerTests.swift in Sources */, - F86AEFE81AE6A315007D9C76 /* TLSEvaluationTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -1401,38 +1538,30 @@ 4CF627001BA7CB3E0011A099 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { - "CODE_SIGN_IDENTITY[sdk=appletvos*]" = ""; + CODE_SIGN_IDENTITY = ""; DEFINES_MODULE = YES; DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; - INFOPLIST_FILE = "Source/Info-tvOS.plist"; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; - PRODUCT_BUNDLE_IDENTIFIER = org.alamofire.Alamofire; - PRODUCT_NAME = Alamofire; SDKROOT = appletvos; SKIP_INSTALL = YES; - TARGETED_DEVICE_FAMILY = 3; }; name = Debug; }; 4CF627011BA7CB3E0011A099 /* Release */ = { isa = XCBuildConfiguration; buildSettings = { - "CODE_SIGN_IDENTITY[sdk=appletvos*]" = ""; + CODE_SIGN_IDENTITY = ""; DEFINES_MODULE = YES; DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; - INFOPLIST_FILE = "Source/Info-tvOS.plist"; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; - PRODUCT_BUNDLE_IDENTIFIER = org.alamofire.Alamofire; - PRODUCT_NAME = Alamofire; SDKROOT = appletvos; SKIP_INSTALL = YES; - TARGETED_DEVICE_FAMILY = 3; }; name = Release; }; @@ -1463,17 +1592,13 @@ 4DD67C1F1A5C55C900ED2280 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { - CODE_SIGN_IDENTITY = ""; DEFINES_MODULE = YES; DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; FRAMEWORK_VERSION = A; - INFOPLIST_FILE = Source/Info.plist; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/Frameworks"; - PRODUCT_BUNDLE_IDENTIFIER = org.alamofire.Alamofire; - PRODUCT_NAME = Alamofire; SDKROOT = macosx; SKIP_INSTALL = YES; }; @@ -1482,17 +1607,13 @@ 4DD67C201A5C55C900ED2280 /* Release */ = { isa = XCBuildConfiguration; buildSettings = { - CODE_SIGN_IDENTITY = ""; DEFINES_MODULE = YES; DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; FRAMEWORK_VERSION = A; - INFOPLIST_FILE = Source/Info.plist; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/Frameworks"; - PRODUCT_BUNDLE_IDENTIFIER = org.alamofire.Alamofire; - PRODUCT_NAME = Alamofire; SDKROOT = macosx; SKIP_INSTALL = YES; }; @@ -1501,38 +1622,30 @@ E4202FDE1B667AA100C997FB /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { - "CODE_SIGN_IDENTITY[sdk=watchos*]" = ""; + CODE_SIGN_IDENTITY = ""; DEFINES_MODULE = YES; DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; - INFOPLIST_FILE = Source/Info.plist; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; - PRODUCT_BUNDLE_IDENTIFIER = org.alamofire.Alamofire; - PRODUCT_NAME = Alamofire; SDKROOT = watchos; SKIP_INSTALL = YES; - TARGETED_DEVICE_FAMILY = 4; }; name = Debug; }; E4202FDF1B667AA100C997FB /* Release */ = { isa = XCBuildConfiguration; buildSettings = { - "CODE_SIGN_IDENTITY[sdk=watchos*]" = ""; + CODE_SIGN_IDENTITY = ""; DEFINES_MODULE = YES; DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; - INFOPLIST_FILE = Source/Info.plist; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; - PRODUCT_BUNDLE_IDENTIFIER = org.alamofire.Alamofire; - PRODUCT_NAME = Alamofire; SDKROOT = watchos; SKIP_INSTALL = YES; - TARGETED_DEVICE_FAMILY = 4; }; name = Release; }; @@ -1566,7 +1679,7 @@ CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; - CODE_SIGN_IDENTITY = "iPhone Developer"; + CODE_SIGN_STYLE = Manual; COPY_PHASE_STRIP = NO; CURRENT_PROJECT_VERSION = 1; DEBUG_INFORMATION_FORMAT = dwarf; @@ -1587,19 +1700,22 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 8.0; - MACOSX_DEPLOYMENT_TARGET = 10.10; - MTL_ENABLE_DEBUG_INFO = YES; + INFOPLIST_FILE = Source/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 10.0; + MACOSX_DEPLOYMENT_TARGET = 10.12; ONLY_ACTIVE_ARCH = YES; - SDKROOT = iphoneos; + OTHER_LDFLAGS = ( + "-framework", + CFNetwork, + ); + PRODUCT_BUNDLE_IDENTIFIER = org.alamofire.Alamofire; + PRODUCT_NAME = Alamofire; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_SWIFT3_OBJC_INFERENCE = Off; - SWIFT_VERSION = 4.0; - TARGETED_DEVICE_FAMILY = "1,2"; - TVOS_DEPLOYMENT_TARGET = 9.0; + SWIFT_VERSION = 4.2; + TVOS_DEPLOYMENT_TARGET = 10.0; VERSIONING_SYSTEM = "apple-generic"; - VERSION_INFO_PREFIX = ""; - WATCHOS_DEPLOYMENT_TARGET = 2.0; + WATCHOS_DEPLOYMENT_TARGET = 3.0; }; name = Debug; }; @@ -1633,7 +1749,7 @@ CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; - CODE_SIGN_IDENTITY = "iPhone Developer"; + CODE_SIGN_STYLE = Manual; COPY_PHASE_STRIP = YES; CURRENT_PROJECT_VERSION = 1; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; @@ -1647,55 +1763,52 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 8.0; - MACOSX_DEPLOYMENT_TARGET = 10.10; - MTL_ENABLE_DEBUG_INFO = NO; - SDKROOT = iphoneos; + INFOPLIST_FILE = Source/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 10.0; + MACOSX_DEPLOYMENT_TARGET = 10.12; + OTHER_LDFLAGS = ( + "-framework", + CFNetwork, + ); + PRODUCT_BUNDLE_IDENTIFIER = org.alamofire.Alamofire; + PRODUCT_NAME = Alamofire; SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; SWIFT_SWIFT3_OBJC_INFERENCE = Off; - SWIFT_VERSION = 4.0; - TARGETED_DEVICE_FAMILY = "1,2"; - TVOS_DEPLOYMENT_TARGET = 9.0; + SWIFT_VERSION = 4.2; + TVOS_DEPLOYMENT_TARGET = 10.0; VALIDATE_PRODUCT = YES; VERSIONING_SYSTEM = "apple-generic"; - VERSION_INFO_PREFIX = ""; - WATCHOS_DEPLOYMENT_TARGET = 2.0; + WATCHOS_DEPLOYMENT_TARGET = 3.0; }; name = Release; }; F8111E4719A95C8B0040E7D1 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { - "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; DEFINES_MODULE = YES; DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; - INFOPLIST_FILE = Source/Info.plist; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; - PRODUCT_BUNDLE_IDENTIFIER = org.alamofire.Alamofire; - PRODUCT_NAME = Alamofire; SDKROOT = iphoneos; SKIP_INSTALL = YES; + TARGETED_DEVICE_FAMILY = "1,2"; }; name = Debug; }; F8111E4819A95C8B0040E7D1 /* Release */ = { isa = XCBuildConfiguration; buildSettings = { - "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; DEFINES_MODULE = YES; DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; - INFOPLIST_FILE = Source/Info.plist; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; - PRODUCT_BUNDLE_IDENTIFIER = org.alamofire.Alamofire; - PRODUCT_NAME = Alamofire; SDKROOT = iphoneos; SKIP_INSTALL = YES; + TARGETED_DEVICE_FAMILY = "1,2"; }; name = Release; }; @@ -1703,6 +1816,7 @@ isa = XCBuildConfiguration; buildSettings = { APPLICATION_EXTENSION_API_ONLY = NO; + CODE_SIGN_STYLE = Manual; INFOPLIST_FILE = Tests/Info.plist; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; PRODUCT_BUNDLE_IDENTIFIER = "org.alamofire.$(PRODUCT_NAME:rfc1034identifier)"; @@ -1715,6 +1829,7 @@ isa = XCBuildConfiguration; buildSettings = { APPLICATION_EXTENSION_API_ONLY = NO; + CODE_SIGN_STYLE = Manual; INFOPLIST_FILE = Tests/Info.plist; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; PRODUCT_BUNDLE_IDENTIFIER = "org.alamofire.$(PRODUCT_NAME:rfc1034identifier)"; @@ -1727,7 +1842,6 @@ isa = XCBuildConfiguration; buildSettings = { APPLICATION_EXTENSION_API_ONLY = NO; - CODE_SIGN_IDENTITY = ""; INFOPLIST_FILE = Tests/Info.plist; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/../Frameworks"; PRODUCT_BUNDLE_IDENTIFIER = "org.alamofire.$(PRODUCT_NAME:rfc1034identifier)"; @@ -1740,7 +1854,6 @@ isa = XCBuildConfiguration; buildSettings = { APPLICATION_EXTENSION_API_ONLY = NO; - CODE_SIGN_IDENTITY = ""; INFOPLIST_FILE = Tests/Info.plist; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/../Frameworks"; PRODUCT_BUNDLE_IDENTIFIER = "org.alamofire.$(PRODUCT_NAME:rfc1034identifier)"; diff --git a/Alamofire.xcodeproj/xcshareddata/xcschemes/Alamofire iOS.xcscheme b/Alamofire.xcodeproj/xcshareddata/xcschemes/Alamofire iOS.xcscheme index 38a78a131..60dc2b734 100644 --- a/Alamofire.xcodeproj/xcshareddata/xcschemes/Alamofire iOS.xcscheme +++ b/Alamofire.xcodeproj/xcshareddata/xcschemes/Alamofire iOS.xcscheme @@ -1,6 +1,6 @@ + + + + diff --git a/Alamofire.xcodeproj/xcshareddata/xcschemes/Alamofire macOS.xcscheme b/Alamofire.xcodeproj/xcshareddata/xcschemes/Alamofire macOS.xcscheme index 58a938b11..02304c9e7 100644 --- a/Alamofire.xcodeproj/xcshareddata/xcschemes/Alamofire macOS.xcscheme +++ b/Alamofire.xcodeproj/xcshareddata/xcschemes/Alamofire macOS.xcscheme @@ -1,6 +1,6 @@ + + + + diff --git a/Alamofire.xcodeproj/xcshareddata/xcschemes/Alamofire tvOS.xcscheme b/Alamofire.xcodeproj/xcshareddata/xcschemes/Alamofire tvOS.xcscheme index eaf810242..30efc763d 100644 --- a/Alamofire.xcodeproj/xcshareddata/xcschemes/Alamofire tvOS.xcscheme +++ b/Alamofire.xcodeproj/xcshareddata/xcschemes/Alamofire tvOS.xcscheme @@ -1,6 +1,6 @@ + + + + diff --git a/Alamofire.xcodeproj/xcshareddata/xcschemes/Alamofire watchOS.xcscheme b/Alamofire.xcodeproj/xcshareddata/xcschemes/Alamofire watchOS.xcscheme index e6b79c5dc..724af3dbb 100644 --- a/Alamofire.xcodeproj/xcshareddata/xcschemes/Alamofire watchOS.xcscheme +++ b/Alamofire.xcodeproj/xcshareddata/xcschemes/Alamofire watchOS.xcscheme @@ -1,6 +1,6 @@ Bool - { + didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool { let splitViewController = window!.rootViewController as! UISplitViewController let navigationController = splitViewController.viewControllers.last as! UINavigationController navigationController.topViewController!.navigationItem.leftBarButtonItem = splitViewController.displayModeButtonItem diff --git a/Example/Source/DetailViewController.swift b/Example/Source/DetailViewController.swift index 80432a50f..27f654c21 100644 --- a/Example/Source/DetailViewController.swift +++ b/Example/Source/DetailViewController.swift @@ -30,7 +30,7 @@ class DetailViewController: UITableViewController { case headers, body } - var request: Alamofire.Request? { + var request: Request? { didSet { oldValue?.cancel() diff --git a/Example/Source/MasterViewController.swift b/Example/Source/MasterViewController.swift index f9d686a64..44fca0afb 100644 --- a/Example/Source/MasterViewController.swift +++ b/Example/Source/MasterViewController.swift @@ -66,23 +66,23 @@ class MasterViewController: UITableViewController { switch segue.identifier! { case "GET": detailViewController.segueIdentifier = "GET" - return Alamofire.request("https://httpbin.org/get") + return AF.request("https://httpbin.org/get") case "POST": detailViewController.segueIdentifier = "POST" - return Alamofire.request("https://httpbin.org/post", method: .post) + return AF.request("https://httpbin.org/post", method: .post) case "PUT": detailViewController.segueIdentifier = "PUT" - return Alamofire.request("https://httpbin.org/put", method: .put) + return AF.request("https://httpbin.org/put", method: .put) case "DELETE": detailViewController.segueIdentifier = "DELETE" - return Alamofire.request("https://httpbin.org/delete", method: .delete) + return AF.request("https://httpbin.org/delete", method: .delete) case "DOWNLOAD": detailViewController.segueIdentifier = "DOWNLOAD" let destination = DownloadRequest.suggestedDownloadDestination( for: .cachesDirectory, in: .userDomainMask ) - return Alamofire.download("https://httpbin.org/stream/1", to: destination) + return AF.download("https://httpbin.org/stream/1", to: destination) default: return nil } diff --git a/Example/iOS Example.xcodeproj/project.pbxproj b/Example/iOS Example.xcodeproj/project.pbxproj index f2eeefa04..bde0c560c 100644 --- a/Example/iOS Example.xcodeproj/project.pbxproj +++ b/Example/iOS Example.xcodeproj/project.pbxproj @@ -3,7 +3,7 @@ archiveVersion = 1; classes = { }; - objectVersion = 46; + objectVersion = 51; objects = { /* Begin PBXBuildFile section */ @@ -205,7 +205,7 @@ isa = PBXProject; attributes = { LastSwiftUpdateCheck = 0720; - LastUpgradeCheck = 0930; + LastUpgradeCheck = 1000; ORGANIZATIONNAME = Alamofire; TargetAttributes = { F8111E0419A951050040E7D1 = { @@ -215,7 +215,7 @@ }; }; buildConfigurationList = F8111E0019A951050040E7D1 /* Build configuration list for PBXProject "iOS Example" */; - compatibilityVersion = "Xcode 3.2"; + compatibilityVersion = "Xcode 10.0"; developmentRegion = English; hasScannedForEncodings = 0; knownRegions = ( @@ -339,8 +339,6 @@ isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; - CLANG_CXX_LIBRARY = "libc++"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; @@ -382,17 +380,16 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 8.0; - MACOSX_DEPLOYMENT_TARGET = 10.11; - MTL_ENABLE_DEBUG_INFO = YES; + IPHONEOS_DEPLOYMENT_TARGET = 10.0; + MACOSX_DEPLOYMENT_TARGET = 10.12; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_SWIFT3_OBJC_INFERENCE = Off; - SWIFT_VERSION = 4.0; + SWIFT_VERSION = 4.2; TARGETED_DEVICE_FAMILY = "1,2"; - TVOS_DEPLOYMENT_TARGET = 9.0; - WATCHOS_DEPLOYMENT_TARGET = 2.0; + TVOS_DEPLOYMENT_TARGET = 10.0; + WATCHOS_DEPLOYMENT_TARGET = 3.0; }; name = Debug; }; @@ -400,8 +397,6 @@ isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; - CLANG_CXX_LIBRARY = "libc++"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; @@ -436,17 +431,16 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 8.0; - MACOSX_DEPLOYMENT_TARGET = 10.11; - MTL_ENABLE_DEBUG_INFO = NO; + IPHONEOS_DEPLOYMENT_TARGET = 10.0; + MACOSX_DEPLOYMENT_TARGET = 10.12; SDKROOT = iphoneos; - SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; + SWIFT_COMPILATION_MODE = wholemodule; SWIFT_SWIFT3_OBJC_INFERENCE = Off; - SWIFT_VERSION = 4.0; + SWIFT_VERSION = 4.2; TARGETED_DEVICE_FAMILY = "1,2"; - TVOS_DEPLOYMENT_TARGET = 9.0; + TVOS_DEPLOYMENT_TARGET = 10.0; VALIDATE_PRODUCT = YES; - WATCHOS_DEPLOYMENT_TARGET = 2.0; + WATCHOS_DEPLOYMENT_TARGET = 3.0; }; name = Release; }; @@ -456,9 +450,13 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_LAUNCHIMAGE_NAME = LaunchImage; INFOPLIST_FILE = Resources/Info.plist; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); PRODUCT_BUNDLE_IDENTIFIER = "org.alamofire.iOS-Example"; PRODUCT_NAME = "iOS Example"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; }; name = Debug; }; @@ -468,9 +466,13 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_LAUNCHIMAGE_NAME = LaunchImage; INFOPLIST_FILE = Resources/Info.plist; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); PRODUCT_BUNDLE_IDENTIFIER = "org.alamofire.iOS-Example"; PRODUCT_NAME = "iOS Example"; + SWIFT_OPTIMIZATION_LEVEL = "-O"; }; name = Release; }; diff --git a/Example/iOS Example.xcodeproj/xcshareddata/xcschemes/iOS Example.xcscheme b/Example/iOS Example.xcodeproj/xcshareddata/xcschemes/iOS Example.xcscheme index a4c607448..04c0c333c 100644 --- a/Example/iOS Example.xcodeproj/xcshareddata/xcschemes/iOS Example.xcscheme +++ b/Example/iOS Example.xcodeproj/xcshareddata/xcschemes/iOS Example.xcscheme @@ -1,6 +1,6 @@ 0.7) minitest (~> 5.1) thread_safe (~> 0.3, >= 0.3.4) diff --git a/README.md b/README.md index 27f7bf993..4702dcf01 100644 --- a/README.md +++ b/README.md @@ -6,9 +6,12 @@ [![Platform](https://img.shields.io/cocoapods/p/Alamofire.svg?style=flat)](https://alamofire.github.io/Alamofire) [![Twitter](https://img.shields.io/badge/twitter-@AlamofireSF-blue.svg?style=flat)](https://twitter.com/AlamofireSF) [![Gitter](https://badges.gitter.im/Alamofire/Alamofire.svg)](https://gitter.im/Alamofire/Alamofire?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge) +[![Open Source Helpers](https://www.codetriage.com/alamofire/alamofire/badges/users.svg)](https://www.codetriage.com/alamofire/alamofire) Alamofire is an HTTP networking library written in Swift. +**⚠️⚠️⚠️ WARNING ⚠️⚠️⚠️** This documentation is out of date during the Alamofire 5 beta process. + - [Features](#features) - [Component Libraries](#component-libraries) - [Requirements](#requirements) @@ -56,12 +59,14 @@ In order to keep Alamofire focused specifically on core networking implementatio ## Requirements -- iOS 8.0+ / macOS 10.10+ / tvOS 9.0+ / watchOS 2.0+ -- Xcode 8.3+ -- Swift 3.1+ +- iOS 10.0+ / macOS 10.12+ / tvOS 10.0+ / watchOS 3.0+ +- Xcode 10.1+ +- Swift 4.2+ + ## Migration Guides +- Alamofire 5.0 Migration Guide: To be written! - [Alamofire 4.0 Migration Guide](https://github.com/Alamofire/Alamofire/blob/master/Documentation/Alamofire%204.0%20Migration%20Guide.md) - [Alamofire 3.0 Migration Guide](https://github.com/Alamofire/Alamofire/blob/master/Documentation/Alamofire%203.0%20Migration%20Guide.md) - [Alamofire 2.0 Migration Guide](https://github.com/Alamofire/Alamofire/blob/master/Documentation/Alamofire%202.0%20Migration%20Guide.md) @@ -79,70 +84,31 @@ In order to keep Alamofire focused specifically on core networking implementatio ### CocoaPods -[CocoaPods](https://cocoapods.org) is a dependency manager for Cocoa projects. You can install it with the following command: - -```bash -$ gem install cocoapods -``` - -> CocoaPods 1.1+ is required to build Alamofire 4.0+. - -To integrate Alamofire into your Xcode project using CocoaPods, specify it in your `Podfile`: +[CocoaPods](https://cocoapods.org) is a dependency manager for Cocoa projects. For usage and installation instructions, visit their website. To integrate Alamofire into your Xcode project using CocoaPods, specify it in your `Podfile`: ```ruby -source 'https://github.com/CocoaPods/Specs.git' -platform :ios, '10.0' -use_frameworks! - -target '' do - pod 'Alamofire', '~> 4.7' -end -``` - -Then, run the following command: - -```bash -$ pod install +pod 'Alamofire', '~> 5.0.0.beta.1' ``` ### Carthage -[Carthage](https://github.com/Carthage/Carthage) is a decentralized dependency manager that builds your dependencies and provides you with binary frameworks. - -You can install Carthage with [Homebrew](https://brew.sh/) using the following command: - -```bash -$ brew update -$ brew install carthage -``` - -To integrate Alamofire into your Xcode project using Carthage, specify it in your `Cartfile`: +[Carthage](https://github.com/Carthage/Carthage) is a decentralized dependency manager that builds your dependencies and provides you with binary frameworks. To integrate Alamofire into your Xcode project using Carthage, specify it in your `Cartfile`: ```ogdl -github "Alamofire/Alamofire" ~> 4.7 +github "Alamofire/Alamofire" ~> 5.0.0.beta.1 ``` -Run `carthage update` to build the framework and drag the built `Alamofire.framework` into your Xcode project. - ### Swift Package Manager The [Swift Package Manager](https://swift.org/package-manager/) is a tool for automating the distribution of Swift code and is integrated into the `swift` compiler. It is in early development, but Alamofire does support its use on supported platforms. Once you have your Swift package set up, adding Alamofire as a dependency is as easy as adding it to the `dependencies` value of your `Package.swift`. -#### Swift 3 - -```swift -dependencies: [ - .Package(url: "https://github.com/Alamofire/Alamofire.git", majorVersion: 4) -] -``` - #### Swift 4 ```swift dependencies: [ - .package(url: "https://github.com/Alamofire/Alamofire.git", from: "4.0.0") + .package(url: "https://github.com/Alamofire/Alamofire.git", from: "5.0.0.beta.1") ] ``` @@ -191,14 +157,15 @@ The following radars have some effect on the current implementation of Alamofire - [`rdar://21349340`](http://www.openradar.me/radar?id=5517037090635776) - Compiler throwing warning due to toll-free bridging issue in test case - `rdar://26870455` - Background URL Session Configurations do not work in the simulator - `rdar://26849668` - Some URLProtocol APIs do not properly handle `URLRequest` -- [`rdar://36082113`](http://openradar.appspot.com/radar?id=4942308441063424) - `URLSessionTaskMetrics` failing to link on watchOS 3.0+ ## Resolved Radars The following radars have been resolved over time after being filed against the Alamofire project. -- [`rdar://26761490`](http://www.openradar.me/radar?id=5010235949318144) - Swift string interpolation causing memory leak with common usage (Resolved on 9/1/17 in Xcode 9 beta 6). - +- [`rdar://26761490`](http://www.openradar.me/radar?id=5010235949318144) - Swift string interpolation causing memory leak with common usage. + - (Resolved): 9/1/17 in Xcode 9 beta 6. +- [`rdar://36082113`](http://openradar.appspot.com/radar?id=4942308441063424) - `URLSessionTaskMetrics` failing to link on watchOS 3.0+ + - (Resolved): Just add `CFNetwork` to you linked frameworks. ## FAQ ### What's the origin of the name Alamofire? @@ -207,7 +174,7 @@ Alamofire is named after the [Alamo Fire flower](https://aggie-horticulture.tamu ### What logic belongs in a Router vs. a Request Adapter? -Simple, static data such as paths, parameters and common headers belong in the `Router`. Dynamic data such as an `Authorization` header whose value can changed based on an authentication system belongs in a `RequestAdapter`. +Simple, static data such as paths, HTTP methods, and common headers belong in the `Router`. Dynamic data such as an `Authorization` header whose value can changed based on an authentication system belongs in a `RequestAdapter`. The reason the dynamic data MUST be placed into the `RequestAdapter` is to support retry operations. When a `Request` is retried, the original request is not rebuilt meaning the `Router` will not be called again. The `RequestAdapter` is called again allowing the dynamic data to be updated on the original request before retrying the `Request`. diff --git a/Source/AFError.swift b/Source/AFError.swift index 8b90d8471..a9f79229d 100644 --- a/Source/AFError.swift +++ b/Source/AFError.swift @@ -27,23 +27,37 @@ import Foundation /// `AFError` is the error type returned by Alamofire. It encompasses a few different types of errors, each with /// their own associated reasons. /// +/// - explicitlyCancelled: Returned when a `Request` is explicitly cancelled. /// - invalidURL: Returned when a `URLConvertible` type fails to create a valid `URL`. /// - parameterEncodingFailed: Returned when a parameter encoding object throws an error during the encoding process. /// - multipartEncodingFailed: Returned when some step in the multipart encoding process fails. /// - responseValidationFailed: Returned when a `validate()` call fails. /// - responseSerializationFailed: Returned when a response serializer encounters an error in the serialization process. +/// - certificatePinningFailed: Returned when a response fails certificate pinning. public enum AFError: Error { /// The underlying reason the parameter encoding error occurred. /// /// - missingURL: The URL request did not have a URL to encode. /// - jsonEncodingFailed: JSON serialization failed with an underlying system error during the /// encoding process. - /// - propertyListEncodingFailed: Property list serialization failed with an underlying system error during - /// encoding process. public enum ParameterEncodingFailureReason { case missingURL case jsonEncodingFailed(error: Error) - case propertyListEncodingFailed(error: Error) + } + + /// Underlying reason the parameter encoder error occured. + public enum ParameterEncoderFailureReason { + /// Possible missing components. + public enum RequiredComponent { + /// The `URL` was missing or unable to be extracted from the passed `URLRequest` or during encoding. + case url + /// The `HTTPMethod` could not be extracted from the passed `URLRequest`. + case httpMethod(rawValue: String) + } + /// A `RequiredComponent` was missing during encoding. + case missingRequiredComponent(RequiredComponent) + /// The underlying encoder failed with the associated error. + case encoderFailed(error: Error) } /// The underlying reason the multipart encoding error occurred. @@ -107,44 +121,93 @@ public enum AFError: Error { } /// The underlying reason the response serialization error occurred. - /// - /// - inputDataNil: The server response contained no data. - /// - inputDataNilOrZeroLength: The server response contained no data or the data was zero length. - /// - inputFileNil: The file containing the server response did not exist. - /// - inputFileReadFailed: The file containing the server response could not be read. - /// - stringSerializationFailed: String serialization failed using the provided `String.Encoding`. - /// - jsonSerializationFailed: JSON serialization failed with an underlying system error. - /// - propertyListSerializationFailed: Property list serialization failed with an underlying system error. public enum ResponseSerializationFailureReason { - case inputDataNil + /// The server response contained no data or the data was zero length. case inputDataNilOrZeroLength + /// The file containing the server response did not exist. case inputFileNil + /// The file containing the server response could not be read from the associated `URL`. case inputFileReadFailed(at: URL) + /// String serialization failed using the provided `String.Encoding`. case stringSerializationFailed(encoding: String.Encoding) + /// JSON serialization failed with an underlying system error. case jsonSerializationFailed(error: Error) - case propertyListSerializationFailed(error: Error) + /// A `DataDecoder` failed to decode the response due to the associated `Error`. + case decodingFailed(error: Error) + /// Generic serialization failed for an empty response that wasn't type `Empty` but instead the associated type. + case invalidEmptyResponse(type: String) } + /// Underlying reason a server trust evaluation error occured. + public enum ServerTrustFailureReason { + /// The output of a server trust evaluation. + public struct Output { + /// The host for which the evaluation was performed. + public let host: String + /// The `SecTrust` value which was evaluated. + public let trust: SecTrust + /// The `OSStatus` of evaluation operation. + public let status: OSStatus + /// The result of the evaluation operation. + public let result: SecTrustResultType + + /// Creates an `Output` value from the provided values. + init(_ host: String, _ trust: SecTrust, _ status: OSStatus, _ result: SecTrustResultType) { + self.host = host + self.trust = trust + self.status = status + self.result = result + } + } + case noRequiredEvaluator(host: String) + /// No certificates were found with which to perform the trust evaluation. + case noCertificatesFound + /// No public keys were found with which to perform the trust evaluation. + case noPublicKeysFound + /// During evaluation, application of the associated `SecPolicy` failed. + case policyApplicationFailed(trust: SecTrust, policy: SecPolicy, status: OSStatus) + /// During evaluation, setting the associated anchor certificates failed. + case settingAnchorCertificatesFailed(status: OSStatus, certificates: [SecCertificate]) + /// During evaluation, creation of the revocation policy failed. + case revocationPolicyCreationFailed + /// Default evaluation failed with the associated `Output`. + case defaultEvaluationFailed(output: Output) + /// Host validation failed with the associated `Output`. + case hostValidationFailed(output: Output) + /// Revocation check failed with the associated `Output` and options. + case revocationCheckFailed(output: Output, options: RevocationTrustEvaluator.Options) + /// Certificate pinning failed. + case certificatePinningFailed(host: String, trust: SecTrust, pinnedCertificates: [SecCertificate], serverCertificates: [SecCertificate]) + /// Public key pinning failed. + case publicKeyPinningFailed(host: String, trust: SecTrust, pinnedKeys: [SecKey], serverKeys: [SecKey]) + } + + case explicitlyCancelled case invalidURL(url: URLConvertible) case parameterEncodingFailed(reason: ParameterEncodingFailureReason) + case parameterEncoderFailed(reason: ParameterEncoderFailureReason) case multipartEncodingFailed(reason: MultipartEncodingFailureReason) case responseValidationFailed(reason: ResponseValidationFailureReason) case responseSerializationFailed(reason: ResponseSerializationFailureReason) -} - -// MARK: - Adapt Error - -struct AdaptError: Error { - let error: Error + case serverTrustEvaluationFailed(reason: ServerTrustFailureReason) } extension Error { - var underlyingAdaptError: Error? { return (self as? AdaptError)?.error } + /// Returns the instance cast as an `AFError`. + public var asAFError: AFError? { + return self as? AFError + } } // MARK: - Error Booleans extension AFError { + /// Returns whether the `AFError` is an explicitly cancelled error. + public var isExplicitlyCancelledError: Bool { + if case .explicitlyCancelled = self { return true } + return false + } + /// Returns whether the AFError is an invalid URL error. public var isInvalidURLError: Bool { if case .invalidURL = self { return true } @@ -158,6 +221,12 @@ extension AFError { return false } + /// Returns whether the instance is a parameter encoder error. + public var isParameterEncoderError: Bool { + if case .parameterEncoderFailed = self { return true } + return false + } + /// Returns whether the AFError is a multipart encoding error. When `true`, the `url` and `underlyingError` properties /// will contain the associated values. public var isMultipartEncodingError: Bool { @@ -178,6 +247,12 @@ extension AFError { if case .responseSerializationFailed = self { return true } return false } + + /// Returns whether the `AFError` is a server trust evaluation error. + public var isServerTrustEvaluationError: Bool { + if case .serverTrustEvaluationFailed = self { return true } + return false + } } // MARK: - Convenience Properties @@ -204,11 +279,13 @@ extension AFError { } /// The `Error` returned by a system framework associated with a `.parameterEncodingFailed`, - /// `.multipartEncodingFailed` or `.responseSerializationFailed` error. + /// `.parameterEncoderFailed`, `.multipartEncodingFailed` or `.responseSerializationFailed` error. public var underlyingError: Error? { switch self { case .parameterEncodingFailed(let reason): return reason.underlyingError + case .parameterEncoderFailed(let reason): + return reason.underlyingError case .multipartEncodingFailed(let reason): return reason.underlyingError case .responseSerializationFailed(let reason): @@ -262,7 +339,18 @@ extension AFError { extension AFError.ParameterEncodingFailureReason { var underlyingError: Error? { switch self { - case .jsonEncodingFailed(let error), .propertyListEncodingFailed(let error): + case .jsonEncodingFailed(let error): + return error + default: + return nil + } + } +} + +extension AFError.ParameterEncoderFailureReason { + var underlyingError: Error? { + switch self { + case .encoderFailed(let error): return error default: return nil @@ -336,7 +424,7 @@ extension AFError.ResponseSerializationFailureReason { var underlyingError: Error? { switch self { - case .jsonSerializationFailed(let error), .propertyListSerializationFailed(let error): + case .jsonSerializationFailed(let error): return error default: return nil @@ -344,21 +432,39 @@ extension AFError.ResponseSerializationFailureReason { } } +extension AFError.ServerTrustFailureReason { + var output: AFError.ServerTrustFailureReason.Output? { + switch self { + case let .defaultEvaluationFailed(output), + let .hostValidationFailed(output), + let .revocationCheckFailed(output, _): + return output + default: return nil + } + } +} + // MARK: - Error Descriptions extension AFError: LocalizedError { public var errorDescription: String? { switch self { + case .explicitlyCancelled: + return "Request explicitly cancelled." case .invalidURL(let url): return "URL is not valid: \(url)" case .parameterEncodingFailed(let reason): return reason.localizedDescription + case .parameterEncoderFailed(let reason): + return reason.localizedDescription case .multipartEncodingFailed(let reason): return reason.localizedDescription case .responseValidationFailed(let reason): return reason.localizedDescription case .responseSerializationFailed(let reason): return reason.localizedDescription + case .serverTrustEvaluationFailed: + return "Server trust evaluation failed." } } } @@ -370,8 +476,17 @@ extension AFError.ParameterEncodingFailureReason { return "URL request to encode was missing a URL" case .jsonEncodingFailed(let error): return "JSON could not be encoded because of error:\n\(error.localizedDescription)" - case .propertyListEncodingFailed(let error): - return "PropertyList could not be encoded because of error:\n\(error.localizedDescription)" + } + } +} + +extension AFError.ParameterEncoderFailureReason { + var localizedDescription: String { + switch self { + case .missingRequiredComponent(let component): + return "Encoding failed due to a missing request component: \(component)" + case .encoderFailed(let error): + return "The underlying encoder failed with the error: \(error)" } } } @@ -418,8 +533,6 @@ extension AFError.MultipartEncodingFailureReason { extension AFError.ResponseSerializationFailureReason { var localizedDescription: String { switch self { - case .inputDataNil: - return "Response could not be serialized, input data was nil." case .inputDataNilOrZeroLength: return "Response could not be serialized, input data was nil or zero length." case .inputFileNil: @@ -430,8 +543,10 @@ extension AFError.ResponseSerializationFailureReason { return "String could not be serialized with encoding: \(encoding)." case .jsonSerializationFailed(let error): return "JSON could not be serialized because of error:\n\(error.localizedDescription)" - case .propertyListSerializationFailed(let error): - return "PropertyList could not be serialized because of error:\n\(error.localizedDescription)" + case .invalidEmptyResponse(let type): + return "Empty response could not be serialized to type: \(type). Use Empty as the expected type for such responses." + case .decodingFailed(let error): + return "Response could not be decoded because of error:\n\(error.localizedDescription)" } } } @@ -458,3 +573,32 @@ extension AFError.ResponseValidationFailureReason { } } } + +extension AFError.ServerTrustFailureReason { + var localizedDescription: String { + switch self { + case let .noRequiredEvaluator(host): + return "A ServerTrustEvaluating value is required for host \(host) but none was found." + case .noCertificatesFound: + return "No certificates were found or provided for evaluation." + case .noPublicKeysFound: + return "No public keys were found or provided for evaluation." + case .policyApplicationFailed: + return "Attempting to set a SecPolicy failed." + case .settingAnchorCertificatesFailed: + return "Attempting to set the provided certificates as anchor certificates failed." + case .revocationPolicyCreationFailed: + return "Attempting to create a revocation policy failed." + case let .defaultEvaluationFailed(output): + return "Default evaluation failed for host \(output.host)." + case let .hostValidationFailed(output): + return "Host validation failed for host \(output.host)." + case let .revocationCheckFailed(output, _): + return "Revocation check failed for host \(output.host)." + case let .certificatePinningFailed(host, _, _, _): + return "Certificate pinning failed for host \(host)." + case let .publicKeyPinningFailed(host, _, _, _): + return "Public key pinning failed for host \(host)." + } + } +} diff --git a/Source/Alamofire.swift b/Source/Alamofire.swift index 2fcc05ca0..9aeead92b 100644 --- a/Source/Alamofire.swift +++ b/Source/Alamofire.swift @@ -24,442 +24,316 @@ import Foundation -/// Types adopting the `URLConvertible` protocol can be used to construct URLs, which are then used to construct -/// URL requests. -public protocol URLConvertible { - /// Returns a URL that conforms to RFC 2396 or throws an `Error`. - /// - /// - throws: An `Error` if the type cannot be converted to a `URL`. +/// Global namespace containing API for the `default` `Session` instance. +public enum AF { + // MARK: - Data Request + + /// Creates a `DataRequest` using `SessionManager.default` to retrive the contents of the specified `url` + /// using the `method`, `parameters`, `encoding`, and `headers` provided. /// - /// - returns: A URL or throws an `Error`. - func asURL() throws -> URL -} + /// - Parameters: + /// - url: The `URLConvertible` value. + /// - method: The `HTTPMethod`, `.get` by default. + /// - parameters: The `Parameters`, `nil` by default. + /// - encoding: The `ParameterEncoding`, `URLEncoding.default` by default. + /// - headers: The `HTTPHeaders`, `nil` by default. + /// - Returns: The created `DataRequest`. + public static func request(_ url: URLConvertible, + method: HTTPMethod = .get, + parameters: Parameters? = nil, + encoding: ParameterEncoding = URLEncoding.default, + headers: HTTPHeaders? = nil) -> DataRequest { + return Session.default.request(url, + method: method, + parameters: parameters, + encoding: encoding, + headers: headers) + } + -extension String: URLConvertible { - /// Returns a URL if `self` represents a valid URL string that conforms to RFC 2396 or throws an `AFError`. + /// Creates a `DataRequest` using `SessionManager.default` to retrive the contents of the specified `url` + /// using the `method`, `parameters`, `encoding`, and `headers` provided. /// - /// - throws: An `AFError.invalidURL` if `self` is not a valid URL string. + /// - Parameters: + /// - url: The `URLConvertible` value. + /// - method: The `HTTPMethod`, `.get` by default. + /// - parameters: The `Encodable` parameters, `nil` by default. + /// - encoding: The `ParameterEncoding`, `URLEncodedFormParameterEncoder.default` by default. + /// - headers: The `HTTPHeaders`, `nil` by default. + /// - Returns: The created `DataRequest`. + public static func request(_ url: URLConvertible, + method: HTTPMethod = .get, + parameters: Parameters? = nil, + encoder: ParameterEncoder = URLEncodedFormParameterEncoder.default, + headers: HTTPHeaders? = nil) -> DataRequest { + return Session.default.request(url, + method: method, + parameters: parameters, + encoder: encoder, + headers: headers) + } + + /// Creates a `DataRequest` using `SessionManager.default` to execute the specified `urlRequest`. /// - /// - returns: A URL or throws an `AFError`. - public func asURL() throws -> URL { - guard let url = URL(string: self) else { throw AFError.invalidURL(url: self) } - return url + /// - Parameter urlRequest: The `URLRequestConvertible` value. + /// - Returns: The created `DataRequest`. + public static func request(_ urlRequest: URLRequestConvertible) -> DataRequest { + return Session.default.request(urlRequest) } -} -extension URL: URLConvertible { - /// Returns self. - public func asURL() throws -> URL { return self } -} + // MARK: - Download Request -extension URLComponents: URLConvertible { - /// Returns a URL if `url` is not nil, otherwise throws an `Error`. + /// Creates a `DownloadRequest` using `SessionManager.default` to download the contents of the specified `url` to + /// the provided `destination` using the `method`, `parameters`, `encoding`, and `headers` provided. /// - /// - throws: An `AFError.invalidURL` if `url` is `nil`. + /// If `destination` is not specified, the download will remain at the temporary location determined by the + /// underlying `URLSession`. /// - /// - returns: A URL or throws an `AFError`. - public func asURL() throws -> URL { - guard let url = url else { throw AFError.invalidURL(url: self) } - return url + /// - Parameters: + /// - url: The `URLConvertible` value. + /// - method: The `HTTPMethod`, `.get` by default. + /// - parameters: The `Parameters`, `nil` by default. + /// - encoding: The `ParameterEncoding`, `URLEncoding.default` by default. + /// - headers: The `HTTPHeaders`, `nil` by default. + /// - destination: The `DownloadRequest.Destination` closure used the determine the destination of the downloaded + /// file. `nil` by default. + /// - Returns: The created `DownloadRequest`. + public static func download(_ url: URLConvertible, + method: HTTPMethod = .get, + parameters: Parameters? = nil, + encoding: ParameterEncoding = URLEncoding.default, + headers: HTTPHeaders? = nil, + to destination: DownloadRequest.Destination? = nil) -> DownloadRequest { + return Session.default.download(url, + method: method, + parameters: parameters, + encoding: encoding, + headers: headers, + to: destination) } -} - -// MARK: - -/// Types adopting the `URLRequestConvertible` protocol can be used to construct URL requests. -public protocol URLRequestConvertible { - /// Returns a URL request or throws if an `Error` was encountered. + /// Creates a `DownloadRequest` using `SessionManager.default` to download the contents of the specified `url` to + /// the provided `destination` using the `method`, encodable `parameters`, `encoder`, and `headers` provided. /// - /// - throws: An `Error` if the underlying `URLRequest` is `nil`. + /// If `destination` is not specified, the download will remain at the temporary location determined by the + /// underlying `URLSession`. /// - /// - returns: A URL request. - func asURLRequest() throws -> URLRequest -} + /// - Parameters: + /// - url: The `URLConvertible` value. + /// - method: The `HTTPMethod`, `.get` by default. + /// - parameters: The `Encodable` parameters, `nil` by default. + /// - encoder: The `ParameterEncoder`, `URLEncodedFormParameterEncoder.default` by default. + /// - headers: The `HTTPHeaders`, `nil` by default. + /// - destination: The `DownloadRequest.Destination` closure used the determine the destination of the downloaded + /// file. `nil` by default. + /// - Returns: The created `DownloadRequest`. + public static func download(_ url: URLConvertible, + method: HTTPMethod = .get, + parameters: Parameters? = nil, + encoder: ParameterEncoder = URLEncodedFormParameterEncoder.default, + headers: HTTPHeaders? = nil, + to destination: DownloadRequest.Destination? = nil) -> DownloadRequest { + return Session.default.download(url, + method: method, + parameters: parameters, + encoder: encoder, + headers: headers, + to: destination) + } -extension URLRequestConvertible { - /// The URL request. - public var urlRequest: URLRequest? { return try? asURLRequest() } -} + // MARK: URLRequest -extension URLRequest: URLRequestConvertible { - /// Returns a URL request or throws if an `Error` was encountered. - public func asURLRequest() throws -> URLRequest { return self } -} + /// Creates a `DownloadRequest` using `SessionManager.default` to execute the specified `urlRequest` and download + /// the result to the provided `destination`. + /// + /// - Parameters: + /// - urlRequest: The `URLRequestConvertible` value. + /// - destination: The `DownloadRequest.Destination` closure used the determine the destination of the downloaded + /// file. `nil` by default. + /// - Returns: The created `DownloadRequest`. + public static func download(_ urlRequest: URLRequestConvertible, + to destination: DownloadRequest.Destination? = nil) -> DownloadRequest { + return Session.default.download(urlRequest, to: destination) + } -// MARK: - + // MARK: Resume Data -extension URLRequest { - /// Creates an instance with the specified `method`, `urlString` and `headers`. + /// Creates a `DownloadRequest` using the `SessionManager.default` from the `resumeData` produced from a previous + /// `DownloadRequest` cancellation to retrieve the contents of the original request and save them to the `destination`. + /// + /// If `destination` is not specified, the contents will remain in the temporary location determined by the + /// underlying URL session. /// - /// - parameter url: The URL. - /// - parameter method: The HTTP method. - /// - parameter headers: The HTTP headers. `nil` by default. + /// On some versions of all Apple platforms (iOS 10 - 10.2, macOS 10.12 - 10.12.2, tvOS 10 - 10.1, watchOS 3 - 3.1.1), + /// `resumeData` is broken on background URL session configurations. There's an underlying bug in the `resumeData` + /// generation logic where the data is written incorrectly and will always fail to resume the download. For more + /// information about the bug and possible workarounds, please refer to the [this Stack Overflow post](http://stackoverflow.com/a/39347461/1342462). /// - /// - returns: The new `URLRequest` instance. - public init(url: URLConvertible, method: HTTPMethod, headers: HTTPHeaders? = nil) throws { - let url = try url.asURL() + /// - Parameters: + /// - resumeData: The resume `Data`. This is an opaque blob produced by `URLSessionDownloadTask` when a task is + /// cancelled. See [Apple's documentation](https://developer.apple.com/documentation/foundation/urlsessiondownloadtask/1411634-cancel) + /// for more information. + /// - destination: The `DownloadRequest.Destination` closure used to determine the destination of the downloaded + /// file. `nil` by default. + /// - Returns: The created `DownloadRequest`. + public static func download(resumingWith resumeData: Data, + to destination: DownloadRequest.Destination? = nil) -> DownloadRequest { + return Session.default.download(resumingWith: resumeData, to: destination) + } - self.init(url: url) + // MARK: - Upload Request - httpMethod = method.rawValue + // MARK: File - if let headers = headers { - for (headerField, headerValue) in headers { - setValue(headerValue, forHTTPHeaderField: headerField) - } - } + /// Creates an `UploadRequest` using `SessionManager.default` to upload the contents of the `fileURL` specified + /// using the `url`, `method` and `headers` provided. + /// + /// - Parameters: + /// - fileURL: The `URL` of the file to upload. + /// - url: The `URLConvertible` value. + /// - method: The `HTTPMethod`, `.post` by default. + /// - headers: The `HTTPHeaders`, `nil` by default. + /// - Returns: The created `UploadRequest`. + public static func upload(_ fileURL: URL, + to url: URLConvertible, + method: HTTPMethod = .post, + headers: HTTPHeaders? = nil) -> UploadRequest { + return Session.default.upload(fileURL, to: url, method: method, headers: headers) } - func adapt(using adapter: RequestAdapter?) throws -> URLRequest { - guard let adapter = adapter else { return self } - return try adapter.adapt(self) + /// Creates an `UploadRequest` using the `SessionManager.default` to upload the contents of the `fileURL` specificed + /// using the `urlRequest` provided. + /// + /// - Parameters: + /// - fileURL: The `URL` of the file to upload. + /// - urlRequest: The `URLRequestConvertible` value. + /// - Returns: The created `UploadRequest`. + public static func upload(_ fileURL: URL, with urlRequest: URLRequestConvertible) -> UploadRequest { + return Session.default.upload(fileURL, with: urlRequest) } -} - -// MARK: - Data Request - -/// Creates a `DataRequest` using the default `SessionManager` to retrieve the contents of the specified `url`, -/// `method`, `parameters`, `encoding` and `headers`. -/// -/// - parameter url: The URL. -/// - parameter method: The HTTP method. `.get` by default. -/// - parameter parameters: The parameters. `nil` by default. -/// - parameter encoding: The parameter encoding. `URLEncoding.default` by default. -/// - parameter headers: The HTTP headers. `nil` by default. -/// -/// - returns: The created `DataRequest`. -@discardableResult -public func request( - _ url: URLConvertible, - method: HTTPMethod = .get, - parameters: Parameters? = nil, - encoding: ParameterEncoding = URLEncoding.default, - headers: HTTPHeaders? = nil) - -> DataRequest -{ - return SessionManager.default.request( - url, - method: method, - parameters: parameters, - encoding: encoding, - headers: headers - ) -} - -/// Creates a `DataRequest` using the default `SessionManager` to retrieve the contents of a URL based on the -/// specified `urlRequest`. -/// -/// - parameter urlRequest: The URL request -/// -/// - returns: The created `DataRequest`. -@discardableResult -public func request(_ urlRequest: URLRequestConvertible) -> DataRequest { - return SessionManager.default.request(urlRequest) -} - -// MARK: - Download Request - -// MARK: URL Request - -/// Creates a `DownloadRequest` using the default `SessionManager` to retrieve the contents of the specified `url`, -/// `method`, `parameters`, `encoding`, `headers` and save them to the `destination`. -/// -/// If `destination` is not specified, the contents will remain in the temporary location determined by the -/// underlying URL session. -/// -/// - parameter url: The URL. -/// - parameter method: The HTTP method. `.get` by default. -/// - parameter parameters: The parameters. `nil` by default. -/// - parameter encoding: The parameter encoding. `URLEncoding.default` by default. -/// - parameter headers: The HTTP headers. `nil` by default. -/// - parameter destination: The closure used to determine the destination of the downloaded file. `nil` by default. -/// -/// - returns: The created `DownloadRequest`. -@discardableResult -public func download( - _ url: URLConvertible, - method: HTTPMethod = .get, - parameters: Parameters? = nil, - encoding: ParameterEncoding = URLEncoding.default, - headers: HTTPHeaders? = nil, - to destination: DownloadRequest.DownloadFileDestination? = nil) - -> DownloadRequest -{ - return SessionManager.default.download( - url, - method: method, - parameters: parameters, - encoding: encoding, - headers: headers, - to: destination - ) -} - -/// Creates a `DownloadRequest` using the default `SessionManager` to retrieve the contents of a URL based on the -/// specified `urlRequest` and save them to the `destination`. -/// -/// If `destination` is not specified, the contents will remain in the temporary location determined by the -/// underlying URL session. -/// -/// - parameter urlRequest: The URL request. -/// - parameter destination: The closure used to determine the destination of the downloaded file. `nil` by default. -/// -/// - returns: The created `DownloadRequest`. -@discardableResult -public func download( - _ urlRequest: URLRequestConvertible, - to destination: DownloadRequest.DownloadFileDestination? = nil) - -> DownloadRequest -{ - return SessionManager.default.download(urlRequest, to: destination) -} - -// MARK: Resume Data - -/// Creates a `DownloadRequest` using the default `SessionManager` from the `resumeData` produced from a -/// previous request cancellation to retrieve the contents of the original request and save them to the `destination`. -/// -/// If `destination` is not specified, the contents will remain in the temporary location determined by the -/// underlying URL session. -/// -/// On the latest release of all the Apple platforms (iOS 10, macOS 10.12, tvOS 10, watchOS 3), `resumeData` is broken -/// on background URL session configurations. There's an underlying bug in the `resumeData` generation logic where the -/// data is written incorrectly and will always fail to resume the download. For more information about the bug and -/// possible workarounds, please refer to the following Stack Overflow post: -/// -/// - http://stackoverflow.com/a/39347461/1342462 -/// -/// - parameter resumeData: The resume data. This is an opaque data blob produced by `URLSessionDownloadTask` -/// when a task is cancelled. See `URLSession -downloadTask(withResumeData:)` for additional -/// information. -/// - parameter destination: The closure used to determine the destination of the downloaded file. `nil` by default. -/// -/// - returns: The created `DownloadRequest`. -@discardableResult -public func download( - resumingWith resumeData: Data, - to destination: DownloadRequest.DownloadFileDestination? = nil) - -> DownloadRequest -{ - return SessionManager.default.download(resumingWith: resumeData, to: destination) -} -// MARK: - Upload Request + // MARK: Data -// MARK: File - -/// Creates an `UploadRequest` using the default `SessionManager` from the specified `url`, `method` and `headers` -/// for uploading the `file`. -/// -/// - parameter file: The file to upload. -/// - parameter url: The URL. -/// - parameter method: The HTTP method. `.post` by default. -/// - parameter headers: The HTTP headers. `nil` by default. -/// -/// - returns: The created `UploadRequest`. -@discardableResult -public func upload( - _ fileURL: URL, - to url: URLConvertible, - method: HTTPMethod = .post, - headers: HTTPHeaders? = nil) - -> UploadRequest -{ - return SessionManager.default.upload(fileURL, to: url, method: method, headers: headers) -} - -/// Creates a `UploadRequest` using the default `SessionManager` from the specified `urlRequest` for -/// uploading the `file`. -/// -/// - parameter file: The file to upload. -/// - parameter urlRequest: The URL request. -/// -/// - returns: The created `UploadRequest`. -@discardableResult -public func upload(_ fileURL: URL, with urlRequest: URLRequestConvertible) -> UploadRequest { - return SessionManager.default.upload(fileURL, with: urlRequest) -} - -// MARK: Data - -/// Creates an `UploadRequest` using the default `SessionManager` from the specified `url`, `method` and `headers` -/// for uploading the `data`. -/// -/// - parameter data: The data to upload. -/// - parameter url: The URL. -/// - parameter method: The HTTP method. `.post` by default. -/// - parameter headers: The HTTP headers. `nil` by default. -/// -/// - returns: The created `UploadRequest`. -@discardableResult -public func upload( - _ data: Data, - to url: URLConvertible, - method: HTTPMethod = .post, - headers: HTTPHeaders? = nil) - -> UploadRequest -{ - return SessionManager.default.upload(data, to: url, method: method, headers: headers) -} - -/// Creates an `UploadRequest` using the default `SessionManager` from the specified `urlRequest` for -/// uploading the `data`. -/// -/// - parameter data: The data to upload. -/// - parameter urlRequest: The URL request. -/// -/// - returns: The created `UploadRequest`. -@discardableResult -public func upload(_ data: Data, with urlRequest: URLRequestConvertible) -> UploadRequest { - return SessionManager.default.upload(data, with: urlRequest) -} - -// MARK: InputStream - -/// Creates an `UploadRequest` using the default `SessionManager` from the specified `url`, `method` and `headers` -/// for uploading the `stream`. -/// -/// - parameter stream: The stream to upload. -/// - parameter url: The URL. -/// - parameter method: The HTTP method. `.post` by default. -/// - parameter headers: The HTTP headers. `nil` by default. -/// -/// - returns: The created `UploadRequest`. -@discardableResult -public func upload( - _ stream: InputStream, - to url: URLConvertible, - method: HTTPMethod = .post, - headers: HTTPHeaders? = nil) - -> UploadRequest -{ - return SessionManager.default.upload(stream, to: url, method: method, headers: headers) -} - -/// Creates an `UploadRequest` using the default `SessionManager` from the specified `urlRequest` for -/// uploading the `stream`. -/// -/// - parameter urlRequest: The URL request. -/// - parameter stream: The stream to upload. -/// -/// - returns: The created `UploadRequest`. -@discardableResult -public func upload(_ stream: InputStream, with urlRequest: URLRequestConvertible) -> UploadRequest { - return SessionManager.default.upload(stream, with: urlRequest) -} - -// MARK: MultipartFormData - -/// Encodes `multipartFormData` using `encodingMemoryThreshold` with the default `SessionManager` and calls -/// `encodingCompletion` with new `UploadRequest` using the `url`, `method` and `headers`. -/// -/// It is important to understand the memory implications of uploading `MultipartFormData`. If the cummulative -/// payload is small, encoding the data in-memory and directly uploading to a server is the by far the most -/// efficient approach. However, if the payload is too large, encoding the data in-memory could cause your app to -/// be terminated. Larger payloads must first be written to disk using input and output streams to keep the memory -/// footprint low, then the data can be uploaded as a stream from the resulting file. Streaming from disk MUST be -/// used for larger payloads such as video content. -/// -/// The `encodingMemoryThreshold` parameter allows Alamofire to automatically determine whether to encode in-memory -/// or stream from disk. If the content length of the `MultipartFormData` is below the `encodingMemoryThreshold`, -/// encoding takes place in-memory. If the content length exceeds the threshold, the data is streamed to disk -/// during the encoding process. Then the result is uploaded as data or as a stream depending on which encoding -/// technique was used. -/// -/// - parameter multipartFormData: The closure used to append body parts to the `MultipartFormData`. -/// - parameter encodingMemoryThreshold: The encoding memory threshold in bytes. -/// `multipartFormDataEncodingMemoryThreshold` by default. -/// - parameter url: The URL. -/// - parameter method: The HTTP method. `.post` by default. -/// - parameter headers: The HTTP headers. `nil` by default. -/// - parameter encodingCompletion: The closure called when the `MultipartFormData` encoding is complete. -public func upload( - multipartFormData: @escaping (MultipartFormData) -> Void, - usingThreshold encodingMemoryThreshold: UInt64 = SessionManager.multipartFormDataEncodingMemoryThreshold, - to url: URLConvertible, - method: HTTPMethod = .post, - headers: HTTPHeaders? = nil, - encodingCompletion: ((SessionManager.MultipartFormDataEncodingResult) -> Void)?) -{ - return SessionManager.default.upload( - multipartFormData: multipartFormData, - usingThreshold: encodingMemoryThreshold, - to: url, - method: method, - headers: headers, - encodingCompletion: encodingCompletion - ) -} + /// Creates an `UploadRequest` using `SessionManager.default` to upload the contents of the `data` specified using + /// the `url`, `method` and `headers` provided. + /// + /// - Parameters: + /// - data: The `Data` to upload. + /// - url: The `URLConvertible` value. + /// - method: The `HTTPMethod`, `.post` by default. + /// - headers: The `HTTPHeaders`, `nil` by default. + /// - Returns: The created `UploadRequest`. + public static func upload(_ data: Data, + to url: URLConvertible, + method: HTTPMethod = .post, + headers: HTTPHeaders? = nil) -> UploadRequest { + return Session.default.upload(data, to: url, method: method, headers: headers) + } -/// Encodes `multipartFormData` using `encodingMemoryThreshold` and the default `SessionManager` and -/// calls `encodingCompletion` with new `UploadRequest` using the `urlRequest`. -/// -/// It is important to understand the memory implications of uploading `MultipartFormData`. If the cummulative -/// payload is small, encoding the data in-memory and directly uploading to a server is the by far the most -/// efficient approach. However, if the payload is too large, encoding the data in-memory could cause your app to -/// be terminated. Larger payloads must first be written to disk using input and output streams to keep the memory -/// footprint low, then the data can be uploaded as a stream from the resulting file. Streaming from disk MUST be -/// used for larger payloads such as video content. -/// -/// The `encodingMemoryThreshold` parameter allows Alamofire to automatically determine whether to encode in-memory -/// or stream from disk. If the content length of the `MultipartFormData` is below the `encodingMemoryThreshold`, -/// encoding takes place in-memory. If the content length exceeds the threshold, the data is streamed to disk -/// during the encoding process. Then the result is uploaded as data or as a stream depending on which encoding -/// technique was used. -/// -/// - parameter multipartFormData: The closure used to append body parts to the `MultipartFormData`. -/// - parameter encodingMemoryThreshold: The encoding memory threshold in bytes. -/// `multipartFormDataEncodingMemoryThreshold` by default. -/// - parameter urlRequest: The URL request. -/// - parameter encodingCompletion: The closure called when the `MultipartFormData` encoding is complete. -public func upload( - multipartFormData: @escaping (MultipartFormData) -> Void, - usingThreshold encodingMemoryThreshold: UInt64 = SessionManager.multipartFormDataEncodingMemoryThreshold, - with urlRequest: URLRequestConvertible, - encodingCompletion: ((SessionManager.MultipartFormDataEncodingResult) -> Void)?) -{ - return SessionManager.default.upload( - multipartFormData: multipartFormData, - usingThreshold: encodingMemoryThreshold, - with: urlRequest, - encodingCompletion: encodingCompletion - ) -} + /// Creates an `UploadRequest` using `SessionManager.default` to upload the contents of the `data` specified using + /// the `urlRequest` provided. + /// + /// - Parameters: + /// - data: The `Data` to upload. + /// - urlRequest: The `URLRequestConvertible` value. + /// - Returns: The created `UploadRequest`. + public static func upload(_ data: Data, with urlRequest: URLRequestConvertible) -> UploadRequest { + return Session.default.upload(data, with: urlRequest) + } -#if !os(watchOS) + // MARK: InputStream -// MARK: - Stream Request + /// Creates an `UploadRequest` using `SessionManager.default` to upload the content provided by the `stream` + /// specified using the `url`, `method` and `headers` provided. + /// + /// - Parameters: + /// - stream: The `InputStream` to upload. + /// - url: The `URLConvertible` value. + /// - method: The `HTTPMethod`, `.post` by default. + /// - headers: The `HTTPHeaders`, `nil` by default. + /// - Returns: The created `UploadRequest`. + public static func upload(_ stream: InputStream, + to url: URLConvertible, + method: HTTPMethod = .post, + headers: HTTPHeaders? = nil) -> UploadRequest { + return Session.default.upload(stream, to: url, method: method, headers: headers) + } -// MARK: Hostname and Port + /// Creates an `UploadRequest` using `SessionManager.default` to upload the content provided by the `stream` + /// specified using the `urlRequest` specified. + /// + /// - Parameters: + /// - stream: The `InputStream` to upload. + /// - urlRequest: The `URLRequestConvertible` value. + /// - Returns: The created `UploadRequest`. + public static func upload(_ stream: InputStream, with urlRequest: URLRequestConvertible) -> UploadRequest { + return Session.default.upload(stream, with: urlRequest) + } -/// Creates a `StreamRequest` using the default `SessionManager` for bidirectional streaming with the `hostname` -/// and `port`. -/// -/// If `startRequestsImmediately` is `true`, the request will have `resume()` called before being returned. -/// -/// - parameter hostName: The hostname of the server to connect to. -/// - parameter port: The port of the server to connect to. -/// -/// - returns: The created `StreamRequest`. -@discardableResult -@available(iOS 9.0, macOS 10.11, tvOS 9.0, *) -public func stream(withHostName hostName: String, port: Int) -> StreamRequest { - return SessionManager.default.stream(withHostName: hostName, port: port) -} + // MARK: MultipartFormData -// MARK: NetService + /// Encodes `multipartFormData` using `encodingMemoryThreshold` and uploads the result using `SessionManager.default` + /// with the `url`, `method`, and `headers` provided. + /// + /// It is important to understand the memory implications of uploading `MultipartFormData`. If the cummulative + /// payload is small, encoding the data in-memory and directly uploading to a server is the by far the most + /// efficient approach. However, if the payload is too large, encoding the data in-memory could cause your app to + /// be terminated. Larger payloads must first be written to disk using input and output streams to keep the memory + /// footprint low, then the data can be uploaded as a stream from the resulting file. Streaming from disk MUST be + /// used for larger payloads such as video content. + /// + /// The `encodingMemoryThreshold` parameter allows Alamofire to automatically determine whether to encode in-memory + /// or stream from disk. If the content length of the `MultipartFormData` is below the `encodingMemoryThreshold`, + /// encoding takes place in-memory. If the content length exceeds the threshold, the data is streamed to disk + /// during the encoding process. Then the result is uploaded as data or as a stream depending on which encoding + /// technique was used. + /// + /// - Parameters: + /// - multipartFormData: The closure used to append body parts to the `MultipartFormData`. + /// - encodingMemoryThreshold: The encoding memory threshold in bytes. `10_000_000` bytes by default. + /// - url: The `URLConvertible` value. + /// - method: The `HTTPMethod`, `.post` by default. + /// - headers: The `HTTPHeaders`, `nil` by default. + /// - Returns: The created `UploadRequest`. + public static func upload(multipartFormData: @escaping (MultipartFormData) -> Void, + usingThreshold encodingMemoryThreshold: UInt64 = MultipartUpload.encodingMemoryThreshold, + to url: URLConvertible, + method: HTTPMethod = .post, + headers: HTTPHeaders? = nil) -> UploadRequest { + return Session.default.upload(multipartFormData: multipartFormData, + usingThreshold: encodingMemoryThreshold, + to: url, + method: method, + headers: headers) + } -/// Creates a `StreamRequest` using the default `SessionManager` for bidirectional streaming with the `netService`. -/// -/// If `startRequestsImmediately` is `true`, the request will have `resume()` called before being returned. -/// -/// - parameter netService: The net service used to identify the endpoint. -/// -/// - returns: The created `StreamRequest`. -@discardableResult -@available(iOS 9.0, macOS 10.11, tvOS 9.0, *) -public func stream(with netService: NetService) -> StreamRequest { - return SessionManager.default.stream(with: netService) + /// Encodes `multipartFormData` using `encodingMemoryThreshold` and uploads the result using `SessionManager.default` + /// using the `urlRequest` provided. + /// + /// It is important to understand the memory implications of uploading `MultipartFormData`. If the cummulative + /// payload is small, encoding the data in-memory and directly uploading to a server is the by far the most + /// efficient approach. However, if the payload is too large, encoding the data in-memory could cause your app to + /// be terminated. Larger payloads must first be written to disk using input and output streams to keep the memory + /// footprint low, then the data can be uploaded as a stream from the resulting file. Streaming from disk MUST be + /// used for larger payloads such as video content. + /// + /// The `encodingMemoryThreshold` parameter allows Alamofire to automatically determine whether to encode in-memory + /// or stream from disk. If the content length of the `MultipartFormData` is below the `encodingMemoryThreshold`, + /// encoding takes place in-memory. If the content length exceeds the threshold, the data is streamed to disk + /// during the encoding process. Then the result is uploaded as data or as a stream depending on which encoding + /// technique was used. + /// + /// - Parameters: + /// - multipartFormData: The closure used to append body parts to the `MultipartFormData`. + /// - encodingMemoryThreshold: The encoding memory threshold in bytes. `10_000_000` bytes by default. + /// - urlRequest: The `URLRequestConvertible` value. + /// - Returns: The `UploadRequest` created. + @discardableResult + public static func upload(multipartFormData: @escaping (MultipartFormData) -> Void, + usingThreshold encodingMemoryThreshold: UInt64 = MultipartUpload.encodingMemoryThreshold, + with urlRequest: URLRequestConvertible) -> UploadRequest { + return Session.default.upload(multipartFormData: multipartFormData, + usingThreshold: encodingMemoryThreshold, + with: urlRequest) + } } - -#endif diff --git a/Source/DispatchQueue+Alamofire.swift b/Source/DispatchQueue+Alamofire.swift index dea3ebc1b..fdfee6b7b 100644 --- a/Source/DispatchQueue+Alamofire.swift +++ b/Source/DispatchQueue+Alamofire.swift @@ -26,11 +26,6 @@ import Dispatch import Foundation extension DispatchQueue { - static var userInteractive: DispatchQueue { return DispatchQueue.global(qos: .userInteractive) } - static var userInitiated: DispatchQueue { return DispatchQueue.global(qos: .userInitiated) } - static var utility: DispatchQueue { return DispatchQueue.global(qos: .utility) } - static var background: DispatchQueue { return DispatchQueue.global(qos: .background) } - func after(_ delay: TimeInterval, execute closure: @escaping () -> Void) { asyncAfter(deadline: .now() + delay, execute: closure) } diff --git a/Source/EventMonitor.swift b/Source/EventMonitor.swift new file mode 100644 index 000000000..f0f99be38 --- /dev/null +++ b/Source/EventMonitor.swift @@ -0,0 +1,795 @@ +// +// EventMonitor.swift +// +// Copyright (c) 2014-2018 Alamofire Software Foundation (http://alamofire.org/) +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +import Foundation + +/// Protocol outlining the lifetime events inside Alamofire. It includes both events received from the various +/// `URLSession` delegate protocols as well as various events from the lifetime of `Request` and its subclasses. +public protocol EventMonitor { + /// The `DispatchQueue` onto which Alamofire's root `CompositeEventMonitor` will dispatch events. Defaults to `.main`. + var queue: DispatchQueue { get } + + // MARK: - URLSession Events + + // MARK: URLSessionDelegate Events + + /// Event called during `URLSessionDelegate`'s `urlSession(_:didBecomeInvalidWithError:)` method. + func urlSession(_ session: URLSession, didBecomeInvalidWithError error: Error?) + + // MARK: URLSessionTaskDelegate Events + + /// Event called during `URLSessionTaskDelegate`'s `urlSession(_:task:didReceive:completionHandler:)` method. + func urlSession(_ session: URLSession, task: URLSessionTask, didReceive challenge: URLAuthenticationChallenge) + + /// Event called during `URLSessionTaskDelegate`'s `urlSession(_:task:didSendBodyData:totalBytesSent:totalBytesExpectedToSend:)` method. + func urlSession(_ session: URLSession, + task: URLSessionTask, + didSendBodyData bytesSent: Int64, + totalBytesSent: Int64, + totalBytesExpectedToSend: Int64) + + /// Event called during `URLSessionTaskDelegate`'s `urlSession(_:task:needNewBodyStream:)` method. + func urlSession(_ session: URLSession, taskNeedsNewBodyStream task: URLSessionTask) + + /// Event called during `URLSessionTaskDelegate`'s `urlSession(_:task:willPerformHTTPRedirection:newRequest:completionHandler:)` method. + func urlSession(_ session: URLSession, + task: URLSessionTask, + willPerformHTTPRedirection response: HTTPURLResponse, + newRequest request: URLRequest) + + /// Event called during `URLSessionTaskDelegate`'s `urlSession(_:task:didFinishCollecting:)` method. + func urlSession(_ session: URLSession, task: URLSessionTask, didFinishCollecting metrics: URLSessionTaskMetrics) + + /// Event called during `URLSessionTaskDelegate`'s `urlSession(_:task:didCompleteWithError:)` method. + func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) + + /// Event called during `URLSessionTaskDelegate`'s `urlSession(_:taskIsWaitingForConnectivity:)` method. + @available(macOS 10.13, iOS 11.0, tvOS 11.0, watchOS 4.0, *) + func urlSession(_ session: URLSession, taskIsWaitingForConnectivity task: URLSessionTask) + + // MARK: URLSessionDataDelegate Events + + /// Event called during `URLSessionDataDelegate`'s `urlSession(_:dataTask:didReceive:)` method. + func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) + + /// Event called during `URLSessionDataDelegate`'s `urlSession(_:dataTask:willCacheResponse:completionHandler:)` method. + func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, willCacheResponse proposedResponse: CachedURLResponse) + + // MARK: URLSessionDownloadDelegate Events + + /// Event called during `URLSessionDownloadDelegate`'s `urlSession(_:downloadTask:didResumeAtOffset:expectedTotalBytes:)` method. + func urlSession(_ session: URLSession, + downloadTask: URLSessionDownloadTask, + didResumeAtOffset fileOffset: Int64, + expectedTotalBytes: Int64) + + /// Event called during `URLSessionDownloadDelegate`'s `urlSession(_:downloadTask:didWriteData:totalBytesWritten:totalBytesExpectedToWrite:)` method. + func urlSession(_ session: URLSession, + downloadTask: URLSessionDownloadTask, + didWriteData bytesWritten: Int64, + totalBytesWritten: Int64, + totalBytesExpectedToWrite: Int64) + + /// Event called during `URLSessionDownloadDelegate`'s `urlSession(_:downloadTask:didFinishDownloadingTo:)` method. + func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) + + // MARK: - Request Events + + /// Event called when a `URLRequest` is first created for a `Request`. + func request(_ request: Request, didCreateURLRequest urlRequest: URLRequest) + + /// Event called when the attempt to create a `URLRequest` from a `Request`'s original `URLRequestConvertible` value fails. + func request(_ request: Request, didFailToCreateURLRequestWithError error: Error) + + /// Event called when a `RequestAdapter` adapts the `Request`'s initial `URLRequest`. + func request(_ request: Request, didAdaptInitialRequest initialRequest: URLRequest, to adaptedRequest: URLRequest) + + /// Event called when a `RequestAdapter` fails to adapt the `Request`'s initial `URLRequest`. + func request(_ request: Request, didFailToAdaptURLRequest initialRequest: URLRequest, withError error: Error) + + /// Event called when a `URLSessionTask` subclass instance is created for a `Request`. + func request(_ request: Request, didCreateTask task: URLSessionTask) + + /// Event called when a `Request` receives a `URLSessionTaskMetrics` value. + func request(_ request: Request, didGatherMetrics metrics: URLSessionTaskMetrics) + + /// Event called when a `Request` fails due to an error created by Alamofire. e.g. When certificat pinning fails. + func request(_ request: Request, didFailTask task: URLSessionTask, earlyWithError error: Error) + + /// Event called when a `Request`'s task completes, possibly with an error. A `Request` may recieve this event + /// multiple times if it is retried. + func request(_ request: Request, didCompleteTask task: URLSessionTask, with error: Error?) + + /// Event called when a `Request` is about to be retried. + func requestIsRetrying(_ request: Request) + + /// Event called when a `Request` finishes and response serializers are being called. + func requestDidFinish(_ request: Request) + + /// Event called when a `Request` receives a `resume` call. + func requestDidResume(_ request: Request) + + /// Event called when a `Request` receives a `suspend` call. + func requestDidSuspend(_ request: Request) + + /// Event called when a `Request` receives a `cancel` call. + func requestDidCancel(_ request: Request) + + // MARK: DataRequest Events + + /// Event called when a `DataRequest` calls a `Validation`. + func request(_ request: DataRequest, + didValidateRequest urlRequest: URLRequest?, + response: HTTPURLResponse, + data: Data?, + withResult result: Request.ValidationResult) + + /// Event called when a `DataRequest` creates a `DataResponse` value without calling a `ResponseSerializer`. + func request(_ request: DataRequest, didParseResponse response: DataResponse) + + /// Event called when a `DataRequest` calls a `ResponseSerializer` and creates a generic `DataResponse`. + func request(_ request: DataRequest, didParseResponse response: DataResponse) + + // MARK: UploadRequest Events + + /// Event called when an `UploadRequest` creates its `Uploadable` value, indicating the type of upload it represents. + func request(_ request: UploadRequest, didCreateUploadable uploadable: UploadRequest.Uploadable) + + /// Event called when an `UploadRequest` failes to create its `Uploadable` value due to an error. + func request(_ request: UploadRequest, didFailToCreateUploadableWithError error: Error) + + /// Event called when an `UploadRequest` provides the `InputStream` from its `Uploadable` value. This only occurs if + /// the `InputStream` does not wrap a `Data` value or file `URL`. + func request(_ request: UploadRequest, didProvideInputStream stream: InputStream) + + // MARK: DownloadRequest Events + + /// Event called when a `DownloadRequest`'s `URLSessionDownloadTask` finishes and the temporary file has been moved. + func request(_ request: DownloadRequest, didFinishDownloadingUsing task: URLSessionTask, with result: Result) + + /// Event called when a `DownloadRequest`'s `Destination` closure is called and creates the destination URL the + /// downloaded file will be moved to. + func request(_ request: DownloadRequest, didCreateDestinationURL url: URL) + + /// Event called when a `DownloadRequest` calls a `Validation`. + func request(_ request: DownloadRequest, + didValidateRequest urlRequest: URLRequest?, + response: HTTPURLResponse, + fileURL: URL?, + withResult result: Request.ValidationResult) + + /// Event called when a `DownloadRequest` creates a `DownloadResponse` without calling a `ResponseSerializer`. + func request(_ request: DownloadRequest, didParseResponse response: DownloadResponse) + + /// Event called when a `DownloadRequest` calls a `DownloadResponseSerializer` and creates a generic `DownloadResponse` + func request(_ request: DownloadRequest, didParseResponse response: DownloadResponse) +} + +extension EventMonitor { + /// The default queue on which `CompositeEventMonitor`s will call the `EventMonitor` methods. Defaults to `.main`. + public var queue: DispatchQueue { return .main } + + // MARK: Default Implementations + + public func urlSession(_ session: URLSession, didBecomeInvalidWithError error: Error?) { } + public func urlSession(_ session: URLSession, + task: URLSessionTask, + didReceive challenge: URLAuthenticationChallenge) { } + public func urlSession(_ session: URLSession, + task: URLSessionTask, + didSendBodyData bytesSent: Int64, + totalBytesSent: Int64, + totalBytesExpectedToSend: Int64) { } + public func urlSession(_ session: URLSession, taskNeedsNewBodyStream task: URLSessionTask) { } + public func urlSession(_ session: URLSession, + task: URLSessionTask, + willPerformHTTPRedirection response: HTTPURLResponse, + newRequest request: URLRequest) { } + public func urlSession(_ session: URLSession, + task: URLSessionTask, + didFinishCollecting metrics: URLSessionTaskMetrics) { } + public func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) { } + public func urlSession(_ session: URLSession, taskIsWaitingForConnectivity task: URLSessionTask) { } + public func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) { } + public func urlSession(_ session: URLSession, + dataTask: URLSessionDataTask, + willCacheResponse proposedResponse: CachedURLResponse) { } + public func urlSession(_ session: URLSession, + downloadTask: URLSessionDownloadTask, + didResumeAtOffset fileOffset: Int64, + expectedTotalBytes: Int64) { } + public func urlSession(_ session: URLSession, + downloadTask: URLSessionDownloadTask, + didWriteData bytesWritten: Int64, + totalBytesWritten: Int64, + totalBytesExpectedToWrite: Int64) { } + public func urlSession(_ session: URLSession, + downloadTask: URLSessionDownloadTask, + didFinishDownloadingTo location: URL) { } + public func request(_ request: Request, didCreateURLRequest urlRequest: URLRequest) { } + public func request(_ request: Request, didFailToCreateURLRequestWithError error: Error) { } + public func request(_ request: Request, + didAdaptInitialRequest initialRequest: URLRequest, + to adaptedRequest: URLRequest) { } + public func request(_ request: Request, + didFailToAdaptURLRequest initialRequest: URLRequest, + withError error: Error) { } + public func request(_ request: Request, didCreateTask task: URLSessionTask) { } + public func request(_ request: Request, didGatherMetrics metrics: URLSessionTaskMetrics) { } + public func request(_ request: Request, didFailTask task: URLSessionTask, earlyWithError error: Error) { } + public func request(_ request: Request, didCompleteTask task: URLSessionTask, with error: Error?) { } + public func requestIsRetrying(_ request: Request) { } + public func requestDidFinish(_ request: Request) { } + public func requestDidResume(_ request: Request) { } + public func requestDidSuspend(_ request: Request) { } + public func requestDidCancel(_ request: Request) { } + public func request(_ request: DataRequest, + didValidateRequest urlRequest: URLRequest?, + response: HTTPURLResponse, + data: Data?, + withResult result: Request.ValidationResult) { } + public func request(_ request: DataRequest, didParseResponse response: DataResponse) { } + public func request(_ request: DataRequest, didParseResponse response: DataResponse) { } + public func request(_ request: UploadRequest, didCreateUploadable uploadable: UploadRequest.Uploadable) { } + public func request(_ request: UploadRequest, didFailToCreateUploadableWithError error: Error) { } + public func request(_ request: UploadRequest, didProvideInputStream stream: InputStream) { } + public func request(_ request: DownloadRequest, didFinishDownloadingUsing task: URLSessionTask, with result: Result) { } + public func request(_ request: DownloadRequest, didCreateDestinationURL url: URL) { } + public func request(_ request: DownloadRequest, + didValidateRequest urlRequest: URLRequest?, + response: HTTPURLResponse, + fileURL: URL?, + withResult result: Request.ValidationResult) { } + public func request(_ request: DownloadRequest, didParseResponse response: DownloadResponse) { } + public func request(_ request: DownloadRequest, didParseResponse response: DownloadResponse) { } +} + +/// An `EventMonitor` which can contain multiple `EventMonitor`s and calls their methods on their queues. +public final class CompositeEventMonitor: EventMonitor { + public let queue = DispatchQueue(label: "org.alamofire.componsiteEventMonitor", qos: .background) + + let monitors: [EventMonitor] + + init(monitors: [EventMonitor]) { + self.monitors = monitors + } + + func performEvent(_ event: @escaping (EventMonitor) -> Void) { + queue.async { + for monitor in self.monitors { + monitor.queue.async { event(monitor) } + } + } + } + + public func urlSession(_ session: URLSession, didBecomeInvalidWithError error: Error?) { + performEvent { $0.urlSession(session, didBecomeInvalidWithError: error) } + } + + public func urlSession(_ session: URLSession, + task: URLSessionTask, + didReceive challenge: URLAuthenticationChallenge) { + performEvent { $0.urlSession(session, task: task, didReceive: challenge) } + } + + public func urlSession(_ session: URLSession, + task: URLSessionTask, + didSendBodyData bytesSent: Int64, + totalBytesSent: Int64, + totalBytesExpectedToSend: Int64) { + performEvent { + $0.urlSession(session, + task: task, + didSendBodyData: bytesSent, + totalBytesSent: totalBytesSent, + totalBytesExpectedToSend: totalBytesExpectedToSend) + } + } + + public func urlSession(_ session: URLSession, taskNeedsNewBodyStream task: URLSessionTask) { + performEvent { + $0.urlSession(session, taskNeedsNewBodyStream: task) + } + } + + public func urlSession(_ session: URLSession, + task: URLSessionTask, + willPerformHTTPRedirection response: HTTPURLResponse, + newRequest request: URLRequest) { + performEvent { + $0.urlSession(session, + task: task, + willPerformHTTPRedirection: response, + newRequest: request) + } + } + + public func urlSession(_ session: URLSession, task: URLSessionTask, didFinishCollecting metrics: URLSessionTaskMetrics) { + performEvent { $0.urlSession(session, task: task, didFinishCollecting: metrics) } + } + + public func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) { + performEvent { $0.urlSession(session, task: task, didCompleteWithError: error) } + } + + @available(macOS 10.13, iOS 11.0, tvOS 11.0, watchOS 4.0, *) + public func urlSession(_ session: URLSession, taskIsWaitingForConnectivity task: URLSessionTask) { + performEvent { $0.urlSession(session, taskIsWaitingForConnectivity: task) } + } + + public func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) { + performEvent { $0.urlSession(session, dataTask: dataTask, didReceive: data) } + } + + public func urlSession(_ session: URLSession, + dataTask: URLSessionDataTask, + willCacheResponse proposedResponse: CachedURLResponse) { + performEvent { $0.urlSession(session, dataTask: dataTask, willCacheResponse: proposedResponse) } + } + + public func urlSession(_ session: URLSession, + downloadTask: URLSessionDownloadTask, + didResumeAtOffset fileOffset: Int64, + expectedTotalBytes: Int64) { + performEvent { + $0.urlSession(session, + downloadTask: downloadTask, + didResumeAtOffset: fileOffset, + expectedTotalBytes: expectedTotalBytes) + } + } + + public func urlSession(_ session: URLSession, + downloadTask: URLSessionDownloadTask, + didWriteData bytesWritten: Int64, + totalBytesWritten: Int64, + totalBytesExpectedToWrite: Int64) { + performEvent { + $0.urlSession(session, + downloadTask: downloadTask, + didWriteData: bytesWritten, + totalBytesWritten: totalBytesWritten, + totalBytesExpectedToWrite: totalBytesExpectedToWrite) + } + } + + public func urlSession(_ session: URLSession, + downloadTask: URLSessionDownloadTask, + didFinishDownloadingTo location: URL) { + performEvent { $0.urlSession(session, downloadTask: downloadTask, didFinishDownloadingTo: location) } + } + + public func request(_ request: Request, didCreateURLRequest urlRequest: URLRequest) { + performEvent { $0.request(request, didCreateURLRequest: urlRequest) } + } + + public func request(_ request: Request, didFailToCreateURLRequestWithError error: Error) { + performEvent { $0.request(request, didFailToCreateURLRequestWithError: error) } + } + + public func request(_ request: Request, didAdaptInitialRequest initialRequest: URLRequest, to adaptedRequest: URLRequest) { + performEvent { $0.request(request, didAdaptInitialRequest: initialRequest, to: adaptedRequest) } + } + + public func request(_ request: Request, didFailToAdaptURLRequest initialRequest: URLRequest, withError error: Error) { + performEvent { $0.request(request, didFailToAdaptURLRequest: initialRequest, withError: error) } + } + + public func request(_ request: Request, didCreateTask task: URLSessionTask) { + performEvent { $0.request(request, didCreateTask: task) } + } + + public func request(_ request: Request, didGatherMetrics metrics: URLSessionTaskMetrics) { + performEvent { $0.request(request, didGatherMetrics: metrics) } + } + + public func request(_ request: Request, didFailTask task: URLSessionTask, earlyWithError error: Error) { + performEvent { $0.request(request, didFailTask: task, earlyWithError: error) } + } + + public func request(_ request: Request, didCompleteTask task: URLSessionTask, with error: Error?) { + performEvent { $0.request(request, didCompleteTask: task, with: error) } + } + + public func requestIsRetrying(_ request: Request) { + performEvent { $0.requestIsRetrying(request) } + } + + public func requestDidFinish(_ request: Request) { + performEvent { $0.requestDidFinish(request) } + } + + public func requestDidResume(_ request: Request) { + performEvent { $0.requestDidResume(request) } + } + + public func requestDidSuspend(_ request: Request) { + performEvent { $0.requestDidSuspend(request) } + } + + public func requestDidCancel(_ request: Request) { + performEvent { $0.requestDidCancel(request) } + } + + public func request(_ request: DataRequest, + didValidateRequest urlRequest: URLRequest?, + response: HTTPURLResponse, + data: Data?, + withResult result: Request.ValidationResult) { + performEvent { $0.request(request, + didValidateRequest: urlRequest, + response: response, + data: data, + withResult: result) + } + } + + public func request(_ request: DataRequest, didParseResponse response: DataResponse) { + performEvent { $0.request(request, didParseResponse: response) } + } + + public func request(_ request: DataRequest, didParseResponse response: DataResponse) { + performEvent { $0.request(request, didParseResponse: response) } + } + + public func request(_ request: UploadRequest, didCreateUploadable uploadable: UploadRequest.Uploadable) { + performEvent { $0.request(request, didCreateUploadable: uploadable) } + } + + public func request(_ request: UploadRequest, didFailToCreateUploadableWithError error: Error) { + performEvent { $0.request(request, didFailToCreateUploadableWithError: error) } + } + + public func request(_ request: UploadRequest, didProvideInputStream stream: InputStream) { + performEvent { $0.request(request, didProvideInputStream: stream) } + } + + public func request(_ request: DownloadRequest, didFinishDownloadingUsing task: URLSessionTask, with result: Result) { + performEvent { $0.request(request, didFinishDownloadingUsing: task, with: result) } + } + + public func request(_ request: DownloadRequest, didCreateDestinationURL url: URL) { + performEvent { $0.request(request, didCreateDestinationURL: url) } + } + + public func request(_ request: DownloadRequest, + didValidateRequest urlRequest: URLRequest?, + response: HTTPURLResponse, + fileURL: URL?, + withResult result: Request.ValidationResult) { + performEvent { $0.request(request, + didValidateRequest: urlRequest, + response: response, + fileURL: fileURL, + withResult: result) } + } + + public func request(_ request: DownloadRequest, didParseResponse response: DownloadResponse) { + performEvent { $0.request(request, didParseResponse: response) } + } + + public func request(_ request: DownloadRequest, didParseResponse response: DownloadResponse) { + performEvent { $0.request(request, didParseResponse: response) } + } +} + +/// `EventMonitor` that allows optional closures to be set to receive events. +open class ClosureEventMonitor: EventMonitor { + /// Closure called on the `urlSession(_:didBecomeInvalidWithError:)` event. + open var sessionDidBecomeInvalidWithError: ((URLSession, Error?) -> Void)? + + /// Closure called on the `urlSession(_:task:didReceive:completionHandler:)`. + open var taskDidReceiveChallenge: ((URLSession, URLSessionTask, URLAuthenticationChallenge) -> Void)? + + /// Closure that receives `urlSession(_:task:didSendBodyData:totalBytesSent:totalBytesExpectedToSend:)` event. + open var taskDidSendBodyData: ((URLSession, URLSessionTask, Int64, Int64, Int64) -> Void)? + + /// Closure called on the `urlSession(_:task:needNewBodyStream:)` event. + open var taskNeedNewBodyStream: ((URLSession, URLSessionTask) -> Void)? + + /// Closure called on the `urlSession(_:task:willPerformHTTPRedirection:newRequest:completionHandler:)` event. + open var taskWillPerformHTTPRedirection: ((URLSession, URLSessionTask, HTTPURLResponse, URLRequest) -> Void)? + + /// Closure called on the `urlSession(_:task:didFinishCollecting:)` event. + open var taskDidFinishCollectingMetrics: ((URLSession, URLSessionTask, URLSessionTaskMetrics) -> Void)? + + /// Closure called on the `urlSession(_:task:didCompleteWithError:)` event. + open var taskDidComplete: ((URLSession, URLSessionTask, Error?) -> Void)? + + /// Closure called on the `urlSession(_:taskIsWaitingForConnectivity:)` event. + open var taskIsWaitingForConnectivity: ((URLSession, URLSessionTask) -> Void)? + + /// Closure that recieves the `urlSession(_:dataTask:didReceive:)` event. + open var dataTaskDidReceiveData: ((URLSession, URLSessionDataTask, Data) -> Void)? + + /// Closure called on the `urlSession(_:dataTask:willCacheResponse:completionHandler:)` event. + open var dataTaskWillCacheResponse: ((URLSession, URLSessionDataTask, CachedURLResponse) -> Void)? + + /// Closure called on the `urlSession(_:downloadTask:didFinishDownloadingTo:)` event. + open var downloadTaskDidFinishDownloadingToURL: ((URLSession, URLSessionDownloadTask, URL) -> Void)? + + /// Closure called on the `urlSession(_:downloadTask:didWriteData:totalBytesWritten:totalBytesExpectedToWrite:)` + /// event. + open var downloadTaskDidWriteData: ((URLSession, URLSessionDownloadTask, Int64, Int64, Int64) -> Void)? + + /// Closure called on the `urlSession(_:downloadTask:didResumeAtOffset:expectedTotalBytes:)` event. + open var downloadTaskDidResumeAtOffset: ((URLSession, URLSessionDownloadTask, Int64, Int64) -> Void)? + + // MARK: - Request Events + + /// Closure called on the `request(_:didCreateURLRequest:)` event. + open var requestDidCreateURLRequest: ((Request, URLRequest) -> Void)? + + /// Closure called on the `request(_:didFailToCreateURLRequestWithError:)` event. + open var requestDidFailToCreateURLRequestWithError: ((Request, Error) -> Void)? + + /// Closure called on the `request(_:didAdaptInitialRequest:to:)` event. + open var requestDidAdaptInitialRequestToAdaptedRequest: ((Request, URLRequest, URLRequest) -> Void)? + + /// Closure called on the `request(_:didFailToAdaptURLRequest:withError:)` event. + open var requestDidFailToAdaptURLRequestWithError: ((Request, URLRequest, Error) -> Void)? + + /// Closure called on the `request(_:didCreateTask:)` event. + open var requestDidCreateTask: ((Request, URLSessionTask) -> Void)? + + /// Closure called on the `request(_:didGatherMetrics:)` event. + open var requestDidGatherMetrics: ((Request, URLSessionTaskMetrics) -> Void)? + + /// Closure called on the `request(_:didFailTask:earlyWithError:)` event. + open var requestDidFailTaskEarlyWithError: ((Request, URLSessionTask, Error) -> Void)? + + /// Closure called on the `request(_:didCompleteTask:with:)` event. + open var requestDidCompleteTaskWithError: ((Request, URLSessionTask, Error?) -> Void)? + + /// Closure called on the `requestIsRetrying(_:)` event. + open var requestIsRetrying: ((Request) -> Void)? + + /// Closure called on the `requestDidFinish(_:)` event. + open var requestDidFinish: ((Request) -> Void)? + + /// Closure called on the `requestDidResume(_:)` event. + open var requestDidResume: ((Request) -> Void)? + + /// Closure called on the `requestDidSuspend(_:)` event. + open var requestDidSuspend: ((Request) -> Void)? + + /// Closure called on the `requestDidCancel(_:)` event. + open var requestDidCancel: ((Request) -> Void)? + + /// Closure called on the `request(_:didValidateRequest:response:data:withResult:)` event. + open var requestDidValidateRequestResponseDataWithResult: ((DataRequest, URLRequest?, HTTPURLResponse, Data?, Request.ValidationResult) -> Void)? + + /// Closure called on the `request(_:didParseResponse:)` event. + open var requestDidParseResponse: ((DataRequest, DataResponse) -> Void)? + + /// Closure called on the `request(_:didParseResponse` event, casting the generic serialized object to `Any`. + open var requestDidParseAnyResponse: ((DataRequest, DataResponse) -> Void)? + + /// Closure called on the `request(_:didCreateUploadable:)` event. + open var requestDidCreateUploadable: ((UploadRequest, UploadRequest.Uploadable) -> Void)? + + /// Closure called on the `request(_:didFailToCreateUploadableWithError:)` event. + open var requestDidFailToCreateUploadableWithError: ((UploadRequest, Error) -> Void)? + + /// Closure called on the `request(_:didProvideInputStream:)` event. + open var requestDidProvideInputStream: ((UploadRequest, InputStream) -> Void)? + + /// Closure called on the `request(_:didFinishDownloadingUsing:with:)` event. + open var requestDidFinishDownloadingUsingTaskWithResult: ((DownloadRequest, URLSessionTask, Result) -> Void)? + + /// Closure called on the `request(_:didCreateDestinationURL:)` event. + open var requestDidCreateDestinationURL: ((DownloadRequest, URL) -> Void)? + + /// Closure called on the `request(_:didValidateRequest:response:temporaryURL:destinationURL:withResult:)` event. + open var requestDidValidateRequestResponseFileURLWithResult: ((DownloadRequest, URLRequest?, HTTPURLResponse, URL?, Request.ValidationResult) -> Void)? + + /// Closure called on the `request(_:didParseResponse:)` event. + open var requestDidParseDownloadResponse: ((DownloadRequest, DownloadResponse) -> Void)? + + /// Closure called on the `request(_:didParseResponse:)` event, casting the generic serialized object to `Any`. + open var requestDidParseAnyDownloadResponse: ((DownloadRequest, DownloadResponse) -> Void)? + + public let queue: DispatchQueue + + public init(queue: DispatchQueue = .main) { + self.queue = queue + } + + open func urlSession(_ session: URLSession, didBecomeInvalidWithError error: Error?) { + sessionDidBecomeInvalidWithError?(session, error) + } + + open func urlSession(_ session: URLSession, task: URLSessionTask, didReceive challenge: URLAuthenticationChallenge) { + taskDidReceiveChallenge?(session, task, challenge) + } + + open func urlSession(_ session: URLSession, + task: URLSessionTask, + didSendBodyData bytesSent: Int64, + totalBytesSent: Int64, + totalBytesExpectedToSend: Int64) { + taskDidSendBodyData?(session, task, bytesSent, totalBytesSent, totalBytesExpectedToSend) + } + + open func urlSession(_ session: URLSession, taskNeedsNewBodyStream task: URLSessionTask) { + taskNeedNewBodyStream?(session, task) + } + + open func urlSession(_ session: URLSession, + task: URLSessionTask, + willPerformHTTPRedirection response: HTTPURLResponse, + newRequest request: URLRequest) { + taskWillPerformHTTPRedirection?(session, task, response, request) + } + + open func urlSession(_ session: URLSession, task: URLSessionTask, didFinishCollecting metrics: URLSessionTaskMetrics) { + taskDidFinishCollectingMetrics?(session, task, metrics) + } + + open func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) { + taskDidComplete?(session, task, error) + } + + open func urlSession(_ session: URLSession, taskIsWaitingForConnectivity task: URLSessionTask) { + taskIsWaitingForConnectivity?(session, task) + } + + open func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) { + dataTaskDidReceiveData?(session, dataTask, data) + } + + open func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, willCacheResponse proposedResponse: CachedURLResponse) { + dataTaskWillCacheResponse?(session, dataTask, proposedResponse) + } + + open func urlSession(_ session: URLSession, + downloadTask: URLSessionDownloadTask, + didResumeAtOffset fileOffset: Int64, + expectedTotalBytes: Int64) { + downloadTaskDidResumeAtOffset?(session, downloadTask, fileOffset, expectedTotalBytes) + } + + open func urlSession(_ session: URLSession, + downloadTask: URLSessionDownloadTask, + didWriteData bytesWritten: Int64, + totalBytesWritten: Int64, + totalBytesExpectedToWrite: Int64) { + downloadTaskDidWriteData?(session, downloadTask, bytesWritten, totalBytesWritten, totalBytesExpectedToWrite) + } + + open func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) { + downloadTaskDidFinishDownloadingToURL?(session, downloadTask, location) + } + + // MARK: Request Events + + open func request(_ request: Request, didCreateURLRequest urlRequest: URLRequest) { + requestDidCreateURLRequest?(request, urlRequest) + } + + open func request(_ request: Request, didFailToCreateURLRequestWithError error: Error) { + requestDidFailToCreateURLRequestWithError?(request, error) + } + + open func request(_ request: Request, didAdaptInitialRequest initialRequest: URLRequest, to adaptedRequest: URLRequest) { + requestDidAdaptInitialRequestToAdaptedRequest?(request, initialRequest, adaptedRequest) + } + + open func request(_ request: Request, didFailToAdaptURLRequest initialRequest: URLRequest, withError error: Error) { + requestDidFailToAdaptURLRequestWithError?(request, initialRequest, error) + } + + open func request(_ request: Request, didCreateTask task: URLSessionTask) { + requestDidCreateTask?(request, task) + } + + open func request(_ request: Request, didGatherMetrics metrics: URLSessionTaskMetrics) { + requestDidGatherMetrics?(request, metrics) + } + + open func request(_ request: Request, didFailTask task: URLSessionTask, earlyWithError error: Error) { + requestDidFailTaskEarlyWithError?(request, task, error) + } + + open func request(_ request: Request, didCompleteTask task: URLSessionTask, with error: Error?) { + requestDidCompleteTaskWithError?(request, task, error) + } + + open func requestIsRetrying(_ request: Request) { + requestIsRetrying?(request) + } + + open func requestDidFinish(_ request: Request) { + requestDidFinish?(request) + } + + open func requestDidResume(_ request: Request) { + requestDidResume?(request) + } + + open func requestDidSuspend(_ request: Request) { + requestDidSuspend?(request) + } + + open func requestDidCancel(_ request: Request) { + requestDidCancel?(request) + } + + open func request(_ request: DataRequest, + didValidateRequest urlRequest: URLRequest?, + response: HTTPURLResponse, + data: Data?, + withResult result: Request.ValidationResult) { + requestDidValidateRequestResponseDataWithResult?(request, urlRequest, response, data, result) + } + + open func request(_ request: DataRequest, didParseResponse response: DataResponse) { + requestDidParseResponse?(request, response) + } + + open func request(_ request: DataRequest, didParseResponse response: DataResponse) { + // TODO: Make all methods optional so this isn't required. + requestDidParseAnyResponse?(request, response as! DataResponse) + } + + open func request(_ request: UploadRequest, didCreateUploadable uploadable: UploadRequest.Uploadable) { + requestDidCreateUploadable?(request, uploadable) + } + + open func request(_ request: UploadRequest, didFailToCreateUploadableWithError error: Error) { + requestDidFailToCreateUploadableWithError?(request, error) + } + + open func request(_ request: UploadRequest, didProvideInputStream stream: InputStream) { + requestDidProvideInputStream?(request, stream) + } + + open func request(_ request: DownloadRequest, didFinishDownloadingUsing task: URLSessionTask, with result: Result) { + requestDidFinishDownloadingUsingTaskWithResult?(request, task, result) + } + + open func request(_ request: DownloadRequest, didCreateDestinationURL url: URL) { + requestDidCreateDestinationURL?(request, url) + } + + open func request(_ request: DownloadRequest, + didValidateRequest urlRequest: URLRequest?, + response: HTTPURLResponse, + fileURL: URL?, + withResult result: Request.ValidationResult) { + requestDidValidateRequestResponseFileURLWithResult?(request, + urlRequest, + response, + fileURL, + result) + } + + open func request(_ request: DownloadRequest, didParseResponse response: DownloadResponse) { + requestDidParseDownloadResponse?(request, response) + } + + open func request(_ request: DownloadRequest, didParseResponse response: DownloadResponse) { + requestDidParseAnyDownloadResponse?(request, response as! DownloadResponse) + } +} diff --git a/Source/HTTPHeaders.swift b/Source/HTTPHeaders.swift new file mode 100644 index 000000000..921182348 --- /dev/null +++ b/Source/HTTPHeaders.swift @@ -0,0 +1,435 @@ +// +// HTTPHeaders.swift +// +// Copyright (c) 2014-2018 Alamofire Software Foundation (http://alamofire.org/) +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +import Foundation + + +/// An order-preserving and case-insensitive representation of HTTP headers. +public struct HTTPHeaders { + private var headers = [HTTPHeader]() + + /// Create an empty instance. + public init() { } + + /// Create an instance from an array of `HTTPHeader`s. Duplicate case-insensitive names are collapsed into the last + /// name and value encountered. + public init(_ headers: [HTTPHeader]) { + self.init() + + headers.forEach { update($0) } + } + + /// Create an instance from a `[String: String]`. Duplicate case-insensitive names are collapsed into the last name + /// and value encountered. + public init(_ dictionary: [String: String]) { + self.init() + + dictionary.forEach { update(HTTPHeader(name: $0.key, value: $0.value)) } + } + + /// Case-insensitively updates or appends an `HTTPHeader` into the instance using the provided `name` and `value`. + /// + /// - Parameters: + /// - name: The `HTTPHeader` name. + /// - value: The `HTTPHeader value. + public mutating func add(name: String, value: String) { + update(HTTPHeader(name: name, value: value)) + } + + /// Case-insensitively updates or appends the provided `HTTPHeader` into the instance. + /// + /// - Parameter header: The `HTTPHeader` to update or append. + public mutating func add(_ header: HTTPHeader) { + update(header) + } + + /// Case-insensitively updates or appends an `HTTPHeader` into the instance using the provided `name` and `value`. + /// + /// - Parameters: + /// - name: The `HTTPHeader` name. + /// - value: The `HTTPHeader value. + public mutating func update(name: String, value: String) { + update(HTTPHeader(name: name, value: value)) + } + + /// Case-insensitively updates or appends the provided `HTTPHeader` into the instance. + /// + /// - Parameter header: The `HTTPHeader` to update or append. + public mutating func update(_ header: HTTPHeader) { + guard let index = headers.index(of: header.name) else { + headers.append(header) + return + } + + headers.replaceSubrange(index...index, with: [header]) + } + + /// Case-insensitively removes an `HTTPHeader`, if it exists, from the instance. + /// + /// - Parameter name: The name of the `HTTPHeader` to remove. + public mutating func remove(name: String) { + guard let index = headers.index(of: name) else { return } + + headers.remove(at: index) + } + + /// Sort the current instance by header name. + mutating public func sort() { + headers.sort { $0.name < $1.name } + } + + /// Returns an instance sorted by header name. + /// + /// - Returns: A copy of the current instance sorted by name. + public func sorted() -> HTTPHeaders { + return HTTPHeaders(headers.sorted { $0.name < $1.name }) + } + + /// Case-insensitively find a header's value by name. + /// + /// - Parameter name: The name of the header to search for, case-insensitively. + /// - Returns: The value of header, if it exists. + public func value(for name: String) -> String? { + guard let index = headers.index(of: name) else { return nil } + + return headers[index].value + } + + /// Case-insensitively access the header with the given name. + /// + /// - Parameter name: The name of the header. + public subscript(_ name: String) -> String? { + get { return value(for: name) } + set { + if let value = newValue { + update(name: name, value: value) + } else { + remove(name: name) + } + } + } + + /// The dictionary representation of all headers. + /// + /// This representation does not preserve the current order of the instance. + public var dictionary: [String: String] { + let namesAndValues = headers.map { ($0.name, $0.value) } + + return Dictionary(namesAndValues, uniquingKeysWith: { (_, last) in last }) + } +} + +extension HTTPHeaders: ExpressibleByDictionaryLiteral { + public init(dictionaryLiteral elements: (String, String)...) { + self.init() + + elements.forEach { update(name: $0.0, value: $0.1) } + } +} + +extension HTTPHeaders: ExpressibleByArrayLiteral { + public init(arrayLiteral elements: HTTPHeader...) { + self.init(elements) + } +} + +extension HTTPHeaders: Sequence { + public func makeIterator() -> IndexingIterator> { + return headers.makeIterator() + } +} + +extension HTTPHeaders: Collection { + public var startIndex: Int { + return headers.startIndex + } + + public var endIndex: Int { + return headers.endIndex + } + + public subscript(position: Int) -> HTTPHeader { + return headers[position] + } + + public func index(after i: Int) -> Int { + return headers.index(after: i) + } +} + +extension HTTPHeaders: CustomStringConvertible { + public var description: String { + return headers.map { $0.description } + .joined(separator: "\n") + } +} + +// MARK: - HTTPHeader + +/// A representation of a single HTTP header's name / value pair. +public struct HTTPHeader: Hashable { + /// Name of the header. + public let name: String + + /// Value of the header. + public let value: String + + /// Creates an instance from the given `name` and `value`. + /// + /// - Parameters: + /// - name: The name of the header. + /// - value: The value of the header. + public init(name: String, value: String) { + self.name = name + self.value = value + } +} + +extension HTTPHeader: CustomStringConvertible { + public var description: String { + return "\(name): \(value)" + } +} + +extension HTTPHeader { + /// Returns an `Accept-Charset` header. + /// + /// - Parameter value: The `Accept-Charset` value. + /// - Returns: The header. + public static func acceptCharset(_ value: String) -> HTTPHeader { + return HTTPHeader(name: "Accept-Charset", value: value) + } + + /// Returns an `Accept-Language` header. + /// + /// Alamofire offers a default Accept-Language header that accumulates and encodes the system's preferred languages. + /// Use `HTTPHeader.defaultAcceptLanguage`. + /// + /// - Parameter value: The `Accept-Language` value. + /// - Returns: The header. + public static func acceptLanguage(_ value: String) -> HTTPHeader { + return HTTPHeader(name: "Accept-Language", value: value) + } + + /// Returns an `Accept-Encoding` header. + /// + /// Alamofire offers a default accept encoding value that provides the most common values. Use + /// `HTTPHeader.defaultAcceptEncoding`. + /// + /// - Parameter value: The `Accept-Encoding` value. + /// - Returns: The header + public static func acceptEncoding(_ value: String) -> HTTPHeader { + return HTTPHeader(name: "Accept-Encoding", value: value) + } + + /// Returns a `Basic` `Authorization` header using the `username` and `password` provided. + /// + /// - Parameters: + /// - username: The username of the header. + /// - password: The password of the header. + /// - Returns: The header. + public static func authorization(username: String, password: String) -> HTTPHeader { + let credential = Data("\(username):\(password)".utf8).base64EncodedString() + + return authorization("Basic \(credential)") + } + + /// Returns a `Bearer` `Authorization` header using the `bearerToken` provided + /// + /// - Parameter bearerToken: The bearer token. + /// - Returns: The header. + public static func authorization(bearerToken: String) -> HTTPHeader { + return authorization("Bearer \(bearerToken)") + } + + /// Returns an `Authorization` header. + /// + /// Alamofire provides built-in methods to produce `Authorization` headers. For a Basic `Authorization` header use + /// `HTTPHeader.authorization(username: password:)`. For a Bearer `Authorization` header, use + /// `HTTPHeader.authorization(bearerToken:)`. + /// + /// - Parameter value: The `Authorization` value. + /// - Returns: The header. + public static func authorization(_ value: String) -> HTTPHeader { + return HTTPHeader(name: "Authorization", value: value) + } + + /// Returns a `Content-Disposition` header. + /// + /// - Parameter value: The `Content-Disposition` value. + /// - Returns: The header. + public static func contentDisposition(_ value: String) -> HTTPHeader { + return HTTPHeader(name: "Content-Disposition", value: value) + } + + /// Returns a `Content-Type` header. + /// + /// All Alamofire `ParameterEncoding`s set the `Content-Type` of the request, so it may not be necessary to manually + /// set this value. + /// + /// - Parameter value: The `Content-Type` value. + /// - Returns: The header. + public static func contentType(_ value: String) -> HTTPHeader { + return HTTPHeader(name: "Content-Type", value: value) + } + + /// Returns a `User-Agent` header. + /// + /// - Parameter value: The `User-Agent` value. + /// - Returns: The header. + public static func userAgent(_ value: String) -> HTTPHeader { + return HTTPHeader(name: "User-Agent", value: value) + } +} + +extension Array where Element == HTTPHeader { + /// Case-insensitively finds the index of an `HTTPHeader` with the provided name, if it exists. + func index(of name: String) -> Int? { + let lowercasedName = name.lowercased() + return firstIndex { $0.name.lowercased() == lowercasedName } + } +} + +// MARK: - Defaults + +extension HTTPHeaders { + /// The default set of `HTTPHeaders` used by Alamofire. Includes `Accept-Encoding`, `Accept-Language`, and + /// `User-Agent`. + public static let `default`: HTTPHeaders = [.defaultAcceptEncoding, + .defaultAcceptLanguage, + .defaultUserAgent] +} + +extension HTTPHeader { + /// Returns Alamofire's default `Accept-Encoding` header, appropriate for the encodings supporte by particular OS + /// versions. + /// + /// See the [Accept-Encoding HTTP header documentation](https://tools.ietf.org/html/rfc7230#section-4.2.3) . + public static let defaultAcceptEncoding: HTTPHeader = { + let encodings: [String] + if #available(iOS 11.0, macOS 10.13, tvOS 11.0, watchOS 4.0, *) { + encodings = ["br", "gzip", "deflate"] + } else { + encodings = ["gzip", "deflate"] + } + + return .acceptEncoding(encodings.qualityEncoded) + }() + + /// Returns Alamofire's default `Accept-Language` header, generated by querying `Locale` for the user's + /// `preferredLanguages`. + /// + /// See the [Accept-Language HTTP header documentation](https://tools.ietf.org/html/rfc7231#section-5.3.5). + public static let defaultAcceptLanguage: HTTPHeader = { + .acceptLanguage(Locale.preferredLanguages.prefix(6).qualityEncoded) + }() + + /// Returns Alamofire's default `User-Agent` header. + /// + /// See the [User-Agent header documentation](https://tools.ietf.org/html/rfc7231#section-5.5.3). + /// + /// Example: `iOS Example/1.0 (org.alamofire.iOS-Example; build:1; iOS 12.0.0) Alamofire/5.0.0` + public static let defaultUserAgent: HTTPHeader = { + let userAgent: String = { + if let info = Bundle.main.infoDictionary { + let executable = info[kCFBundleExecutableKey as String] as? String ?? "Unknown" + let bundle = info[kCFBundleIdentifierKey as String] as? String ?? "Unknown" + let appVersion = info["CFBundleShortVersionString"] as? String ?? "Unknown" + let appBuild = info[kCFBundleVersionKey as String] as? String ?? "Unknown" + + let osNameVersion: String = { + let version = ProcessInfo.processInfo.operatingSystemVersion + let versionString = "\(version.majorVersion).\(version.minorVersion).\(version.patchVersion)" + + let osName: String = { + #if os(iOS) + return "iOS" + #elseif os(watchOS) + return "watchOS" + #elseif os(tvOS) + return "tvOS" + #elseif os(macOS) + return "macOS" + #elseif os(Linux) + return "Linux" + #else + return "Unknown" + #endif + }() + + return "\(osName) \(versionString)" + }() + + let alamofireVersion: String = { + guard + let afInfo = Bundle(for: Session.self).infoDictionary, + let build = afInfo["CFBundleShortVersionString"] + else { return "Unknown" } + + return "Alamofire/\(build)" + }() + + return "\(executable)/\(appVersion) (\(bundle); build:\(appBuild); \(osNameVersion)) \(alamofireVersion)" + } + + return "Alamofire" + }() + + return .userAgent(userAgent) + }() +} + +extension Collection where Element == String { + var qualityEncoded: String { + return enumerated().map { (index, encoding) in + let quality = 1.0 - (Double(index) * 0.1) + return "\(encoding);q=\(quality)" + }.joined(separator: ", ") + } +} + +// MARK: - System Type Extensions + +extension URLRequest { + /// Returns `allHTTPHeaderFields` as `HTTPHeaders`. + public var httpHeaders: HTTPHeaders { + get { return allHTTPHeaderFields.map(HTTPHeaders.init) ?? HTTPHeaders() } + set { allHTTPHeaderFields = newValue.dictionary } + } +} + +extension HTTPURLResponse { + /// Returns `allHeaderFields` as `HTTPHeaders`. + public var httpHeaders: HTTPHeaders { + return (allHeaderFields as? [String: String]).map(HTTPHeaders.init) ?? HTTPHeaders() + } +} + +extension URLSessionConfiguration { + /// Returns `httpAdditionalHeaders` as `HTTPHeaders`. + public var httpHeaders: HTTPHeaders { + get { return (httpAdditionalHeaders as? [String: String]).map(HTTPHeaders.init) ?? HTTPHeaders() } + set { httpAdditionalHeaders = newValue.dictionary } + } +} diff --git a/Source/HTTPMethod.swift b/Source/HTTPMethod.swift new file mode 100644 index 000000000..44049bcb4 --- /dev/null +++ b/Source/HTTPMethod.swift @@ -0,0 +1,38 @@ +// +// HTTPMethod.swift +// +// Copyright (c) 2014-2018 Alamofire Software Foundation (http://alamofire.org/) +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +/// HTTP method definitions. +/// +/// See https://tools.ietf.org/html/rfc7231#section-4.3 +public enum HTTPMethod: String { + case connect = "CONNECT" + case delete = "DELETE" + case get = "GET" + case head = "HEAD" + case options = "OPTIONS" + case patch = "PATCH" + case post = "POST" + case put = "PUT" + case trace = "TRACE" +} diff --git a/Source/Info-tvOS.plist b/Source/Info-tvOS.plist deleted file mode 100644 index 4de2abe69..000000000 --- a/Source/Info-tvOS.plist +++ /dev/null @@ -1,30 +0,0 @@ - - - - - CFBundleDevelopmentRegion - en - CFBundleExecutable - $(EXECUTABLE_NAME) - CFBundleIdentifier - $(PRODUCT_BUNDLE_IDENTIFIER) - CFBundleInfoDictionaryVersion - 6.0 - CFBundleName - $(PRODUCT_NAME) - CFBundlePackageType - FMWK - CFBundleShortVersionString - 4.8.0 - CFBundleSignature - ???? - CFBundleVersion - $(CURRENT_PROJECT_VERSION) - NSPrincipalClass - - UIRequiredDeviceCapabilities - - arm64 - - - diff --git a/Source/Info.plist b/Source/Info.plist index 279a20f41..747466d1b 100644 --- a/Source/Info.plist +++ b/Source/Info.plist @@ -15,12 +15,10 @@ CFBundlePackageType FMWK CFBundleShortVersionString - 4.8.0 + 5.0.0 CFBundleSignature ???? CFBundleVersion $(CURRENT_PROJECT_VERSION) - NSPrincipalClass - diff --git a/Source/MultipartFormData.swift b/Source/MultipartFormData.swift index 057e68b97..9b258f756 100644 --- a/Source/MultipartFormData.swift +++ b/Source/MultipartFormData.swift @@ -71,7 +71,7 @@ open class MultipartFormData { boundaryText = "\(EncodingCharacters.crlf)--\(boundary)--\(EncodingCharacters.crlf)" } - return boundaryText.data(using: String.Encoding.utf8, allowLossyConversion: false)! + return Data(boundaryText.utf8) } } @@ -100,6 +100,7 @@ open class MultipartFormData { /// The boundary used to separate the body parts in the encoded form data. public let boundary: String + private let fileManager: FileManager private var bodyParts: [BodyPart] private var bodyPartError: AFError? private let streamBufferSize: Int @@ -109,8 +110,9 @@ open class MultipartFormData { /// Creates a multipart form data object. /// /// - returns: The multipart form data object. - public init() { - self.boundary = BoundaryGenerator.randomBoundary() + public init(fileManager: FileManager = .default, boundary: String? = nil) { + self.fileManager = fileManager + self.boundary = boundary ?? BoundaryGenerator.randomBoundary() self.bodyParts = [] /// @@ -257,7 +259,7 @@ open class MultipartFormData { var isDirectory: ObjCBool = false let path = fileURL.path - guard FileManager.default.fileExists(atPath: path, isDirectory: &isDirectory) && !isDirectory.boolValue else { + guard fileManager.fileExists(atPath: path, isDirectory: &isDirectory) && !isDirectory.boolValue else { setBodyPartError(withReason: .bodyPartFileIsDirectory(at: fileURL)) return } @@ -269,7 +271,7 @@ open class MultipartFormData { let bodyContentLength: UInt64 do { - guard let fileSize = try FileManager.default.attributesOfItem(atPath: path)[.size] as? NSNumber else { + guard let fileSize = try fileManager.attributesOfItem(atPath: path)[.size] as? NSNumber else { setBodyPartError(withReason: .bodyPartFileSizeNotAvailable(at: fileURL)) return } @@ -340,7 +342,7 @@ open class MultipartFormData { /// /// It is important to note that this method will load all the appended body parts into memory all at the same /// time. This method should only be used when the encoded data will have a small memory footprint. For large data - /// cases, please use the `writeEncodedDataToDisk(fileURL:completionHandler:)` method. + /// cases, please use the `writeEncodedData(to:))` method. /// /// - throws: An `AFError` if encoding encounters an error. /// @@ -376,7 +378,7 @@ open class MultipartFormData { throw bodyPartError } - if FileManager.default.fileExists(atPath: fileURL.path) { + if fileManager.fileExists(atPath: fileURL.path) { throw AFError.multipartEncodingFailed(reason: .outputStreamFileAlreadyExists(at: fileURL)) } else if !fileURL.isFileURL { throw AFError.multipartEncodingFailed(reason: .outputStreamURLInvalid(url: fileURL)) @@ -419,14 +421,11 @@ open class MultipartFormData { } private func encodeHeaders(for bodyPart: BodyPart) -> Data { - var headerText = "" + let headerText = bodyPart.headers.map { "\($0.name): \($0.value)\(EncodingCharacters.crlf)" } + .joined() + + EncodingCharacters.crlf - for (key, value) in bodyPart.headers { - headerText += "\(key): \(value)\(EncodingCharacters.crlf)" - } - headerText += EncodingCharacters.crlf - - return headerText.data(using: String.Encoding.utf8, allowLossyConversion: false)! + return Data(headerText.utf8) } private func encodeBodyStream(for bodyPart: BodyPart) throws -> Data { @@ -547,12 +546,12 @@ open class MultipartFormData { // MARK: - Private - Content Headers - private func contentHeaders(withName name: String, fileName: String? = nil, mimeType: String? = nil) -> [String: String] { + private func contentHeaders(withName name: String, fileName: String? = nil, mimeType: String? = nil) -> HTTPHeaders { var disposition = "form-data; name=\"\(name)\"" if let fileName = fileName { disposition += "; filename=\"\(fileName)\"" } - var headers = ["Content-Disposition": disposition] - if let mimeType = mimeType { headers["Content-Type"] = mimeType } + var headers: HTTPHeaders = [.contentDisposition(disposition)] + if let mimeType = mimeType { headers.add(.contentType(mimeType)) } return headers } diff --git a/Source/MultipartUpload.swift b/Source/MultipartUpload.swift new file mode 100644 index 000000000..7db69f468 --- /dev/null +++ b/Source/MultipartUpload.swift @@ -0,0 +1,93 @@ +// +// MultipartUpload.swift +// +// Copyright (c) 2014-2018 Alamofire Software Foundation (http://alamofire.org/) +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +import Foundation + +open class MultipartUpload { + /// Default memory threshold used when encoding `MultipartFormData`, in bytes. + public static let encodingMemoryThreshold: UInt64 = 10_000_000 + + lazy var result = Result { try build() } + + let isInBackgroundSession: Bool + let multipartBuilder: (MultipartFormData) -> Void + let encodingMemoryThreshold: UInt64 + let request: URLRequestConvertible + let fileManager: FileManager + + init(isInBackgroundSession: Bool, + encodingMemoryThreshold: UInt64 = MultipartUpload.encodingMemoryThreshold, + request: URLRequestConvertible, + fileManager: FileManager = .default, + multipartBuilder: @escaping (MultipartFormData) -> Void) { + self.isInBackgroundSession = isInBackgroundSession + self.encodingMemoryThreshold = encodingMemoryThreshold + self.request = request + self.fileManager = fileManager + self.multipartBuilder = multipartBuilder + } + + func build() throws -> (request: URLRequest, uploadable: UploadRequest.Uploadable) { + let formData = MultipartFormData(fileManager: fileManager) + multipartBuilder(formData) + + var urlRequest = try request.asURLRequest() + urlRequest.setValue(formData.contentType, forHTTPHeaderField: "Content-Type") + + let uploadable: UploadRequest.Uploadable + if formData.contentLength < encodingMemoryThreshold && !isInBackgroundSession { + let data = try formData.encode() + + uploadable = .data(data) + } else { + let tempDirectoryURL = fileManager.temporaryDirectory + let directoryURL = tempDirectoryURL.appendingPathComponent("org.alamofire.manager/multipart.form.data") + let fileName = UUID().uuidString + let fileURL = directoryURL.appendingPathComponent(fileName) + + try fileManager.createDirectory(at: directoryURL, withIntermediateDirectories: true, attributes: nil) + + do { + try formData.writeEncodedData(to: fileURL) + } catch { + // Cleanup after attempted write if it fails. + try? fileManager.removeItem(at: fileURL) + } + + uploadable = .file(fileURL, shouldRemove: true) + } + + return (request: urlRequest, uploadable: uploadable) + } +} + +extension MultipartUpload: UploadConvertible { + public func asURLRequest() throws -> URLRequest { + return try result.unwrap().request + } + + public func createUploadable() throws -> UploadRequest.Uploadable { + return try result.unwrap().uploadable + } +} diff --git a/Source/Notifications.swift b/Source/Notifications.swift index e1b612049..5e6a47a45 100644 --- a/Source/Notifications.swift +++ b/Source/Notifications.swift @@ -24,32 +24,67 @@ import Foundation -extension Notification.Name { - /// Used as a namespace for all `URLSessionTask` related notifications. - public struct Task { - /// Posted when a `URLSessionTask` is resumed. The notification `object` contains the resumed `URLSessionTask`. - public static let DidResume = Notification.Name(rawValue: "org.alamofire.notification.name.task.didResume") +public extension Request { + /// Posted when a `Request`'s task is resumed. The `Notification` contains the resumed `Request`. + static let didResume = Notification.Name(rawValue: "org.alamofire.notification.name.request.didResume") + /// Posted when a `Request`'s task is suspended. The `Notification` contains the suspended `Request`. + static let didSuspend = Notification.Name(rawValue: "org.alamofire.notification.name.request.didSuspend") + /// Posted when a `Request` is cancelled. The `Notification` contains the cancelled `Request`. + static let didCancel = Notification.Name(rawValue: "org.alamofire.notification.name.request.didCancel") + /// Posted when a `Request`'s task is completed. The `Notification` contains the completed `Request`. + static let didComplete = Notification.Name(rawValue: "org.alamofire.notification.name.request.didComplete") +} - /// Posted when a `URLSessionTask` is suspended. The notification `object` contains the suspended `URLSessionTask`. - public static let DidSuspend = Notification.Name(rawValue: "org.alamofire.notification.name.task.didSuspend") +// MARK: - - /// Posted when a `URLSessionTask` is cancelled. The notification `object` contains the cancelled `URLSessionTask`. - public static let DidCancel = Notification.Name(rawValue: "org.alamofire.notification.name.task.didCancel") +extension Notification { + /// The `Request` contained by the instance's `userInfo`, `nil` otherwise. + public var request: Request? { + return userInfo?[String.requestKey] as? Request + } - /// Posted when a `URLSessionTask` is completed. The notification `object` contains the completed `URLSessionTask`. - public static let DidComplete = Notification.Name(rawValue: "org.alamofire.notification.name.task.didComplete") + /// Convenience initializer for a `Notification` containing a `Request` payload. + /// + /// - Parameters: + /// - name: The name of the notification. + /// - request: The `Request` payload. + init(name: Notification.Name, request: Request) { + self.init(name: name, object: nil, userInfo: [String.requestKey: request]) } } -// MARK: - +extension NotificationCenter { + /// Convenience function for posting notifications with `Request` payloads. + /// + /// - Parameters: + /// - name: The name of the notification. + /// - request: The `Request` payload. + func postNotification(named name: Notification.Name, with request: Request) { + let notification = Notification(name: name, request: request) + post(notification) + } +} -extension Notification { - /// Used as a namespace for all `Notification` user info dictionary keys. - public struct Key { - /// User info dictionary key representing the `URLSessionTask` associated with the notification. - public static let Task = "org.alamofire.notification.key.task" +extension String { + /// User info dictionary key representing the `Request` associated with the notification. + fileprivate static let requestKey = "org.alamofire.notification.key.request" +} + +/// `EventMonitor` that provides Alamofire's notifications. +public final class AlamofireNotifications: EventMonitor { + public func request(_ request: Request, didCompleteTask task: URLSessionTask, with error: Error?) { + NotificationCenter.default.postNotification(named: Request.didComplete, with: request) + } + + public func requestDidResume(_ request: Request) { + NotificationCenter.default.postNotification(named: Request.didResume, with: request) + } + + public func requestDidSuspend(_ request: Request) { + NotificationCenter.default.postNotification(named: Request.didSuspend, with: request) + } - /// User info dictionary key representing the responseData associated with the notification. - public static let ResponseData = "org.alamofire.notification.key.responseData" + public func requestDidCancel(_ request: Request) { + NotificationCenter.default.postNotification(named: Request.didCancel, with: request) } } diff --git a/Source/OperationQueue+Alamofire.swift b/Source/OperationQueue+Alamofire.swift new file mode 100644 index 000000000..be630f5f7 --- /dev/null +++ b/Source/OperationQueue+Alamofire.swift @@ -0,0 +1,40 @@ +// +// OperationQueue+Alamofire.swift +// +// Copyright (c) 2014-2018 Alamofire Software Foundation (http://alamofire.org/) +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +import Foundation + +extension OperationQueue { + convenience init(qualityOfService: QualityOfService = .default, + maxConcurrentOperationCount: Int = OperationQueue.defaultMaxConcurrentOperationCount, + underlyingQueue: DispatchQueue? = nil, + name: String? = nil, + startSuspended: Bool = false) { + self.init() + self.qualityOfService = qualityOfService + self.maxConcurrentOperationCount = maxConcurrentOperationCount + self.underlyingQueue = underlyingQueue + self.name = name + self.isSuspended = startSuspended + } +} diff --git a/Source/ParameterEncoder.swift b/Source/ParameterEncoder.swift new file mode 100644 index 000000000..95975944d --- /dev/null +++ b/Source/ParameterEncoder.swift @@ -0,0 +1,813 @@ +// +// ParameterEncoder.swift +// +// Copyright (c) 2014-2018 Alamofire Software Foundation (http://alamofire.org/) +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +import Foundation + +/// A type that can encode any `Encodable` type into a `URLRequest`. +public protocol ParameterEncoder { + /// Encode the provided `Encodable` parameters into `request`. + /// + /// - Parameters: + /// - parameters: The `Encodable` parameter value. + /// - request: The `URLRequest` into which to encode the parameters. + /// - Returns: A `URLRequest` with the result of the encoding. + /// - Throws: An `Error` when encoding fails. For Alamofire provided encoders, this will be an instance of + /// `AFError.parameterEncoderFailed` with an associated `ParameterEncoderFailureReason`. + func encode(_ parameters: Parameters?, into request: URLRequest) throws -> URLRequest +} + +/// A `ParameterEncoder` that encodes types as JSON body data. +/// +/// If no `Content-Type` header is already set on the provided `URLRequest`s, it's set to `application/json`. +open class JSONParameterEncoder: ParameterEncoder { + /// Returns an encoder with default parameters. + public static var `default`: JSONParameterEncoder { return JSONParameterEncoder() } + + /// Returns an encoder with `JSONEncoder.outputFormatting` set to `.prettyPrinted`. + public static var prettyPrinted: JSONParameterEncoder { + let encoder = JSONEncoder() + encoder.outputFormatting = .prettyPrinted + + return JSONParameterEncoder(encoder: encoder) + } + + /// Returns an encoder with `JSONEncoder.outputFormatting` set to `.sortedKeys`. + @available(macOS 10.13, iOS 11.0, tvOS 11.0, watchOS 4.0, *) + public static var sortedKeys: JSONParameterEncoder { + let encoder = JSONEncoder() + encoder.outputFormatting = .sortedKeys + + return JSONParameterEncoder(encoder: encoder) + } + + /// `JSONEncoder` used to encode parameters. + public let encoder: JSONEncoder + + /// Creates an instance with the provided `JSONEncoder`. + /// + /// - Parameter encoder: The `JSONEncoder`. Defaults to `JSONEncoder()`. + public init(encoder: JSONEncoder = JSONEncoder()) { + self.encoder = encoder + } + + open func encode(_ parameters: Parameters?, + into request: URLRequest) throws -> URLRequest { + guard let parameters = parameters else { return request } + + var request = request + + do { + let data = try encoder.encode(parameters) + request.httpBody = data + if request.httpHeaders["Content-Type"] == nil { + request.httpHeaders.update(.contentType("application/json")) + } + } catch { + throw AFError.parameterEncodingFailed(reason: .jsonEncodingFailed(error: error)) + } + + return request + } +} + +/// A `ParameterEncoder` that encodes types as URL-encoded query strings to be set on the URL or as body data, depending +/// on the `Destination` set. +/// +/// If no `Content-Type` header is already set on the provided `URLRequest`s, it will be set to +/// `application/x-www-form-urlencoded; charset=utf-8`. +/// +/// There is no published specification for how to encode collection types. By default, the convention of appending +/// `[]` to the key for array values (`foo[]=1&foo[]=2`), and appending the key surrounded by square brackets for +/// nested dictionary values (`foo[bar]=baz`) is used. Optionally, `ArrayEncoding` can be used to omit the +/// square brackets appended to array keys. +/// +/// `BoolEncoding` can be used to configure how boolean values are encoded. The default behavior is to encode +/// `true` as 1 and `false` as 0. +open class URLEncodedFormParameterEncoder: ParameterEncoder { + /// Defines where the URL-encoded string should be set for each `URLRequest`. + public enum Destination { + /// Applies the encoded query string to any existing query string for `.get`, `.head`, and `.delete` request. + /// Sets it to the `httpBody` for all other methods. + case methodDependent + /// Applies the encoded query string to any existing query string from the `URLRequest`. + case queryString + /// Applies the encoded query string to the `httpBody` of the `URLRequest`. + case httpBody + + /// Determines whether the URL-encoded string should be applied to the `URLRequest`'s `url`. + /// + /// - Parameter method: The `HTTPMethod`. + /// - Returns: Whether the URL-encoded string should be applied to a `URL`. + func encodesParametersInURL(for method: HTTPMethod) -> Bool { + switch self { + case .methodDependent: return [.get, .head, .delete].contains(method) + case .queryString: return true + case .httpBody: return false + } + } + } + + /// Returns an encoder with default parameters. + public static var `default`: URLEncodedFormParameterEncoder { return URLEncodedFormParameterEncoder() } + + /// The `URLEncodedFormEncoder` to use. + public let encoder: URLEncodedFormEncoder + + /// The `Destination` for the URL-encoded string. + public let destination: Destination + + /// Creates an instance with the provided `URLEncodedFormEncoder` instance and `Destination` value. + /// + /// - Parameters: + /// - encoder: The `URLEncodedFormEncoder`. Defaults to `URLEncodedFormEncoder()`. + /// - destination: The `Destination`. Defaults to `.methodDependent`. + public init(encoder: URLEncodedFormEncoder = URLEncodedFormEncoder(), destination: Destination = .methodDependent) { + self.encoder = encoder + self.destination = destination + } + + open func encode(_ parameters: Parameters?, + into request: URLRequest) throws -> URLRequest { + guard let parameters = parameters else { return request } + + var request = request + + guard let url = request.url else { + throw AFError.parameterEncoderFailed(reason: .missingRequiredComponent(.url)) + } + + guard let rawMethod = request.httpMethod, let method = HTTPMethod(rawValue: rawMethod) else { + let rawValue = request.httpMethod ?? "nil" + throw AFError.parameterEncoderFailed(reason: .missingRequiredComponent(.httpMethod(rawValue: rawValue))) + } + + if destination.encodesParametersInURL(for: method), + var components = URLComponents(url: url, resolvingAgainstBaseURL: false) { + let query: String = try Result { try encoder.encode(parameters) } + .mapError { AFError.parameterEncoderFailed(reason: .encoderFailed(error: $0)) }.unwrap() + let newQueryString = [components.percentEncodedQuery, query].compactMap { $0 }.joinedWithAmpersands() + components.percentEncodedQuery = newQueryString + + guard let newURL = components.url else { + throw AFError.parameterEncoderFailed(reason: .missingRequiredComponent(.url)) + } + + request.url = newURL + } else { + if request.httpHeaders["Content-Type"] == nil { + request.httpHeaders.update(.contentType("application/x-www-form-urlencoded; charset=utf-8")) + } + + request.httpBody = try Result { try encoder.encode(parameters) } + .mapError { AFError.parameterEncoderFailed(reason: .encoderFailed(error: $0)) }.unwrap() + } + + return request + } +} + +/// An object that encodes instances into URL-encoded query strings. +/// +/// There is no published specification for how to encode collection types. By default, the convention of appending +/// `[]` to the key for array values (`foo[]=1&foo[]=2`), and appending the key surrounded by square brackets for +/// nested dictionary values (`foo[bar]=baz`) is used. Optionally, `ArrayEncoding` can be used to omit the +/// square brackets appended to array keys. +/// +/// `BoolEncoding` can be used to configure how `Bool` values are encoded. The default behavior is to encode +/// `true` as 1 and `false` as 0. +/// +/// `SpaceEncoding` can be used to configure how spaces are encoded. Modern encodings use percent replacement (%20), +/// while older encoding may expect spaces to be replaced with +. +/// +/// This type is largely based on Vapor's [`url-encoded-form`](https://github.com/vapor/url-encoded-form) project. +public final class URLEncodedFormEncoder { + /// Configures how `Bool` parameters are encoded. + public enum BoolEncoding { + /// Encodes `true` as `1`, `false` as `0`. + case numeric + /// Encodes `true` as "true", `false` as "false". + case literal + + /// Encodes the given `Bool` as a `String`. + /// + /// - Parameter value: The `Bool` to encode. + /// - Returns: The encoded `String`. + func encode(_ value: Bool) -> String { + switch self { + case .numeric: return value ? "1" : "0" + case .literal: return value ? "true" : "false" + } + } + } + + /// Configures how `Array` parameters are encoded. + public enum ArrayEncoding { + /// An empty set of square brackets ("[]") are sppended to the key for every value. + case brackets + /// No brackets are appended to the key and the key is encoded as is. + case noBrackets + + func encode(_ key: String) -> String { + switch self { + case .brackets: return "\(key)[]" + case .noBrackets: return key + } + } + } + + /// Configures how spaces are encoded. + public enum SpaceEncoding { + /// Encodes spaces according to normal percent escaping rules (%20). + case percentEscaped + /// Encodes spaces as `+`, + case plusReplaced + + /// Encodes the string according to the encoding. + /// + /// - Parameter string: The `String` to encode. + /// - Returns: The encoded `String`. + func encode(_ string: String) -> String { + switch self { + case .percentEscaped: return string.replacingOccurrences(of: " ", with: "%20") + case .plusReplaced: return string.replacingOccurrences(of: " ", with: "+") + } + } + } + + /// `URLEncodedFormEncoder` error. + public enum Error: Swift.Error { + /// An invalid root object was created by the encoder. Only keyed values are valid. + case invalidRootObject(String) + + var localizedDescription: String { + switch self { + case let .invalidRootObject(object): return "URLEncodedFormEncoder requires keyed root object. Received \(object) instead." + } + } + } + + /// The `ArrayEncoding` to use. + public let arrayEncoding: ArrayEncoding + /// The `BoolEncoding` to use. + public let boolEncoding: BoolEncoding + /// The `SpaceEncoding` to use. + public let spaceEncoding: SpaceEncoding + /// The `CharacterSet` of allowed characters. + public var allowedCharacters: CharacterSet + + /// Creates an instance from the supplied parameters. + /// + /// - Parameters: + /// - arrayEncoding: The `ArrayEncoding` instance. Defaults to `.brackets`. + /// - boolEncoding: The `BoolEncoding` instance. Defaults to `.numeric`. + /// - spaceEncoding: The `SpaceEncoding` instance. Defaults to `.percentEscaped`. + /// - allowedCharacters: The `CharacterSet` of allowed (non-escaped) characters. Defaults to `.afURLQueryAllowed`. + public init(arrayEncoding: ArrayEncoding = .brackets, + boolEncoding: BoolEncoding = .numeric, + spaceEncoding: SpaceEncoding = .percentEscaped, + allowedCharacters: CharacterSet = .afURLQueryAllowed) { + self.arrayEncoding = arrayEncoding + self.boolEncoding = boolEncoding + self.spaceEncoding = spaceEncoding + self.allowedCharacters = allowedCharacters + } + + func encode(_ value: Encodable) throws -> URLEncodedFormComponent { + let context = URLEncodedFormContext(.object([:])) + let encoder = _URLEncodedFormEncoder(context: context, boolEncoding: boolEncoding) + try value.encode(to: encoder) + + return context.component + } + + /// Encodes the `value` as a URL form encoded `String`. + /// + /// - Parameter value: The `Encodable` value.` + /// - Returns: The encoded `String`. + /// - Throws: An `Error` or `EncodingError` instance if encoding fails. + public func encode(_ value: Encodable) throws -> String { + let component: URLEncodedFormComponent = try encode(value) + + guard case let .object(object) = component else { + throw Error.invalidRootObject("\(component)") + } + + let serializer = URLEncodedFormSerializer(arrayEncoding: arrayEncoding, + spaceEncoding: spaceEncoding, + allowedCharacters: allowedCharacters) + let query = serializer.serialize(object) + + return query + } + + /// Encodes the value as `Data`. This is performed by first creating an encoded `String` and then returning the + /// `.utf8` data. + /// + /// - Parameter value: The `Encodable` value. + /// - Returns: The encoded `Data`. + /// - Throws: An `Error` or `EncodingError` instance if encoding fails. + public func encode(_ value: Encodable) throws -> Data { + let string: String = try encode(value) + + return Data(string.utf8) + } +} + +final class _URLEncodedFormEncoder { + var codingPath: [CodingKey] + // Returns an empty dictionary, as this encoder doesn't support userInfo. + var userInfo: [CodingUserInfoKey : Any] { return [:] } + + let context: URLEncodedFormContext + + private let boolEncoding: URLEncodedFormEncoder.BoolEncoding + + public init(context: URLEncodedFormContext, + codingPath: [CodingKey] = [], + boolEncoding: URLEncodedFormEncoder.BoolEncoding) { + self.context = context + self.codingPath = codingPath + self.boolEncoding = boolEncoding + } +} + +extension _URLEncodedFormEncoder: Encoder { + func container(keyedBy type: Key.Type) -> KeyedEncodingContainer where Key : CodingKey { + let container = _URLEncodedFormEncoder.KeyedContainer(context: context, + codingPath: codingPath, + boolEncoding: boolEncoding) + return KeyedEncodingContainer(container) + } + + func unkeyedContainer() -> UnkeyedEncodingContainer { + return _URLEncodedFormEncoder.UnkeyedContainer(context: context, + codingPath: codingPath, + boolEncoding: boolEncoding) + } + + func singleValueContainer() -> SingleValueEncodingContainer { + return _URLEncodedFormEncoder.SingleValueContainer(context: context, + codingPath: codingPath, + boolEncoding: boolEncoding) + } +} + +final class URLEncodedFormContext { + var component: URLEncodedFormComponent + + init(_ component: URLEncodedFormComponent) { + self.component = component + } +} + +enum URLEncodedFormComponent { + case string(String) + case array([URLEncodedFormComponent]) + case object([String: URLEncodedFormComponent]) + + /// Converts self to an `[URLEncodedFormData]` or returns `nil` if not convertible. + var array: [URLEncodedFormComponent]? { + switch self { + case let .array(array): return array + default: return nil + } + } + + /// Converts self to an `[String: URLEncodedFormData]` or returns `nil` if not convertible. + var object: [String: URLEncodedFormComponent]? { + switch self { + case let .object(object): return object + default: return nil + } + } + + /// Sets self to the supplied value at a given path. + /// + /// data.set(to: "hello", at: ["path", "to", "value"]) + /// + /// - parameters: + /// - value: Value of `Self` to set at the supplied path. + /// - path: `CodingKey` path to update with the supplied value. + public mutating func set(to value: URLEncodedFormComponent, at path: [CodingKey]) { + set(&self, to: value, at: path) + } + + /// Recursive backing method to `set(to:at:)`. + private func set(_ context: inout URLEncodedFormComponent, to value: URLEncodedFormComponent, at path: [CodingKey]) { + guard path.count >= 1 else { + context = value + return + } + + let end = path[0] + var child: URLEncodedFormComponent + switch path.count { + case 1: + child = value + case 2...: + if let index = end.intValue { + let array = context.array ?? [] + if array.count > index { + child = array[index] + } else { + child = .array([]) + } + set(&child, to: value, at: Array(path[1...])) + } else { + child = context.object?[end.stringValue] ?? .object([:]) + set(&child, to: value, at: Array(path[1...])) + } + default: fatalError("Unreachable") + } + + if let index = end.intValue { + if var array = context.array { + if array.count > index { + array[index] = child + } else { + array.append(child) + } + context = .array(array) + } else { + context = .array([child]) + } + } else { + if var object = context.object { + object[end.stringValue] = child + context = .object(object) + } else { + context = .object([end.stringValue: child]) + } + } + } +} + +struct AnyCodingKey: CodingKey, Hashable { + let stringValue: String + let intValue: Int? + + init?(stringValue: String) { + self.stringValue = stringValue + intValue = nil + } + + init?(intValue: Int) { + stringValue = "\(intValue)" + self.intValue = intValue + } + + init(_ base: Key) where Key : CodingKey { + if let intValue = base.intValue { + self.init(intValue: intValue)! + } else { + self.init(stringValue: base.stringValue)! + } + } +} + +extension _URLEncodedFormEncoder { + final class KeyedContainer where Key: CodingKey { + var codingPath: [CodingKey] + + private let context: URLEncodedFormContext + private let boolEncoding: URLEncodedFormEncoder.BoolEncoding + + init(context: URLEncodedFormContext, + codingPath: [CodingKey], + boolEncoding: URLEncodedFormEncoder.BoolEncoding) { + self.context = context + self.codingPath = codingPath + self.boolEncoding = boolEncoding + } + + private func nestedCodingPath(for key: CodingKey) -> [CodingKey] { + return codingPath + [key] + } + } +} + +extension _URLEncodedFormEncoder.KeyedContainer: KeyedEncodingContainerProtocol { + func encodeNil(forKey key: Key) throws { + let context = EncodingError.Context(codingPath: codingPath, + debugDescription: "URLEncodedFormEncoder cannot encode nil values.") + throw EncodingError.invalidValue("\(key): nil", context) + } + + func encode(_ value: T, forKey key: Key) throws where T : Encodable { + var container = nestedSingleValueEncoder(for: key) + try container.encode(value) + } + + func nestedSingleValueEncoder(for key: Key) -> SingleValueEncodingContainer { + let container = _URLEncodedFormEncoder.SingleValueContainer(context: context, + codingPath: nestedCodingPath(for: key), + boolEncoding: boolEncoding) + + return container + } + + func nestedUnkeyedContainer(forKey key: Key) -> UnkeyedEncodingContainer { + let container = _URLEncodedFormEncoder.UnkeyedContainer(context: context, + codingPath: nestedCodingPath(for: key), + boolEncoding: boolEncoding) + + return container + } + + func nestedContainer(keyedBy keyType: NestedKey.Type, forKey key: Key) -> KeyedEncodingContainer where NestedKey : CodingKey { + let container = _URLEncodedFormEncoder.KeyedContainer(context: context, + codingPath: nestedCodingPath(for: key), + boolEncoding: boolEncoding) + + return KeyedEncodingContainer(container) + } + + func superEncoder() -> Encoder { + return _URLEncodedFormEncoder(context: context, codingPath: codingPath, boolEncoding: boolEncoding) + } + + func superEncoder(forKey key: Key) -> Encoder { + return _URLEncodedFormEncoder(context: context, codingPath: nestedCodingPath(for: key), boolEncoding: boolEncoding) + } +} + +extension _URLEncodedFormEncoder { + final class SingleValueContainer { + var codingPath: [CodingKey] + + private var canEncodeNewValue = true + + private let context: URLEncodedFormContext + private let boolEncoding: URLEncodedFormEncoder.BoolEncoding + + init(context: URLEncodedFormContext, codingPath: [CodingKey], boolEncoding: URLEncodedFormEncoder.BoolEncoding) { + self.context = context + self.codingPath = codingPath + self.boolEncoding = boolEncoding + } + + private func checkCanEncode(value: Any?) throws { + guard canEncodeNewValue else { + let context = EncodingError.Context(codingPath: codingPath, + debugDescription: "Attempt to encode value through single value container when previously value already encoded.") + throw EncodingError.invalidValue(value as Any, context) + } + } + } +} + +extension _URLEncodedFormEncoder.SingleValueContainer: SingleValueEncodingContainer { + func encodeNil() throws { + try checkCanEncode(value: nil) + defer { canEncodeNewValue = false } + + let context = EncodingError.Context(codingPath: codingPath, + debugDescription: "URLEncodedFormEncoder cannot encode nil values.") + throw EncodingError.invalidValue("nil", context) + } + + func encode(_ value: Bool) throws { + try encode(value, as: String(boolEncoding.encode(value))) + } + + func encode(_ value: String) throws { + try encode(value, as: value) + } + + func encode(_ value: Double) throws { + try encode(value, as: String(value)) + } + + func encode(_ value: Float) throws { + try encode(value, as: String(value)) + } + + func encode(_ value: Int) throws { + try encode(value, as: String(value)) + } + + func encode(_ value: Int8) throws { + try encode(value, as: String(value)) + } + + func encode(_ value: Int16) throws { + try encode(value, as: String(value)) + } + + func encode(_ value: Int32) throws { + try encode(value, as: String(value)) + } + + func encode(_ value: Int64) throws { + try encode(value, as: String(value)) + } + + func encode(_ value: UInt) throws { + try encode(value, as: String(value)) + } + + func encode(_ value: UInt8) throws { + try encode(value, as: String(value)) + } + + func encode(_ value: UInt16) throws { + try encode(value, as: String(value)) + } + + func encode(_ value: UInt32) throws { + try encode(value, as: String(value)) + } + + func encode(_ value: UInt64) throws { + try encode(value, as: String(value)) + } + + private func encode(_ value: T, as string: String) throws where T : Encodable { + try checkCanEncode(value: value) + defer { canEncodeNewValue = false } + + context.component.set(to: .string(string), at: codingPath) + } + + func encode(_ value: T) throws where T : Encodable { + try checkCanEncode(value: value) + defer { canEncodeNewValue = false } + + let encoder = _URLEncodedFormEncoder(context: context, + codingPath: codingPath, + boolEncoding: boolEncoding) + try value.encode(to: encoder) + } +} + +extension _URLEncodedFormEncoder { + final class UnkeyedContainer { + var codingPath: [CodingKey] + + var count = 0 + var nestedCodingPath: [CodingKey] { + return codingPath + [AnyCodingKey(intValue: count)!] + } + + private let context: URLEncodedFormContext + private let boolEncoding: URLEncodedFormEncoder.BoolEncoding + + init(context: URLEncodedFormContext, + codingPath: [CodingKey], + boolEncoding: URLEncodedFormEncoder.BoolEncoding) { + self.context = context + self.codingPath = codingPath + self.boolEncoding = boolEncoding + } + } +} + +extension _URLEncodedFormEncoder.UnkeyedContainer: UnkeyedEncodingContainer { + func encodeNil() throws { + let context = EncodingError.Context(codingPath: codingPath, + debugDescription: "URLEncodedFormEncoder cannot encode nil values.") + throw EncodingError.invalidValue("nil", context) + } + + func encode(_ value: T) throws where T : Encodable { + var container = nestedSingleValueContainer() + try container.encode(value) + } + + func nestedSingleValueContainer() -> SingleValueEncodingContainer { + defer { count += 1 } + + return _URLEncodedFormEncoder.SingleValueContainer(context: context, + codingPath: nestedCodingPath, + boolEncoding: boolEncoding) + } + + func nestedContainer(keyedBy keyType: NestedKey.Type) -> KeyedEncodingContainer where NestedKey : CodingKey { + defer { count += 1 } + let container = _URLEncodedFormEncoder.KeyedContainer(context: context, + codingPath: nestedCodingPath, + boolEncoding: boolEncoding) + + return KeyedEncodingContainer(container) + } + + func nestedUnkeyedContainer() -> UnkeyedEncodingContainer { + defer { count += 1 } + + return _URLEncodedFormEncoder.UnkeyedContainer(context: context, + codingPath: nestedCodingPath, + boolEncoding: boolEncoding) + } + + func superEncoder() -> Encoder { + defer { count += 1 } + + return _URLEncodedFormEncoder(context: context, codingPath: codingPath, boolEncoding: boolEncoding) + } +} + +final class URLEncodedFormSerializer { + let arrayEncoding: URLEncodedFormEncoder.ArrayEncoding + let spaceEncoding: URLEncodedFormEncoder.SpaceEncoding + let allowedCharacters: CharacterSet + + init(arrayEncoding: URLEncodedFormEncoder.ArrayEncoding, + spaceEncoding: URLEncodedFormEncoder.SpaceEncoding, + allowedCharacters: CharacterSet) { + self.arrayEncoding = arrayEncoding + self.spaceEncoding = spaceEncoding + self.allowedCharacters = allowedCharacters + } + + func serialize(_ object: [String: URLEncodedFormComponent]) -> String { + var output: [String] = [] + for (key, component) in object { + let value = serialize(component, forKey: key) + output.append(value) + } + + return output.joinedWithAmpersands() + } + + func serialize(_ component: URLEncodedFormComponent, forKey key: String) -> String { + switch component { + case let .string(string): return "\(escape(key))=\(escape(string))" + case let .array(array): return serialize(array, forKey: key) + case let .object(dictionary): return serialize(dictionary, forKey: key) + } + } + + func serialize(_ object: [String: URLEncodedFormComponent], forKey key: String) -> String { + let segments: [String] = object.map { (subKey, value) in + let keyPath = "[\(subKey)]" + return serialize(value, forKey: key + keyPath) + } + + return segments.joinedWithAmpersands() + } + + func serialize(_ array: [URLEncodedFormComponent], forKey key: String) -> String { + let segments: [String] = array.map { (component) in + let keyPath = arrayEncoding.encode(key) + return serialize(component, forKey: keyPath) + } + + return segments.joinedWithAmpersands() + } + + func escape(_ query: String) -> String { + var allowedCharactersWithSpace = allowedCharacters + allowedCharactersWithSpace.insert(charactersIn: " ") + let escapedQuery = query.addingPercentEncoding(withAllowedCharacters: allowedCharactersWithSpace) ?? query + let spaceEncodedQuery = spaceEncoding.encode(escapedQuery) + + return spaceEncodedQuery + } +} + +extension Array where Element == String { + func joinedWithAmpersands() -> String { + return joined(separator: "&") + } +} + +extension CharacterSet { + /// Creates a CharacterSet from RFC 3986 allowed characters. + /// + /// RFC 3986 states that the following characters are "reserved" characters. + /// + /// - General Delimiters: ":", "#", "[", "]", "@", "?", "/" + /// - Sub-Delimiters: "!", "$", "&", "'", "(", ")", "*", "+", ",", ";", "=" + /// + /// In RFC 3986 - Section 3.4, it states that the "?" and "/" characters should not be escaped to allow + /// query strings to include a URL. Therefore, all "reserved" characters with the exception of "?" and "/" + /// should be percent-escaped in the query string. + public static let afURLQueryAllowed: CharacterSet = { + let generalDelimitersToEncode = ":#[]@" // does not include "?" or "/" due to RFC 3986 - Section 3.4 + let subDelimitersToEncode = "!$&'()*+,;=" + let encodableDelimiters = CharacterSet(charactersIn: "\(generalDelimitersToEncode)\(subDelimitersToEncode)") + + return CharacterSet.urlQueryAllowed.subtracting(encodableDelimiters) + }() +} diff --git a/Source/ParameterEncoding.swift b/Source/ParameterEncoding.swift index 4a54f2dd0..c155e62aa 100644 --- a/Source/ParameterEncoding.swift +++ b/Source/ParameterEncoding.swift @@ -24,23 +24,6 @@ import Foundation -/// HTTP method definitions. -/// -/// See https://tools.ietf.org/html/rfc7231#section-4.3 -public enum HTTPMethod: String { - case options = "OPTIONS" - case get = "GET" - case head = "HEAD" - case post = "POST" - case put = "PUT" - case patch = "PATCH" - case delete = "DELETE" - case trace = "TRACE" - case connect = "CONNECT" -} - -// MARK: - - /// A dictionary of parameters to apply to a `URLRequest`. public typealias Parameters = [String: Any] @@ -191,7 +174,7 @@ public struct URLEncoding: ParameterEncoding { urlRequest.setValue("application/x-www-form-urlencoded; charset=utf-8", forHTTPHeaderField: "Content-Type") } - urlRequest.httpBody = query(parameters).data(using: .utf8, allowLossyConversion: false) + urlRequest.httpBody = Data(query(parameters).utf8) } return urlRequest @@ -231,58 +214,11 @@ public struct URLEncoding: ParameterEncoding { /// Returns a percent-escaped string following RFC 3986 for a query string key or value. /// - /// RFC 3986 states that the following characters are "reserved" characters. - /// - /// - General Delimiters: ":", "#", "[", "]", "@", "?", "/" - /// - Sub-Delimiters: "!", "$", "&", "'", "(", ")", "*", "+", ",", ";", "=" - /// - /// In RFC 3986 - Section 3.4, it states that the "?" and "/" characters should not be escaped to allow - /// query strings to include a URL. Therefore, all "reserved" characters with the exception of "?" and "/" - /// should be percent-escaped in the query string. - /// /// - parameter string: The string to be percent-escaped. /// /// - returns: The percent-escaped string. public func escape(_ string: String) -> String { - let generalDelimitersToEncode = ":#[]@" // does not include "?" or "/" due to RFC 3986 - Section 3.4 - let subDelimitersToEncode = "!$&'()*+,;=" - - var allowedCharacterSet = CharacterSet.urlQueryAllowed - allowedCharacterSet.remove(charactersIn: "\(generalDelimitersToEncode)\(subDelimitersToEncode)") - - var escaped = "" - - //========================================================================================================== - // - // Batching is required for escaping due to an internal bug in iOS 8.1 and 8.2. Encoding more than a few - // hundred Chinese characters causes various malloc error crashes. To avoid this issue until iOS 8 is no - // longer supported, batching MUST be used for encoding. This introduces roughly a 20% overhead. For more - // info, please refer to: - // - // - https://github.com/Alamofire/Alamofire/issues/206 - // - //========================================================================================================== - - if #available(iOS 8.3, *) { - escaped = string.addingPercentEncoding(withAllowedCharacters: allowedCharacterSet) ?? string - } else { - let batchSize = 50 - var index = string.startIndex - - while index != string.endIndex { - let startIndex = index - let endIndex = string.index(index, offsetBy: batchSize, limitedBy: string.endIndex) ?? string.endIndex - let range = startIndex.. String { @@ -403,81 +339,6 @@ public struct JSONEncoding: ParameterEncoding { // MARK: - -/// Uses `PropertyListSerialization` to create a plist representation of the parameters object, according to the -/// associated format and write options values, which is set as the body of the request. The `Content-Type` HTTP header -/// field of an encoded request is set to `application/x-plist`. -public struct PropertyListEncoding: ParameterEncoding { - - // MARK: Properties - - /// Returns a default `PropertyListEncoding` instance. - public static var `default`: PropertyListEncoding { return PropertyListEncoding() } - - /// Returns a `PropertyListEncoding` instance with xml formatting and default writing options. - public static var xml: PropertyListEncoding { return PropertyListEncoding(format: .xml) } - - /// Returns a `PropertyListEncoding` instance with binary formatting and default writing options. - public static var binary: PropertyListEncoding { return PropertyListEncoding(format: .binary) } - - /// The property list serialization format. - public let format: PropertyListSerialization.PropertyListFormat - - /// The options for writing the parameters as plist data. - public let options: PropertyListSerialization.WriteOptions - - // MARK: Initialization - - /// Creates a `PropertyListEncoding` instance using the specified format and options. - /// - /// - parameter format: The property list serialization format. - /// - parameter options: The options for writing the parameters as plist data. - /// - /// - returns: The new `PropertyListEncoding` instance. - public init( - format: PropertyListSerialization.PropertyListFormat = .xml, - options: PropertyListSerialization.WriteOptions = 0) - { - self.format = format - self.options = options - } - - // MARK: Encoding - - /// Creates a URL request by encoding parameters and applying them onto an existing request. - /// - /// - parameter urlRequest: The request to have parameters applied. - /// - parameter parameters: The parameters to apply. - /// - /// - throws: An `Error` if the encoding process encounters an error. - /// - /// - returns: The encoded request. - public func encode(_ urlRequest: URLRequestConvertible, with parameters: Parameters?) throws -> URLRequest { - var urlRequest = try urlRequest.asURLRequest() - - guard let parameters = parameters else { return urlRequest } - - do { - let data = try PropertyListSerialization.data( - fromPropertyList: parameters, - format: format, - options: options - ) - - if urlRequest.value(forHTTPHeaderField: "Content-Type") == nil { - urlRequest.setValue("application/x-plist", forHTTPHeaderField: "Content-Type") - } - - urlRequest.httpBody = data - } catch { - throw AFError.parameterEncodingFailed(reason: .propertyListEncodingFailed(error: error)) - } - - return urlRequest - } -} - -// MARK: - - extension NSNumber { fileprivate var isBool: Bool { return CFBooleanGetTypeID() == CFGetTypeID(self) } } diff --git a/Source/Protector.swift b/Source/Protector.swift new file mode 100644 index 000000000..0fbedb752 --- /dev/null +++ b/Source/Protector.swift @@ -0,0 +1,130 @@ +// +// Protector.swift +// +// Copyright (c) 2014-2018 Alamofire Software Foundation (http://alamofire.org/) +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +import Foundation + +// MARK: - + +/// An `os_unfair_lock` wrapper. +final class UnfairLock { + private var unfairLock = os_unfair_lock() + + fileprivate func lock() { + os_unfair_lock_lock(&unfairLock) + } + + fileprivate func unlock() { + os_unfair_lock_unlock(&unfairLock) + } + + /// Executes a closure returning a value while acquiring the lock. + /// + /// - Parameter closure: The closure to run. + /// - Returns: The value the closure generated. + func around(_ closure: () -> T) -> T { + lock(); defer { unlock() } + return closure() + } + + /// Execute a closure while aquiring the lock. + /// + /// - Parameter closure: The closure to run. + func around(_ closure: () -> Void) { + lock(); defer { unlock() } + return closure() + } +} + +/// A thread-safe wrapper around a value. +final class Protector { + private let lock = UnfairLock() + private var value: T + + init(_ value: T) { + self.value = value + } + + /// The contained value. Unsafe for anything more than direct read or write. + var directValue: T { + get { return lock.around { value } } + set { lock.around { value = newValue } } + } + + /// Synchronously read or transform the contained value. + /// + /// - Parameter closure: The closure to execute. + /// - Returns: The return value of the closure passed. + func read(_ closure: (T) -> U) -> U { + return lock.around { closure(self.value) } + } + + /// Synchronously modify the protected value. + /// + /// - Parameter closure: The closure to execute. + /// - Returns: The modified value. + @discardableResult + func write(_ closure: (inout T) -> U) -> U { + return lock.around { closure(&self.value) } + } +} + +extension Protector where T: RangeReplaceableCollection { + /// Adds a new element to the end of this protected collection. + /// + /// - Parameter newElement: The `Element` to append. + func append(_ newElement: T.Element) { + write { (ward: inout T) in + ward.append(newElement) + } + } + + /// Adds the elements of a sequence to the end of this protected collection. + /// + /// - Parameter newElements: The `Sequence` to append. + func append(contentsOf newElements: S) where S.Element == T.Element { + write { (ward: inout T) in + ward.append(contentsOf: newElements) + } + } + + /// Add the elements of a collection to the end of the protected collection. + /// + /// - Parameter newElements: The `Collection` to append. + func append(contentsOf newElements: C) where C.Element == T.Element { + write { (ward: inout T) in + ward.append(contentsOf: newElements) + } + } +} + +extension Protector where T == Data? { + /// Adds the contents of a `Data` value to the end of the protected `Data`. + /// + /// - Parameter data: The `Data` to be appended. + func append(_ data: Data) { + write { (ward: inout T) in + ward?.append(data) + } + } +} diff --git a/Source/Request.swift b/Source/Request.swift index bf569f4ca..3a5d81607 100644 --- a/Source/Request.swift +++ b/Source/Request.swift @@ -24,265 +24,516 @@ import Foundation -/// A type that can inspect and optionally adapt a `URLRequest` in some manner if necessary. -public protocol RequestAdapter { - /// Inspects and adapts the specified `URLRequest` in some manner if necessary and returns the result. +/// `Request` is the common superclass of all Alamofire request types and provides common state, delegate, and callback +/// handling. +open class Request { + /// State of the `Request`, with managed transitions between states set when calling `resume()`, `suspend()`, or + /// `cancel()` on the `Request`. /// - /// - parameter urlRequest: The URL request to adapt. + /// - initialized: Initial state of the `Request`. + /// - resumed: Set when `resume()` is called. Any tasks created for the `Request` will have `resume()` called on + /// them in this state. + /// - suspended: Set when `suspend()` is called. Any tasks created for the `Request` will have `suspend()` called on + /// them in this state. + /// - cancelled: Set when `cancel()` is called. Any tasks created for the `Request` will have `cancel()` called on + /// them. Unlike `resumed` or `suspended`, once in the `cancelled` state, the `Request` can no longer + /// transition to any other state. + public enum State { + case initialized, resumed, suspended, cancelled + + /// Determines whether `self` can be transitioned to `state`. + func canTransitionTo(_ state: State) -> Bool { + switch (self, state) { + case (.initialized, _): return true + case (_, .initialized), (.cancelled, _): return false + case (.resumed, .cancelled), (.suspended, .cancelled), + (.resumed, .suspended), (.suspended, .resumed): return true + case (.suspended, .suspended), (.resumed, .resumed): return false + } + } + } + + // MARK: - Initial State + + /// `UUID` prividing a unique identifier for the `Request`, used in the `Hashable` and `Equatable` conformances. + public let id: UUID + /// The serial queue for all internal async actions. + public let underlyingQueue: DispatchQueue + /// The queue used for all serialization actions. By default it's a serial queue that targets `underlyingQueue`. + public let serializationQueue: DispatchQueue + /// `EventMonitor` used for event callbacks. + public let eventMonitor: EventMonitor? + /// The `Request`'s delegate. + public weak var delegate: RequestDelegate? + + /// `OperationQueue` used internally to enqueue response callbacks. Starts suspended but is activated when the + /// `Request` is finished. + let internalQueue: OperationQueue + + // MARK: - Updated State + + /// Type encapsulating all mutable state that may need to be accessed from anything other than the `underlyingQueue`. + private struct MutableState { + /// State of the `Request`. + var state: State = .initialized + /// `ProgressHandler` and `DispatchQueue` provided for upload progress callbacks. + var uploadProgressHandler: (handler: ProgressHandler, queue: DispatchQueue)? + /// `ProgressHandler` and `DispatchQueue` provided for download progress callbacks. + var downloadProgressHandler: (handler: ProgressHandler, queue: DispatchQueue)? + /// `URLCredential` used for authentication challenges. + var credential: URLCredential? + /// All `URLRequest`s created by Alamofire on behalf of the `Request`. + var requests: [URLRequest] = [] + /// All `URLSessionTask`s created by Alamofire on behalf of the `Request`. + var tasks: [URLSessionTask] = [] + /// All `URLSessionTaskMetrics` values gathered by Alamofire on behalf of the `Request`. Should correspond + /// exactly the the `tasks` created. + var metrics: [URLSessionTaskMetrics] = [] + /// Number of times any retriers provided retried the `Request`. + var retryCount = 0 + /// Final `Error` for the `Request`, whether from various internal Alamofire calls or as a result of a `task`. + var error: Error? + } + + /// Protected `MutableState` value that provides threadsafe access to state values. + private let protectedMutableState: Protector = Protector(MutableState()) + + /// `State` of the `Request`. + public fileprivate(set) var state: State { + get { return protectedMutableState.directValue.state } + set { protectedMutableState.write { $0.state = newValue } } + } + /// Returns whether `state` is `.cancelled`. + public var isCancelled: Bool { return state == .cancelled } + /// Returns whether `state is `.resumed`. + public var isResumed: Bool { return state == .resumed } + /// Returns whether `state` is `.suspended`. + public var isSuspended: Bool { return state == .suspended } + /// Returns whether `state` is `.initialized`. + public var isInitialized: Bool { return state == .initialized } + + // Progress + + /// Closure type executed when monitoring the upload or download progress of a request. + public typealias ProgressHandler = (Progress) -> Void + + /// `Progress` of the upload of the body of the executed `URLRequest`. Reset to `0` if the `Request` is retried. + public let uploadProgress = Progress(totalUnitCount: 0) + /// `Progress` of the download of any response data. Reset to `0` if the `Request` is retried. + public let downloadProgress = Progress(totalUnitCount: 0) + /// `ProgressHandler` called when `uploadProgress` is updated, on the provided `DispatchQueue`. + fileprivate var uploadProgressHandler: (handler: ProgressHandler, queue: DispatchQueue)? { + get { return protectedMutableState.directValue.uploadProgressHandler } + set { protectedMutableState.write { $0.uploadProgressHandler = newValue } } + } + /// `ProgressHandler` called when `downloadProgress` is updated, on the provided `DispatchQueue`. + fileprivate var downloadProgressHandler: (handler: ProgressHandler, queue: DispatchQueue)? { + get { return protectedMutableState.directValue.downloadProgressHandler } + set { protectedMutableState.write { $0.downloadProgressHandler = newValue } } + } + + // Credential + + /// `URLCredential` used for authentication challenges. Created by calling one of the `authenticate` methods. + public private(set) var credential: URLCredential? { + get { return protectedMutableState.directValue.credential } + set { protectedMutableState.write { $0.credential = newValue } } + } + + // Validators + + /// `Validator` callback closures that store the validation calls enqueued. + fileprivate var protectedValidators: Protector<[() -> Void]> = Protector([]) + + // Requests + + /// All `URLRequests` created on behalf of the `Request`, including original and adapted requests. + public var requests: [URLRequest] { return protectedMutableState.directValue.requests } + /// First `URLRequest` created on behalf of the `Request`. May not be the first one actually executed. + public var firstRequest: URLRequest? { return requests.first } + /// Last `URLRequest` created on behalf of the `Request`. + public var lastRequest: URLRequest? { return requests.last } + /// Current `URLRequest` created on behalf of the `Request`. + public var request: URLRequest? { return lastRequest } + + /// `URLRequest`s from all of the `URLSessionTask`s executed on behalf of the `Request`. + public var performedRequests: [URLRequest] { + return protectedMutableState.read { $0.tasks.compactMap { $0.currentRequest } } + } + + // Response + + /// `HTTPURLResponse` received from the server, if any. If the `Request` was retried, this is the response of the + /// last `URLSessionTask`. + public var response: HTTPURLResponse? { return lastTask?.response as? HTTPURLResponse } + + // Tasks + + /// All `URLSessionTask`s created on behalf of the `Request`. + public var tasks: [URLSessionTask] { return protectedMutableState.directValue.tasks } + /// First `URLSessionTask` created on behalf of the `Request`. + public var firstTask: URLSessionTask? { return tasks.first } + /// Last `URLSessionTask` crated on behalf of the `Request`. + public var lastTask: URLSessionTask? { return tasks.last } + /// Current `URLSessionTask` created on behalf of the `Request`. + public var task: URLSessionTask? { return lastTask } + + // Metrics + + /// All `URLSessionTaskMetrics` gathered on behalf of the `Request`. Should correspond to the `tasks` created. + public var allMetrics: [URLSessionTaskMetrics] { return protectedMutableState.directValue.metrics } + /// First `URLSessionTaskMetrics` gathered on behalf of the `Request`. + public var firstMetrics: URLSessionTaskMetrics? { return allMetrics.first } + /// Last `URLSessionTaskMetrics` gathered on behalf of the `Request`. + public var lastMetrics: URLSessionTaskMetrics? { return allMetrics.last } + /// Current `URLSessionTaskMetrics` gathered on behalf of the `Request`. + public var metrics: URLSessionTaskMetrics? { return lastMetrics } + + /// Number of times the `Request` has been retried. + public var retryCount: Int { return protectedMutableState.directValue.retryCount } + + /// `Error` returned from Alamofire internally, from the network request directly, or any validators executed. + fileprivate(set) public var error: Error? { + get { return protectedMutableState.directValue.error } + set { protectedMutableState.write { $0.error = newValue } } + } + + + /// Default initializer for the `Request` superclass. /// - /// - throws: An `Error` if the adaptation encounters an error. + /// - Parameters: + /// - id: `UUID` used for the `Hashable` and `Equatable` implementations. Defaults to a random `UUID`. + /// - underlyingQueue: `DispatchQueue` on which all internal `Request` work is performed. + /// - serializationQueue: `DispatchQueue` on which all serialization work is performed. Targets the + /// `underlyingQueue` when created by a `SessionManager`. + /// - eventMonitor: `EventMonitor` used for event callbacks from internal `Request` actions. + /// - delegate: `RequestDelegate` that provides an interface to actions not performed by the `Request`. + public init(id: UUID = UUID(), + underlyingQueue: DispatchQueue, + serializationQueue: DispatchQueue, + eventMonitor: EventMonitor?, + delegate: RequestDelegate) { + self.id = id + self.underlyingQueue = underlyingQueue + self.serializationQueue = serializationQueue + internalQueue = OperationQueue(maxConcurrentOperationCount: 1, + underlyingQueue: underlyingQueue, + name: "org.alamofire.request-\(id)", + startSuspended: true) + self.eventMonitor = eventMonitor + self.delegate = delegate + } + + // MARK: - Internal API + // Called from underlyingQueue. + + /// Called when a `URLRequest` has been created on behalf of the `Request`. /// - /// - returns: The adapted `URLRequest`. - func adapt(_ urlRequest: URLRequest) throws -> URLRequest -} + /// - Parameter request: `URLRequest` created. + func didCreateURLRequest(_ request: URLRequest) { + protectedMutableState.write { $0.requests.append(request) } -// MARK: - + eventMonitor?.request(self, didCreateURLRequest: request) + } + + /// Called when initial `URLRequest` creation has failed, typically through a `URLRequestConvertible`. Triggers retry. + /// + /// - Parameter error: `Error` thrown from the failed creation. + func didFailToCreateURLRequest(with error: Error) { + self.error = error + + eventMonitor?.request(self, didFailToCreateURLRequestWithError: error) -/// A closure executed when the `RequestRetrier` determines whether a `Request` should be retried or not. -public typealias RequestRetryCompletion = (_ shouldRetry: Bool, _ timeDelay: TimeInterval) -> Void + retryOrFinish(error: error) + } -/// A type that determines whether a request should be retried after being executed by the specified session manager -/// and encountering an error. -public protocol RequestRetrier { - /// Determines whether the `Request` should be retried by calling the `completion` closure. + /// Called when a `RequestAdapter` has successfully adapted a `URLRequest`. /// - /// This operation is fully asynchronous. Any amount of time can be taken to determine whether the request needs - /// to be retried. The one requirement is that the completion closure is called to ensure the request is properly - /// cleaned up after. + /// - Parameters: + /// - initialRequest: The `URLRequest` that was adapted. + /// - adaptedRequest: The `URLRequest` returned by the `RequestAdapter`. + func didAdaptInitialRequest(_ initialRequest: URLRequest, to adaptedRequest: URLRequest) { + protectedMutableState.write { $0.requests.append(adaptedRequest) } + + eventMonitor?.request(self, didAdaptInitialRequest: initialRequest, to: adaptedRequest) + } + + /// Called when a `RequestAdapter` fails to adapt a `URLRequest`. Triggers retry. /// - /// - parameter manager: The session manager the request was executed on. - /// - parameter request: The request that failed due to the encountered error. - /// - parameter error: The error encountered when executing the request. - /// - parameter completion: The completion closure to be executed when retry decision has been determined. - func should(_ manager: SessionManager, retry request: Request, with error: Error, completion: @escaping RequestRetryCompletion) -} + /// - Parameters: + /// - request: The `URLRequest` the adapter was called with. + /// - error: The `Error` returned by the `RequestAdapter`. + func didFailToAdaptURLRequest(_ request: URLRequest, withError error: Error) { + self.error = error -// MARK: - + eventMonitor?.request(self, didFailToAdaptURLRequest: request, withError: error) -protocol TaskConvertible { - func task(session: URLSession, adapter: RequestAdapter?, queue: DispatchQueue) throws -> URLSessionTask -} + retryOrFinish(error: error) + } -/// A dictionary of headers to apply to a `URLRequest`. -public typealias HTTPHeaders = [String: String] + /// Called when a `URLSessionTask` is created on behalf of the `Request`. Calls `reset()`. + /// + /// - Parameter task: The `URLSessionTask` created. + func didCreateTask(_ task: URLSessionTask) { + protectedMutableState.write { $0.tasks.append(task) } -// MARK: - + reset() -/// Responsible for sending a request and receiving the response and associated data from the server, as well as -/// managing its underlying `URLSessionTask`. -open class Request { + eventMonitor?.request(self, didCreateTask: task) + } + + /// Called when resumption is completed. + func didResume() { + eventMonitor?.requestDidResume(self) + } - // MARK: Helper Types + /// Called when suspension is completed. + func didSuspend() { + eventMonitor?.requestDidSuspend(self) + } - /// A closure executed when monitoring upload or download progress of a request. - public typealias ProgressHandler = (Progress) -> Void + /// Called when cancellation is completed, sets `error` to `AFError.explicitlyCancelled`. + func didCancel() { + error = AFError.explicitlyCancelled - enum RequestTask { - case data(TaskConvertible?, URLSessionTask?) - case download(TaskConvertible?, URLSessionTask?) - case upload(TaskConvertible?, URLSessionTask?) - case stream(TaskConvertible?, URLSessionTask?) + eventMonitor?.requestDidCancel(self) } - // MARK: Properties + /// Called when a `URLSessionTaskMetrics` value is gathered on behalf of the `Request`. + func didGatherMetrics(_ metrics: URLSessionTaskMetrics) { + protectedMutableState.write { $0.metrics.append(metrics) } - /// The delegate for the underlying task. - open internal(set) var delegate: TaskDelegate { - get { - taskDelegateLock.lock() ; defer { taskDelegateLock.unlock() } - return taskDelegate - } - set { - taskDelegateLock.lock() ; defer { taskDelegateLock.unlock() } - taskDelegate = newValue - } + eventMonitor?.request(self, didGatherMetrics: metrics) } - /// The underlying task. - open var task: URLSessionTask? { return delegate.task } + /// Called when a `URLSessionTask` fails before it is finished, typically during certificate pinning. + func didFailTask(_ task: URLSessionTask, earlyWithError error: Error) { + self.error = error - /// The session belonging to the underlying task. - public let session: URLSession + // Task will still complete, so didCompleteTask(_:with:) will handle retry. + eventMonitor?.request(self, didFailTask: task, earlyWithError: error) + } - /// The request sent or to be sent to the server. - open var request: URLRequest? { return task?.originalRequest } + /// Called when a `URLSessionTask` completes. All tasks will eventually call this method. + func didCompleteTask(_ task: URLSessionTask, with error: Error?) { + self.error = self.error ?? error + protectedValidators.directValue.forEach { $0() } - /// The response received from the server, if any. - open var response: HTTPURLResponse? { return task?.response as? HTTPURLResponse } + eventMonitor?.request(self, didCompleteTask: task, with: error) - /// The number of times the request has been retried. - open internal(set) var retryCount: UInt = 0 + retryOrFinish(error: self.error) + } - let originalTask: TaskConvertible? + /// Called when the `RequestDelegate` is retrying this `Request`. + func requestIsRetrying() { + protectedMutableState.write { $0.retryCount += 1 } - var startTime: CFAbsoluteTime? - var endTime: CFAbsoluteTime? + eventMonitor?.requestIsRetrying(self) + } - var validations: [() -> Void] = [] + /// Called to trigger retry or finish this `Request`. + func retryOrFinish(error: Error?) { + if let error = error, delegate?.willRetryRequest(self) == true { + delegate?.retryRequest(self, ifNecessaryWithError: error) + return + } else { + finish() + } + } - private var taskDelegate: TaskDelegate - private var taskDelegateLock = NSLock() + /// Finishes this `Request` and starts the response serializers. + func finish() { + // Start response handlers + internalQueue.isSuspended = false - // MARK: Lifecycle + eventMonitor?.requestDidFinish(self) + } - init(session: URLSession, requestTask: RequestTask, error: Error? = nil) { - self.session = session + /// Resets all task related state for retry. + func reset() { + error = nil - switch requestTask { - case .data(let originalTask, let task): - taskDelegate = DataTaskDelegate(task: task) - self.originalTask = originalTask - case .download(let originalTask, let task): - taskDelegate = DownloadTaskDelegate(task: task) - self.originalTask = originalTask - case .upload(let originalTask, let task): - taskDelegate = UploadTaskDelegate(task: task) - self.originalTask = originalTask - case .stream(let originalTask, let task): - taskDelegate = TaskDelegate(task: task) - self.originalTask = originalTask - } + uploadProgress.totalUnitCount = 0 + uploadProgress.completedUnitCount = 0 + downloadProgress.totalUnitCount = 0 + downloadProgress.completedUnitCount = 0 + } + + /// Called when updating the upload progress. + func updateUploadProgress(totalBytesSent: Int64, totalBytesExpectedToSend: Int64) { + uploadProgress.totalUnitCount = totalBytesExpectedToSend + uploadProgress.completedUnitCount = totalBytesSent - delegate.error = error - delegate.queue.addOperation { self.endTime = CFAbsoluteTimeGetCurrent() } + uploadProgressHandler?.queue.async { self.uploadProgressHandler?.handler(self.uploadProgress) } } - // MARK: Authentication + // MARK: Task Creation - /// Associates an HTTP Basic credential with the request. - /// - /// - parameter user: The user. - /// - parameter password: The password. - /// - parameter persistence: The URL credential persistence. `.ForSession` by default. + /// Called when creating a `URLSessionTask` for this `Request`. Subclasses must override. + func task(for request: URLRequest, using session: URLSession) -> URLSessionTask { + fatalError("Subclasses must override.") + } + + // MARK: - Public API + + // These APIs are callable from any queue. + + // MARK: - State + + /// Cancels the `Request`. Once cancelled, a `Request` can no longer be resumed or suspended. /// - /// - returns: The request. + /// - Returns: The `Request`. @discardableResult - open func authenticate( - user: String, - password: String, - persistence: URLCredential.Persistence = .forSession) - -> Self - { - let credential = URLCredential(user: user, password: password, persistence: persistence) - return authenticate(usingCredential: credential) + open func cancel() -> Self { + guard state.canTransitionTo(.cancelled) else { return self } + + state = .cancelled + + delegate?.cancelRequest(self) + + return self } - /// Associates a specified credential with the request. + /// Suspends the `Request`. /// - /// - parameter credential: The credential. - /// - /// - returns: The request. + /// - Returns: The `Request`. @discardableResult - open func authenticate(usingCredential credential: URLCredential) -> Self { - delegate.credential = credential + open func suspend() -> Self { + guard state.canTransitionTo(.suspended) else { return self } + + state = .suspended + + delegate?.suspendRequest(self) + return self } - /// Returns a base64 encoded basic authentication credential as an authorization header tuple. - /// - /// - parameter user: The user. - /// - parameter password: The password. + + /// Resumes the `Request`. /// - /// - returns: A tuple with Authorization header and credential value if encoding succeeds, `nil` otherwise. - open class func authorizationHeader(user: String, password: String) -> (key: String, value: String)? { - guard let data = "\(user):\(password)".data(using: .utf8) else { return nil } + /// - Returns: The `Request`. + @discardableResult + open func resume() -> Self { + guard state.canTransitionTo(.resumed) else { return self } + + state = .resumed - let credential = data.base64EncodedString(options: []) + delegate?.resumeRequest(self) - return (key: "Authorization", value: "Basic \(credential)") + return self } - // MARK: State + // MARK: - Closure API - /// Resumes the request. - open func resume() { - guard let task = task else { delegate.queue.isSuspended = false ; return } + /// Associates a credential using the provided values with the `Request`. + /// + /// - Parameters: + /// - username: The username. + /// - password: The password. + /// - persistence: The `URLCredential.Persistence` for the created `URLCredential`. + /// - Returns: The `Request`. + @discardableResult + open func authenticate(username: String, password: String, persistence: URLCredential.Persistence = .forSession) -> Self { + let credential = URLCredential(user: username, password: password, persistence: persistence) - if startTime == nil { startTime = CFAbsoluteTimeGetCurrent() } + return authenticate(with: credential) + } - task.resume() + /// Associates the provided credential with the `Request`. + /// + /// - Parameter credential: The `URLCredential`. + /// - Returns: The `Request`. + @discardableResult + open func authenticate(with credential: URLCredential) -> Self { + protectedMutableState.write { $0.credential = credential } - NotificationCenter.default.post( - name: Notification.Name.Task.DidResume, - object: self, - userInfo: [Notification.Key.Task: task] - ) + return self } - /// Suspends the request. - open func suspend() { - guard let task = task else { return } + /// Sets a closure to be called periodically during the lifecycle of the `Request` as data is read from the server. + /// + /// - parameter queue: The dispatch queue to execute the closure on. + /// - parameter closure: The code to be executed periodically as data is read from the server. + /// + /// - returns: The request. - task.suspend() + /// Sets a closure to be called periodically during the lifecycle of the `Request` as data is read from the server. + /// + /// Only the last closure provided is used. + /// + /// - Parameters: + /// - queue: The `DispatchQueue` to execute the closure on. Defaults to `.main`. + /// - closure: The code to be executed periodically as data is read from the server. + /// - Returns: The `Request`. + @discardableResult + open func downloadProgress(queue: DispatchQueue = .main, closure: @escaping ProgressHandler) -> Self { + protectedMutableState.write { $0.downloadProgressHandler = (handler: closure, queue: queue) } - NotificationCenter.default.post( - name: Notification.Name.Task.DidSuspend, - object: self, - userInfo: [Notification.Key.Task: task] - ) + return self } - /// Cancels the request. - open func cancel() { - guard let task = task else { return } - - task.cancel() + /// Sets a closure to be called periodically during the lifecycle of the `Request` as data is sent to the server. + /// + /// Only the last closure provided is used. + /// + /// - Parameters: + /// - queue: The `DispatchQueue` to execute the closure on. Defaults to `.main`. + /// - closure: The closure to be executed periodically as data is sent to the server. + /// - Returns: The `Request`. + @discardableResult + open func uploadProgress(queue: DispatchQueue = .main, closure: @escaping ProgressHandler) -> Self { + protectedMutableState.write { $0.uploadProgressHandler = (handler: closure, queue: queue) } - NotificationCenter.default.post( - name: Notification.Name.Task.DidCancel, - object: self, - userInfo: [Notification.Key.Task: task] - ) + return self } } -// MARK: - CustomStringConvertible +// MARK: - Protocol Conformances -extension Request: CustomStringConvertible { - /// The textual representation used when written to an output stream, which includes the HTTP method and URL, as - /// well as the response status code if a response has been received. - open var description: String { - var components: [String] = [] +extension Request: Equatable { + public static func == (lhs: Request, rhs: Request) -> Bool { + return lhs.id == rhs.id + } +} - if let HTTPMethod = request?.httpMethod { - components.append(HTTPMethod) - } +extension Request: Hashable { + public var hashValue: Int { + return id.hashValue + } +} - if let urlString = request?.url?.absoluteString { - components.append(urlString) - } +extension Request: CustomStringConvertible { + /// A textual representation of this instance, including the `HTTPMethod` and `URL` if the `URLRequest` has been + /// created, as well as the response status code, if a response has been received. + public var description: String { + guard let request = performedRequests.last ?? lastRequest, + let url = request.url, + let method = request.httpMethod else { return "No request created yet." } - if let response = response { - components.append("(\(response.statusCode))") - } + let requestDescription = "\(method) \(url.absoluteString)" - return components.joined(separator: " ") + return response.map { "\(requestDescription) (\($0.statusCode))" } ?? requestDescription } } -// MARK: - CustomDebugStringConvertible - extension Request: CustomDebugStringConvertible { - /// The textual representation used when written to an output stream, in the form of a cURL command. - open var debugDescription: String { + /// A textual representation of this instance in the form of a cURL command. + public var debugDescription: String { return cURLRepresentation() } func cURLRepresentation() -> String { - var components = ["$ curl -v"] + guard + let request = lastRequest, + let url = request.url, + let host = url.host, + let method = request.httpMethod else { return "$ curl command could not be created" } - guard let request = self.request, - let url = request.url, - let host = url.host - else { - return "$ curl command could not be created" - } + var components = ["$ curl -v"] - if let httpMethod = request.httpMethod, httpMethod != "GET" { - components.append("-X \(httpMethod)") - } + components.append("-X \(method)") - if let credentialStorage = self.session.configuration.urlCredentialStorage { + if let credentialStorage = delegate?.sessionConfiguration.urlCredentialStorage { let protectionSpace = URLProtectionSpace( host: host, port: url.port ?? 0, @@ -297,39 +548,40 @@ extension Request: CustomDebugStringConvertible { components.append("-u \(user):\(password)") } } else { - if let credential = delegate.credential, let user = credential.user, let password = credential.password { + if let credential = credential, let user = credential.user, let password = credential.password { components.append("-u \(user):\(password)") } } } - if session.configuration.httpShouldSetCookies { + if let configuration = delegate?.sessionConfiguration, configuration.httpShouldSetCookies { if - let cookieStorage = session.configuration.httpCookieStorage, + let cookieStorage = configuration.httpCookieStorage, let cookies = cookieStorage.cookies(for: url), !cookies.isEmpty { - let string = cookies.reduce("") { $0 + "\($1.name)=\($1.value);" } + let allCookies = cookies.map { "\($0.name)=\($0.value)" }.joined(separator: ";") - #if swift(>=3.2) - components.append("-b \"\(string[.. Bool + func retryRequest(_ request: Request, ifNecessaryWithError error: Error) + + func cancelRequest(_ request: Request) + func cancelDownloadRequest(_ request: DownloadRequest, byProducingResumeData: @escaping (Data?) -> Void) + func suspendRequest(_ request: Request) + func resumeRequest(_ request: Request) +} - // MARK: Helper Types +// MARK: - Subclasses - struct Requestable: TaskConvertible { - let urlRequest: URLRequest +// MARK: DataRequest - func task(session: URLSession, adapter: RequestAdapter?, queue: DispatchQueue) throws -> URLSessionTask { - do { - let urlRequest = try self.urlRequest.adapt(using: adapter) - return queue.sync { session.dataTask(with: urlRequest) } - } catch { - throw AdaptError(error: error) - } - } +open class DataRequest: Request { + public let convertible: URLRequestConvertible + + private var protectedData: Protector = Protector(nil) + public var data: Data? { return protectedData.directValue } + + init(id: UUID = UUID(), + convertible: URLRequestConvertible, + underlyingQueue: DispatchQueue, + serializationQueue: DispatchQueue, + eventMonitor: EventMonitor?, + delegate: RequestDelegate) { + self.convertible = convertible + + super.init(id: id, + underlyingQueue: underlyingQueue, + serializationQueue: serializationQueue, + eventMonitor: eventMonitor, + delegate: delegate) } - // MARK: Properties + override func reset() { + super.reset() - /// The request sent or to be sent to the server. - open override var request: URLRequest? { - if let request = super.request { return request } - if let requestable = originalTask as? Requestable { return requestable.urlRequest } + protectedData.directValue = nil + } + + func didReceive(data: Data) { + if self.data == nil { + protectedData.directValue = data + } else { + protectedData.append(data) + } - return nil + updateDownloadProgress() } - /// The progress of fetching the response data from the server for the request. - open var progress: Progress { return dataDelegate.progress } + override func task(for request: URLRequest, using session: URLSession) -> URLSessionTask { + let copiedRequest = request + return session.dataTask(with: copiedRequest) + } - var dataDelegate: DataTaskDelegate { return delegate as! DataTaskDelegate } + func updateDownloadProgress() { + let totalBytesRecieved = Int64(data?.count ?? 0) + let totalBytesExpected = task?.response?.expectedContentLength ?? NSURLSessionTransferSizeUnknown - // MARK: Stream + downloadProgress.totalUnitCount = totalBytesExpected + downloadProgress.completedUnitCount = totalBytesRecieved - /// Sets a closure to be called periodically during the lifecycle of the request as data is read from the server. + downloadProgressHandler?.queue.async { self.downloadProgressHandler?.handler(self.downloadProgress) } + } + + /// Validates the request, using the specified closure. /// - /// This closure returns the bytes most recently received from the server, not including data from previous calls. - /// If this closure is set, data will only be available within this closure, and will not be saved elsewhere. It is - /// also important to note that the server data in any `Response` object will be `nil`. + /// If validation fails, subsequent calls to response handlers will have an associated error. /// - /// - parameter closure: The code to be executed periodically during the lifecycle of the request. + /// - parameter validation: A closure to validate the request. /// /// - returns: The request. @discardableResult - open func stream(closure: ((Data) -> Void)? = nil) -> Self { - dataDelegate.dataStream = closure - return self - } + public func validate(_ validation: @escaping Validation) -> Self { + let validator: () -> Void = { [unowned self] in + guard self.error == nil, let response = self.response else { return } - // MARK: Progress + let result = validation(self.request, response, self.data) + + result.withError { self.error = $0 } + + self.eventMonitor?.request(self, + didValidateRequest: self.request, + response: response, + data: self.data, + withResult: result) + } + + protectedValidators.append(validator) - /// Sets a closure to be called periodically during the lifecycle of the `Request` as data is read from the server. - /// - /// - parameter queue: The dispatch queue to execute the closure on. - /// - parameter closure: The code to be executed periodically as data is read from the server. - /// - /// - returns: The request. - @discardableResult - open func downloadProgress(queue: DispatchQueue = DispatchQueue.main, closure: @escaping ProgressHandler) -> Self { - dataDelegate.progressHandler = (closure, queue) return self } } -// MARK: - - -/// Specific type of `Request` that manages an underlying `URLSessionDownloadTask`. open class DownloadRequest: Request { - - // MARK: Helper Types - /// A collection of options to be executed prior to moving a downloaded file from the temporary URL to the /// destination URL. - public struct DownloadOptions: OptionSet { - /// Returns the raw bitmask value of the option and satisfies the `RawRepresentable` protocol. - public let rawValue: UInt - + public struct Options: OptionSet { /// A `DownloadOptions` flag that creates intermediate directories for the destination URL if specified. - public static let createIntermediateDirectories = DownloadOptions(rawValue: 1 << 0) + public static let createIntermediateDirectories = Options(rawValue: 1 << 0) /// A `DownloadOptions` flag that removes a previous file from the destination URL if specified. - public static let removePreviousFile = DownloadOptions(rawValue: 1 << 1) + public static let removePreviousFile = Options(rawValue: 1 << 1) + + /// Returns the raw bitmask value of the option and satisfies the `RawRepresentable` protocol. + public let rawValue: Int - /// Creates a `DownloadFileDestinationOptions` instance with the specified raw value. + /// Creates a `DownloadRequest.Options` instance with the specified raw value. /// /// - parameter rawValue: The raw bitmask value for the option. /// - /// - returns: A new log level instance. - public init(rawValue: UInt) { + /// - returns: A new `DownloadRequest.Options` instance. + public init(rawValue: Int) { self.rawValue = rawValue } } @@ -445,205 +722,241 @@ open class DownloadRequest: Request { /// temporary file written to during the download process. The closure takes two arguments: the temporary file URL /// and the URL response, and returns a two arguments: the file URL where the temporary file should be moved and /// the options defining how the file should be moved. - public typealias DownloadFileDestination = ( - _ temporaryURL: URL, - _ response: HTTPURLResponse) - -> (destinationURL: URL, options: DownloadOptions) + public typealias Destination = (_ temporaryURL: URL, + _ response: HTTPURLResponse) -> (destinationURL: URL, options: Options) + + // MARK: Destination + + /// Creates a download file destination closure which uses the default file manager to move the temporary file to a + /// file URL in the first available directory with the specified search path directory and search path domain mask. + /// + /// - parameter directory: The search path directory. `.documentDirectory` by default. + /// - parameter domain: The search path domain mask. `.userDomainMask` by default. + /// + /// - returns: A download file destination closure. + open class func suggestedDownloadDestination(for directory: FileManager.SearchPathDirectory = .documentDirectory, + in domain: FileManager.SearchPathDomainMask = .userDomainMask, + options: Options = []) -> Destination { + return { (temporaryURL, response) in + let directoryURLs = FileManager.default.urls(for: directory, in: domain) + let url = directoryURLs.first?.appendingPathComponent(response.suggestedFilename!) ?? temporaryURL + + return (url, options) + } + } + + static let defaultDestination: Destination = { (url, _) in + let filename = "Alamofire_\(url.lastPathComponent)" + let destination = url.deletingLastPathComponent().appendingPathComponent(filename) + + return (destination, []) + } - enum Downloadable: TaskConvertible { - case request(URLRequest) + public enum Downloadable { + case request(URLRequestConvertible) case resumeData(Data) + } - func task(session: URLSession, adapter: RequestAdapter?, queue: DispatchQueue) throws -> URLSessionTask { - do { - let task: URLSessionTask + // MARK: Initial State + public let downloadable: Downloadable + let destination: Destination? - switch self { - case let .request(urlRequest): - let urlRequest = try urlRequest.adapt(using: adapter) - task = queue.sync { session.downloadTask(with: urlRequest) } - case let .resumeData(resumeData): - task = queue.sync { session.downloadTask(withResumeData: resumeData) } - } + // MARK: Updated State - return task - } catch { - throw AdaptError(error: error) - } - } + private struct MutableState { + var resumeData: Data? + var fileURL: URL? } - // MARK: Properties + private let protectedMutableState: Protector = Protector(MutableState()) - /// The request sent or to be sent to the server. - open override var request: URLRequest? { - if let request = super.request { return request } + public var resumeData: Data? { return protectedMutableState.directValue.resumeData } + public var fileURL: URL? { return protectedMutableState.directValue.fileURL } - if let downloadable = originalTask as? Downloadable, case let .request(urlRequest) = downloadable { - return urlRequest - } + // MARK: Init + + init(id: UUID = UUID(), + downloadable: Downloadable, + underlyingQueue: DispatchQueue, + serializationQueue: DispatchQueue, + eventMonitor: EventMonitor?, + delegate: RequestDelegate, + destination: Destination? = nil) { + self.downloadable = downloadable + self.destination = destination - return nil + super.init(id: id, + underlyingQueue: underlyingQueue, + serializationQueue: serializationQueue, + eventMonitor: eventMonitor, + delegate: delegate) } - /// The resume data of the underlying download task if available after a failure. - open var resumeData: Data? { return downloadDelegate.resumeData } + override func reset() { + super.reset() - /// The progress of downloading the response data from the server for the request. - open var progress: Progress { return downloadDelegate.progress } + protectedMutableState.write { $0.resumeData = nil } + protectedMutableState.write { $0.fileURL = nil } + } - var downloadDelegate: DownloadTaskDelegate { return delegate as! DownloadTaskDelegate } + func didFinishDownloading(using task: URLSessionTask, with result: Result) { + eventMonitor?.request(self, didFinishDownloadingUsing: task, with: result) - // MARK: State + result.withValue { url in protectedMutableState.write { $0.fileURL = url } } + .withError { self.error = $0 } + } - /// Cancels the request. - open override func cancel() { - downloadDelegate.downloadTask.cancel { self.downloadDelegate.resumeData = $0 } + func updateDownloadProgress(bytesWritten: Int64, totalBytesExpectedToWrite: Int64) { + downloadProgress.totalUnitCount = totalBytesExpectedToWrite + downloadProgress.completedUnitCount += bytesWritten - NotificationCenter.default.post( - name: Notification.Name.Task.DidCancel, - object: self, - userInfo: [Notification.Key.Task: task as Any] - ) + downloadProgressHandler?.queue.async { self.downloadProgressHandler?.handler(self.downloadProgress) } } - // MARK: Progress + override func task(for request: URLRequest, using session: URLSession) -> URLSessionTask { + return session.downloadTask(with: request) + } + + open func task(forResumeData data: Data, using session: URLSession) -> URLSessionTask { + return session.downloadTask(withResumeData: data) + } - /// Sets a closure to be called periodically during the lifecycle of the `Request` as data is read from the server. - /// - /// - parameter queue: The dispatch queue to execute the closure on. - /// - parameter closure: The code to be executed periodically as data is read from the server. - /// - /// - returns: The request. @discardableResult - open func downloadProgress(queue: DispatchQueue = DispatchQueue.main, closure: @escaping ProgressHandler) -> Self { - downloadDelegate.progressHandler = (closure, queue) + open override func cancel() -> Self { + guard state.canTransitionTo(.cancelled) else { return self } + + state = .cancelled + + delegate?.cancelDownloadRequest(self) { (resumeData) in + self.protectedMutableState.write { $0.resumeData = resumeData } + } + + eventMonitor?.requestDidCancel(self) + return self } - // MARK: Destination - - /// Creates a download file destination closure which uses the default file manager to move the temporary file to a - /// file URL in the first available directory with the specified search path directory and search path domain mask. + /// Validates the request, using the specified closure. /// - /// - parameter directory: The search path directory. `.DocumentDirectory` by default. - /// - parameter domain: The search path domain mask. `.UserDomainMask` by default. + /// If validation fails, subsequent calls to response handlers will have an associated error. /// - /// - returns: A download file destination closure. - open class func suggestedDownloadDestination( - for directory: FileManager.SearchPathDirectory = .documentDirectory, - in domain: FileManager.SearchPathDomainMask = .userDomainMask) - -> DownloadFileDestination - { - return { temporaryURL, response in - let directoryURLs = FileManager.default.urls(for: directory, in: domain) + /// - parameter validation: A closure to validate the request. + /// + /// - returns: The request. + @discardableResult + public func validate(_ validation: @escaping Validation) -> Self { + let validator: () -> Void = { [unowned self] in + guard self.error == nil, let response = self.response else { return } - if !directoryURLs.isEmpty { - return (directoryURLs[0].appendingPathComponent(response.suggestedFilename!), []) - } + let result = validation(self.request, response, self.fileURL) - return (temporaryURL, []) + result.withError { self.error = $0 } + + self.eventMonitor?.request(self, + didValidateRequest: self.request, + response: response, + fileURL: self.fileURL, + withResult: result) } + + protectedValidators.append(validator) + + return self } } -// MARK: - - -/// Specific type of `Request` that manages an underlying `URLSessionUploadTask`. open class UploadRequest: DataRequest { + public enum Uploadable { + case data(Data) + case file(URL, shouldRemove: Bool) + case stream(InputStream) + } - // MARK: Helper Types - - enum Uploadable: TaskConvertible { - case data(Data, URLRequest) - case file(URL, URLRequest) - case stream(InputStream, URLRequest) - - func task(session: URLSession, adapter: RequestAdapter?, queue: DispatchQueue) throws -> URLSessionTask { - do { - let task: URLSessionTask - - switch self { - case let .data(data, urlRequest): - let urlRequest = try urlRequest.adapt(using: adapter) - task = queue.sync { session.uploadTask(with: urlRequest, from: data) } - case let .file(url, urlRequest): - let urlRequest = try urlRequest.adapt(using: adapter) - task = queue.sync { session.uploadTask(with: urlRequest, fromFile: url) } - case let .stream(_, urlRequest): - let urlRequest = try urlRequest.adapt(using: adapter) - task = queue.sync { session.uploadTask(withStreamedRequest: urlRequest) } - } + // MARK: - Initial State - return task - } catch { - throw AdaptError(error: error) - } - } - } + public let upload: UploadableConvertible - // MARK: Properties + // MARK: - Updated State - /// The request sent or to be sent to the server. - open override var request: URLRequest? { - if let request = super.request { return request } + public var uploadable: Uploadable? - guard let uploadable = originalTask as? Uploadable else { return nil } + init(id: UUID = UUID(), + convertible: UploadConvertible, + underlyingQueue: DispatchQueue, + serializationQueue: DispatchQueue, + eventMonitor: EventMonitor?, + delegate: RequestDelegate) { + self.upload = convertible - switch uploadable { - case .data(_, let urlRequest), .file(_, let urlRequest), .stream(_, let urlRequest): - return urlRequest + super.init(id: id, + convertible: convertible, + underlyingQueue: underlyingQueue, + serializationQueue: serializationQueue, + eventMonitor: eventMonitor, + delegate: delegate) + + // Automatically remove temporary upload files (e.g. multipart form data) + internalQueue.addOperation { + guard + let uploadable = self.uploadable, + case let .file(url, shouldRemove) = uploadable, + shouldRemove else { return } + + // TODO: Abstract file manager + try? FileManager.default.removeItem(at: url) } } - /// The progress of uploading the payload to the server for the upload request. - open var uploadProgress: Progress { return uploadDelegate.uploadProgress } + func didCreateUploadable(_ uploadable: Uploadable) { + self.uploadable = uploadable - var uploadDelegate: UploadTaskDelegate { return delegate as! UploadTaskDelegate } + eventMonitor?.request(self, didCreateUploadable: uploadable) + } - // MARK: Upload Progress + func didFailToCreateUploadable(with error: Error) { + self.error = error - /// Sets a closure to be called periodically during the lifecycle of the `UploadRequest` as data is sent to - /// the server. - /// - /// After the data is sent to the server, the `progress(queue:closure:)` APIs can be used to monitor the progress - /// of data being read from the server. - /// - /// - parameter queue: The dispatch queue to execute the closure on. - /// - parameter closure: The code to be executed periodically as data is sent to the server. - /// - /// - returns: The request. - @discardableResult - open func uploadProgress(queue: DispatchQueue = DispatchQueue.main, closure: @escaping ProgressHandler) -> Self { - uploadDelegate.uploadProgressHandler = (closure, queue) - return self + eventMonitor?.request(self, didFailToCreateUploadableWithError: error) + + retryOrFinish(error: error) } -} -// MARK: - + override func task(for request: URLRequest, using session: URLSession) -> URLSessionTask { + guard let uploadable = uploadable else { + fatalError("Attempting to create a URLSessionUploadTask when Uploadable value doesn't exist.") + } -#if !os(watchOS) + switch uploadable { + case let .data(data): return session.uploadTask(with: request, from: data) + case let .file(url, _): return session.uploadTask(with: request, fromFile: url) + case .stream: return session.uploadTask(withStreamedRequest: request) + } + } -/// Specific type of `Request` that manages an underlying `URLSessionStreamTask`. -@available(iOS 9.0, macOS 10.11, tvOS 9.0, *) -open class StreamRequest: Request { - enum Streamable: TaskConvertible { - case stream(hostName: String, port: Int) - case netService(NetService) + func inputStream() -> InputStream { + guard let uploadable = uploadable else { + fatalError("Attempting to access the input stream but the uploadable doesn't exist.") + } - func task(session: URLSession, adapter: RequestAdapter?, queue: DispatchQueue) throws -> URLSessionTask { - let task: URLSessionTask + guard case let .stream(stream) = uploadable else { + fatalError("Attempted to access the stream of an UploadRequest that wasn't created with one.") + } - switch self { - case let .stream(hostName, port): - task = queue.sync { session.streamTask(withHostName: hostName, port: port) } - case let .netService(netService): - task = queue.sync { session.streamTask(with: netService) } - } + eventMonitor?.request(self, didProvideInputStream: stream) - return task - } + return stream } } -#endif +public protocol UploadableConvertible { + func createUploadable() throws -> UploadRequest.Uploadable +} + +extension UploadRequest.Uploadable: UploadableConvertible { + public func createUploadable() throws -> UploadRequest.Uploadable { + return self + } +} + +public protocol UploadConvertible: UploadableConvertible & URLRequestConvertible { } + diff --git a/Source/RequestAdapter.swift b/Source/RequestAdapter.swift new file mode 100644 index 000000000..c48057b8b --- /dev/null +++ b/Source/RequestAdapter.swift @@ -0,0 +1,35 @@ +// +// RequestAdapter.swift +// +// Copyright (c) 2014-2018 Alamofire Software Foundation (http://alamofire.org/) +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +import Foundation + +/// A type that can inspect and optionally adapt a `URLRequest` in some manner if necessary. +public protocol RequestAdapter { + /// Inspects and adapts the specified `URLRequest` in some manner and calls the completion handler with the Result. + /// + /// - Parameters: + /// - urlRequest: The `URLRequest` to adapt. + /// - completion: The completion handler that must be called when adaptation is complete. + func adapt(_ urlRequest: URLRequest, completion: @escaping (_ result: Result) -> Void) +} diff --git a/Source/RequestRetrier.swift b/Source/RequestRetrier.swift new file mode 100644 index 000000000..6f488e5d9 --- /dev/null +++ b/Source/RequestRetrier.swift @@ -0,0 +1,44 @@ +// +// RequestRetrier.swift +// +// Copyright (c) 2014-2018 Alamofire Software Foundation (http://alamofire.org/) +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +import Foundation + +/// A closure executed when the `RequestRetrier` determines whether a `Request` should be retried or not. +public typealias RequestRetryCompletion = (_ shouldRetry: Bool, _ timeDelay: TimeInterval) -> Void + +/// A type that determines whether a request should be retried after being executed by the specified session manager +/// and encountering an error. +public protocol RequestRetrier { + /// Determines whether the `Request` should be retried by calling the `completion` closure. + /// + /// This operation is fully asynchronous. Any amount of time can be taken to determine whether the request needs + /// to be retried. The one requirement is that the completion closure is called to ensure the request is properly + /// cleaned up after. + /// + /// - parameter manager: The session manager the request was executed on. + /// - parameter request: The request that failed due to the encountered error. + /// - parameter error: The error encountered when executing the request. + /// - parameter completion: The completion closure to be executed when retry decision has been determined. + func should(_ manager: Session, retry request: Request, with error: Error, completion: @escaping RequestRetryCompletion) +} diff --git a/Source/RequestTaskMap.swift b/Source/RequestTaskMap.swift new file mode 100644 index 000000000..63e15e281 --- /dev/null +++ b/Source/RequestTaskMap.swift @@ -0,0 +1,88 @@ +// +// RequestTaskMap.swift +// +// Copyright (c) 2014-2018 Alamofire Software Foundation (http://alamofire.org/) +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +import Foundation + +/// A type that maintains a two way, one to one map of `URLSessionTask`s to `Request`s. +struct RequestTaskMap { + private var requests: [URLSessionTask: Request] + private var tasks: [Request: URLSessionTask] + + init(requests: [URLSessionTask: Request] = [:], tasks: [Request: URLSessionTask] = [:]) { + self.requests = requests + self.tasks = tasks + } + + subscript(_ request: Request) -> URLSessionTask? { + get { return tasks[request] } + set { + guard let newValue = newValue else { + guard let task = tasks[request] else { + fatalError("RequestTaskMap consistency error: no task corresponding to request found.") + } + + tasks.removeValue(forKey: request) + requests.removeValue(forKey: task) + + return + } + + tasks[request] = newValue + requests[newValue] = request + } + } + + subscript(_ task: URLSessionTask) -> Request? { + get { return requests[task] } + set { + guard let newValue = newValue else { + guard let request = requests[task] else { + fatalError("RequestTaskMap consistency error: no request corresponding to task found.") + } + + requests.removeValue(forKey: task) + tasks.removeValue(forKey: request) + + return + } + + requests[task] = newValue + tasks[newValue] = task + } + } + + var count: Int { + precondition(requests.count == tasks.count, + "RequestTaskMap.count invalid, requests.count: \(requests.count) != tasks.count: \(tasks.count)") + + return requests.count + } + + var isEmpty: Bool { + precondition(requests.isEmpty == tasks.isEmpty, + "RequestTaskMap.isEmpty invalid, requests.isEmpty: \(requests.isEmpty) != tasks.isEmpty: \(tasks.isEmpty)") + + return requests.isEmpty + } +} diff --git a/Source/Response.swift b/Source/Response.swift index 74b1ef54d..142a178a1 100644 --- a/Source/Response.swift +++ b/Source/Response.swift @@ -24,52 +24,6 @@ import Foundation -/// Used to store all data associated with an non-serialized response of a data or upload request. -public struct DefaultDataResponse { - /// The URL request sent to the server. - public let request: URLRequest? - - /// The server's response to the URL request. - public let response: HTTPURLResponse? - - /// The data returned by the server. - public let data: Data? - - /// The error encountered while executing or validating the request. - public let error: Error? - - /// The timeline of the complete lifecycle of the request. - public let timeline: Timeline - - var _metrics: AnyObject? - - /// Creates a `DefaultDataResponse` instance from the specified parameters. - /// - /// - Parameters: - /// - request: The URL request sent to the server. - /// - response: The server's response to the URL request. - /// - data: The data returned by the server. - /// - error: The error encountered while executing or validating the request. - /// - timeline: The timeline of the complete lifecycle of the request. `Timeline()` by default. - /// - metrics: The task metrics containing the request / response statistics. `nil` by default. - public init( - request: URLRequest?, - response: HTTPURLResponse?, - data: Data?, - error: Error?, - timeline: Timeline = Timeline(), - metrics: AnyObject? = nil) - { - self.request = request - self.response = response - self.data = data - self.error = error - self.timeline = timeline - } -} - -// MARK: - - /// Used to store all data associated with a serialized response of a data or upload request. public struct DataResponse { /// The URL request sent to the server. @@ -81,41 +35,42 @@ public struct DataResponse { /// The data returned by the server. public let data: Data? + /// The final metrics of the response. + public let metrics: URLSessionTaskMetrics? + + /// The time taken to serialize the response. + public let serializationDuration: TimeInterval + /// The result of response serialization. public let result: Result - /// The timeline of the complete lifecycle of the request. - public let timeline: Timeline - /// Returns the associated value of the result if it is a success, `nil` otherwise. public var value: Value? { return result.value } /// Returns the associated error value if the result if it is a failure, `nil` otherwise. public var error: Error? { return result.error } - var _metrics: AnyObject? - - /// Creates a `DataResponse` instance with the specified parameters derived from response serialization. + /// Creates a `DataResponse` instance with the specified parameters derviced from the response serialization. /// - /// - parameter request: The URL request sent to the server. - /// - parameter response: The server's response to the URL request. - /// - parameter data: The data returned by the server. - /// - parameter result: The result of response serialization. - /// - parameter timeline: The timeline of the complete lifecycle of the `Request`. Defaults to `Timeline()`. - /// - /// - returns: The new `DataResponse` instance. - public init( - request: URLRequest?, - response: HTTPURLResponse?, - data: Data?, - result: Result, - timeline: Timeline = Timeline()) - { + /// - Parameters: + /// - request: The `URLRequest` sent to the server. + /// - response: The `HTTPURLResponse` from the server. + /// - data: The `Data` returned by the server. + /// - metrics: The `URLSessionTaskMetrics` of the serialized response. + /// - serializationDuration: The duration taken by serialization. + /// - result: The `Result` of response serialization. + public init(request: URLRequest?, + response: HTTPURLResponse?, + data: Data?, + metrics: URLSessionTaskMetrics?, + serializationDuration: TimeInterval, + result: Result) { self.request = request self.response = response self.data = data + self.metrics = metrics + self.serializationDuration = serializationDuration self.result = result - self.timeline = timeline } } @@ -129,17 +84,29 @@ extension DataResponse: CustomStringConvertible, CustomDebugStringConvertible { } /// The debug textual representation used when written to an output stream, which includes the URL request, the URL - /// response, the server data, the response serialization result and the timeline. + /// response, the server data, the duration of the network and serializatino actions, and the response serialization + /// result. public var debugDescription: String { - var output: [String] = [] - - output.append(request != nil ? "[Request]: \(request!.httpMethod ?? "GET") \(request!)" : "[Request]: nil") - output.append(response != nil ? "[Response]: \(response!)" : "[Response]: nil") - output.append("[Data]: \(data?.count ?? 0) bytes") - output.append("[Result]: \(result.debugDescription)") - output.append("[Timeline]: \(timeline.debugDescription)") - - return output.joined(separator: "\n") + let requestDescription = request.map { "\($0.httpMethod!) \($0)" } ?? "nil" + let responseDescription = response.map { (response) in + let sortedHeaders = response.httpHeaders.sorted() + + return """ + [Status Code]: \(response.statusCode) + [Headers]: + \(sortedHeaders) + """ + } ?? "nil" + let metricsDescription = metrics.map { "\($0.taskInterval.duration)s" } ?? "None" + + return """ + [Request]: \(requestDescription) + [Response]: \n\(responseDescription) + [Data]: \(data?.description ?? "None") + [Network Duration]: \(metricsDescription) + [Serialization Duration]: \(serializationDuration)s + [Result]: \(result.debugDescription) + """ } } @@ -159,17 +126,12 @@ extension DataResponse { /// - returns: A `DataResponse` whose result wraps the value returned by the given closure. If this instance's /// result is a failure, returns a response wrapping the same failure. public func map(_ transform: (Value) -> T) -> DataResponse { - var response = DataResponse( - request: request, - response: self.response, - data: data, - result: result.map(transform), - timeline: timeline - ) - - response._metrics = _metrics - - return response + return DataResponse(request: request, + response: self.response, + data: data, + metrics: metrics, + serializationDuration: serializationDuration, + result: result.map(transform)) } /// Evaluates the given closure when the result of this `DataResponse` is a success, passing the unwrapped result @@ -187,17 +149,12 @@ extension DataResponse { /// - returns: A success or failure `DataResponse` depending on the result of the given closure. If this instance's /// result is a failure, returns the same failure. public func flatMap(_ transform: (Value) throws -> T) -> DataResponse { - var response = DataResponse( - request: request, - response: self.response, - data: data, - result: result.flatMap(transform), - timeline: timeline - ) - - response._metrics = _metrics - - return response + return DataResponse(request: request, + response: self.response, + data: data, + metrics: metrics, + serializationDuration: serializationDuration, + result: result.flatMap(transform)) } /// Evaluates the specified closure when the `DataResponse` is a failure, passing the unwrapped error as a parameter. @@ -210,17 +167,12 @@ extension DataResponse { /// - Parameter transform: A closure that takes the error of the instance. /// - Returns: A `DataResponse` instance containing the result of the transform. public func mapError(_ transform: (Error) -> E) -> DataResponse { - var response = DataResponse( - request: request, - response: self.response, - data: data, - result: result.mapError(transform), - timeline: timeline - ) - - response._metrics = _metrics - - return response + return DataResponse(request: request, + response: self.response, + data: data, + metrics: metrics, + serializationDuration: serializationDuration, + result: result.mapError(transform)) } /// Evaluates the specified closure when the `DataResponse` is a failure, passing the unwrapped error as a parameter. @@ -236,75 +188,12 @@ extension DataResponse { /// /// - Returns: A `DataResponse` instance containing the result of the transform. public func flatMapError(_ transform: (Error) throws -> E) -> DataResponse { - var response = DataResponse( - request: request, - response: self.response, - data: data, - result: result.flatMapError(transform), - timeline: timeline - ) - - response._metrics = _metrics - - return response - } -} - -// MARK: - - -/// Used to store all data associated with an non-serialized response of a download request. -public struct DefaultDownloadResponse { - /// The URL request sent to the server. - public let request: URLRequest? - - /// The server's response to the URL request. - public let response: HTTPURLResponse? - - /// The temporary destination URL of the data returned from the server. - public let temporaryURL: URL? - - /// The final destination URL of the data returned from the server if it was moved. - public let destinationURL: URL? - - /// The resume data generated if the request was cancelled. - public let resumeData: Data? - - /// The error encountered while executing or validating the request. - public let error: Error? - - /// The timeline of the complete lifecycle of the request. - public let timeline: Timeline - - var _metrics: AnyObject? - - /// Creates a `DefaultDownloadResponse` instance from the specified parameters. - /// - /// - Parameters: - /// - request: The URL request sent to the server. - /// - response: The server's response to the URL request. - /// - temporaryURL: The temporary destination URL of the data returned from the server. - /// - destinationURL: The final destination URL of the data returned from the server if it was moved. - /// - resumeData: The resume data generated if the request was cancelled. - /// - error: The error encountered while executing or validating the request. - /// - timeline: The timeline of the complete lifecycle of the request. `Timeline()` by default. - /// - metrics: The task metrics containing the request / response statistics. `nil` by default. - public init( - request: URLRequest?, - response: HTTPURLResponse?, - temporaryURL: URL?, - destinationURL: URL?, - resumeData: Data?, - error: Error?, - timeline: Timeline = Timeline(), - metrics: AnyObject? = nil) - { - self.request = request - self.response = response - self.temporaryURL = temporaryURL - self.destinationURL = destinationURL - self.resumeData = resumeData - self.error = error - self.timeline = timeline + return DataResponse(request: request, + response: self.response, + data: data, + metrics: metrics, + serializationDuration: serializationDuration, + result: result.flatMapError(transform)) } } @@ -318,56 +207,54 @@ public struct DownloadResponse { /// The server's response to the URL request. public let response: HTTPURLResponse? - /// The temporary destination URL of the data returned from the server. - public let temporaryURL: URL? - - /// The final destination URL of the data returned from the server if it was moved. - public let destinationURL: URL? + /// The final destination URL of the data returned from the server after it is moved. + public let fileURL: URL? /// The resume data generated if the request was cancelled. public let resumeData: Data? + /// The final metrics of the response. + public let metrics: URLSessionTaskMetrics? + + /// The time taken to serialize the response. + public let serializationDuration: TimeInterval + /// The result of response serialization. public let result: Result - /// The timeline of the complete lifecycle of the request. - public let timeline: Timeline - /// Returns the associated value of the result if it is a success, `nil` otherwise. public var value: Value? { return result.value } /// Returns the associated error value if the result if it is a failure, `nil` otherwise. public var error: Error? { return result.error } - var _metrics: AnyObject? - /// Creates a `DownloadResponse` instance with the specified parameters derived from response serialization. /// - /// - parameter request: The URL request sent to the server. - /// - parameter response: The server's response to the URL request. - /// - parameter temporaryURL: The temporary destination URL of the data returned from the server. - /// - parameter destinationURL: The final destination URL of the data returned from the server if it was moved. - /// - parameter resumeData: The resume data generated if the request was cancelled. - /// - parameter result: The result of response serialization. - /// - parameter timeline: The timeline of the complete lifecycle of the `Request`. Defaults to `Timeline()`. - /// - /// - returns: The new `DownloadResponse` instance. + /// - Parameters: + /// - request: The `URLRequest` sent to the server. + /// - response: The `HTTPURLResponse` from the server. + /// - temporaryURL: The temporary destinatio `URL` of the data returned from the server. + /// - destinationURL: The final destination `URL` of the data returned from the server, if it was moved. + /// - resumeData: The resume `Data` generated if the request was cancelled. + /// - metrics: The `URLSessionTaskMetrics` of the serialized response. + /// - serializationDuration: The duration taken by serialization. + /// - result: The `Result` of response serialization. public init( request: URLRequest?, response: HTTPURLResponse?, - temporaryURL: URL?, - destinationURL: URL?, + fileURL: URL?, resumeData: Data?, - result: Result, - timeline: Timeline = Timeline()) + metrics: URLSessionTaskMetrics?, + serializationDuration: TimeInterval, + result: Result) { self.request = request self.response = response - self.temporaryURL = temporaryURL - self.destinationURL = destinationURL + self.fileURL = fileURL self.resumeData = resumeData + self.metrics = metrics + self.serializationDuration = serializationDuration self.result = result - self.timeline = timeline } } @@ -381,20 +268,31 @@ extension DownloadResponse: CustomStringConvertible, CustomDebugStringConvertibl } /// The debug textual representation used when written to an output stream, which includes the URL request, the URL - /// response, the temporary and destination URLs, the resume data, the response serialization result and the - /// timeline. + /// response, the temporary and destination URLs, the resume data, the durations of the network and serialization + /// actions, and the response serialization result. public var debugDescription: String { - var output: [String] = [] - - output.append(request != nil ? "[Request]: \(request!.httpMethod ?? "GET") \(request!)" : "[Request]: nil") - output.append(response != nil ? "[Response]: \(response!)" : "[Response]: nil") - output.append("[TemporaryURL]: \(temporaryURL?.path ?? "nil")") - output.append("[DestinationURL]: \(destinationURL?.path ?? "nil")") - output.append("[ResumeData]: \(resumeData?.count ?? 0) bytes") - output.append("[Result]: \(result.debugDescription)") - output.append("[Timeline]: \(timeline.debugDescription)") - - return output.joined(separator: "\n") + let requestDescription = request.map { "\($0.httpMethod!) \($0)" } ?? "nil" + let responseDescription = response.map { (response) in + let sortedHeaders = response.httpHeaders.sorted() + + return """ + [Status Code]: \(response.statusCode) + [Headers]: + \(sortedHeaders) + """ + } ?? "nil" + let metricsDescription = metrics.map { "\($0.taskInterval.duration)s" } ?? "None" + let resumeDataDescription = resumeData.map { "\($0)" } ?? "None" + + return """ + [Request]: \(requestDescription) + [Response]: \n\(responseDescription) + [File URL]: \(fileURL?.path ?? "nil") + [ResumeData]: \(resumeDataDescription) + [Network Duration]: \(metricsDescription) + [Serialization Duration]: \(serializationDuration)s + [Result]: \(result.debugDescription) + """ } } @@ -414,19 +312,15 @@ extension DownloadResponse { /// - returns: A `DownloadResponse` whose result wraps the value returned by the given closure. If this instance's /// result is a failure, returns a response wrapping the same failure. public func map(_ transform: (Value) -> T) -> DownloadResponse { - var response = DownloadResponse( + return DownloadResponse( request: request, - response: self.response, - temporaryURL: temporaryURL, - destinationURL: destinationURL, + response: response, + fileURL: fileURL, resumeData: resumeData, - result: result.map(transform), - timeline: timeline + metrics: metrics, + serializationDuration: serializationDuration, + result: result.map(transform) ) - - response._metrics = _metrics - - return response } /// Evaluates the given closure when the result of this `DownloadResponse` is a success, passing the unwrapped @@ -444,19 +338,15 @@ extension DownloadResponse { /// - returns: A success or failure `DownloadResponse` depending on the result of the given closure. If this /// instance's result is a failure, returns the same failure. public func flatMap(_ transform: (Value) throws -> T) -> DownloadResponse { - var response = DownloadResponse( + return DownloadResponse( request: request, - response: self.response, - temporaryURL: temporaryURL, - destinationURL: destinationURL, + response: response, + fileURL: fileURL, resumeData: resumeData, - result: result.flatMap(transform), - timeline: timeline + metrics: metrics, + serializationDuration: serializationDuration, + result: result.flatMap(transform) ) - - response._metrics = _metrics - - return response } /// Evaluates the specified closure when the `DownloadResponse` is a failure, passing the unwrapped error as a parameter. @@ -469,19 +359,15 @@ extension DownloadResponse { /// - Parameter transform: A closure that takes the error of the instance. /// - Returns: A `DownloadResponse` instance containing the result of the transform. public func mapError(_ transform: (Error) -> E) -> DownloadResponse { - var response = DownloadResponse( + return DownloadResponse( request: request, - response: self.response, - temporaryURL: temporaryURL, - destinationURL: destinationURL, + response: response, + fileURL: fileURL, resumeData: resumeData, - result: result.mapError(transform), - timeline: timeline + metrics: metrics, + serializationDuration: serializationDuration, + result: result.mapError(transform) ) - - response._metrics = _metrics - - return response } /// Evaluates the specified closure when the `DownloadResponse` is a failure, passing the unwrapped error as a parameter. @@ -497,71 +383,14 @@ extension DownloadResponse { /// /// - Returns: A `DownloadResponse` instance containing the result of the transform. public func flatMapError(_ transform: (Error) throws -> E) -> DownloadResponse { - var response = DownloadResponse( + return DownloadResponse( request: request, - response: self.response, - temporaryURL: temporaryURL, - destinationURL: destinationURL, + response: response, + fileURL: fileURL, resumeData: resumeData, - result: result.flatMapError(transform), - timeline: timeline + metrics: metrics, + serializationDuration: serializationDuration, + result: result.flatMapError(transform) ) - - response._metrics = _metrics - - return response } } - -// MARK: - - -protocol Response { - /// The task metrics containing the request / response statistics. - var _metrics: AnyObject? { get set } - mutating func add(_ metrics: AnyObject?) -} - -extension Response { - mutating func add(_ metrics: AnyObject?) { - #if !os(watchOS) - guard #available(iOS 10.0, macOS 10.12, tvOS 10.0, *) else { return } - guard let metrics = metrics as? URLSessionTaskMetrics else { return } - - _metrics = metrics - #endif - } -} - -// MARK: - - -@available(iOS 10.0, macOS 10.12, tvOS 10.0, *) -extension DefaultDataResponse: Response { -#if !os(watchOS) - /// The task metrics containing the request / response statistics. - public var metrics: URLSessionTaskMetrics? { return _metrics as? URLSessionTaskMetrics } -#endif -} - -@available(iOS 10.0, macOS 10.12, tvOS 10.0, *) -extension DataResponse: Response { -#if !os(watchOS) - /// The task metrics containing the request / response statistics. - public var metrics: URLSessionTaskMetrics? { return _metrics as? URLSessionTaskMetrics } -#endif -} - -@available(iOS 10.0, macOS 10.12, tvOS 10.0, *) -extension DefaultDownloadResponse: Response { -#if !os(watchOS) - /// The task metrics containing the request / response statistics. - public var metrics: URLSessionTaskMetrics? { return _metrics as? URLSessionTaskMetrics } -#endif -} - -@available(iOS 10.0, macOS 10.12, tvOS 10.0, *) -extension DownloadResponse: Response { -#if !os(watchOS) - /// The task metrics containing the request / response statistics. - public var metrics: URLSessionTaskMetrics? { return _metrics as? URLSessionTaskMetrics } -#endif -} diff --git a/Source/ResponseSerialization.swift b/Source/ResponseSerialization.swift index 3333726d5..0c55b8883 100644 --- a/Source/ResponseSerialization.swift +++ b/Source/ResponseSerialization.swift @@ -24,80 +24,77 @@ import Foundation -/// The type in which all data response serializers must conform to in order to serialize a response. +// MARK: Protocols + +/// The type to which all data response serializers must conform in order to serialize a response. public protocol DataResponseSerializerProtocol { - /// The type of serialized object to be created by this `DataResponseSerializerType`. + /// The type of serialized object to be created by this serializer. associatedtype SerializedObject - /// A closure used by response handlers that takes a request, response, data and error and returns a result. - var serializeResponse: (URLRequest?, HTTPURLResponse?, Data?, Error?) -> Result { get } -} - -// MARK: - - -/// A generic `DataResponseSerializerType` used to serialize a request, response, and data into a serialized object. -public struct DataResponseSerializer: DataResponseSerializerProtocol { - /// The type of serialized object to be created by this `DataResponseSerializer`. - public typealias SerializedObject = Value - - /// A closure used by response handlers that takes a request, response, data and error and returns a result. - public var serializeResponse: (URLRequest?, HTTPURLResponse?, Data?, Error?) -> Result - - /// Initializes the `ResponseSerializer` instance with the given serialize response closure. - /// - /// - parameter serializeResponse: The closure used to serialize the response. - /// - /// - returns: The new generic response serializer instance. - public init(serializeResponse: @escaping (URLRequest?, HTTPURLResponse?, Data?, Error?) -> Result) { - self.serializeResponse = serializeResponse - } + /// The function used to serialize the response data in response handlers. + func serialize(request: URLRequest?, response: HTTPURLResponse?, data: Data?, error: Error?) throws -> SerializedObject } -// MARK: - - -/// The type in which all download response serializers must conform to in order to serialize a response. +/// The type to which all download response serializers must conform in order to serialize a response. public protocol DownloadResponseSerializerProtocol { /// The type of serialized object to be created by this `DownloadResponseSerializerType`. associatedtype SerializedObject - /// A closure used by response handlers that takes a request, response, url and error and returns a result. - var serializeResponse: (URLRequest?, HTTPURLResponse?, URL?, Error?) -> Result { get } + /// The function used to serialize the downloaded data in response handlers. + func serializeDownload(request: URLRequest?, response: HTTPURLResponse?, fileURL: URL?, error: Error?) throws -> SerializedObject } -// MARK: - +/// A serializer that can handle both data and download responses. +public protocol ResponseSerializer: DataResponseSerializerProtocol & DownloadResponseSerializerProtocol { + var emptyRequestMethods: Set { get } + var emptyResponseCodes: Set { get } +} -/// A generic `DownloadResponseSerializerType` used to serialize a request, response, and data into a serialized object. -public struct DownloadResponseSerializer: DownloadResponseSerializerProtocol { - /// The type of serialized object to be created by this `DownloadResponseSerializer`. - public typealias SerializedObject = Value +extension ResponseSerializer { + public static var defaultEmptyRequestMethods: Set { return [.head] } + public static var defaultEmptyResponseCodes: Set { return [204, 205] } - /// A closure used by response handlers that takes a request, response, url and error and returns a result. - public var serializeResponse: (URLRequest?, HTTPURLResponse?, URL?, Error?) -> Result + public var emptyRequestMethods: Set { return Self.defaultEmptyRequestMethods } + public var emptyResponseCodes: Set { return Self.defaultEmptyResponseCodes } - /// Initializes the `ResponseSerializer` instance with the given serialize response closure. - /// - /// - parameter serializeResponse: The closure used to serialize the response. - /// - /// - returns: The new generic response serializer instance. - public init(serializeResponse: @escaping (URLRequest?, HTTPURLResponse?, URL?, Error?) -> Result) { - self.serializeResponse = serializeResponse + public func requestAllowsEmptyResponseData(_ request: URLRequest?) -> Bool? { + return request.flatMap { $0.httpMethod } + .flatMap(HTTPMethod.init) + .map { emptyRequestMethods.contains($0) } + } + + public func responseAllowsEmptyResponseData(_ response: HTTPURLResponse?) -> Bool? { + return response.flatMap { $0.statusCode } + .map { emptyResponseCodes.contains($0) } + } + + public func emptyResponseAllowed(forRequest request: URLRequest?, response: HTTPURLResponse?) -> Bool { + return requestAllowsEmptyResponseData(request) ?? responseAllowsEmptyResponseData(response) ?? false } } -// MARK: - Timeline +/// By default, any serializer declared to conform to both types will get file serialization for free, as it just feeds +/// the data read from disk into the data response serializer. +public extension DownloadResponseSerializerProtocol where Self: DataResponseSerializerProtocol { + func serializeDownload(request: URLRequest?, response: HTTPURLResponse?, fileURL: URL?, error: Error?) throws -> Self.SerializedObject { + guard error == nil else { throw error! } -extension Request { - var timeline: Timeline { - let requestStartTime = self.startTime ?? CFAbsoluteTimeGetCurrent() - let requestCompletedTime = self.endTime ?? CFAbsoluteTimeGetCurrent() - let initialResponseTime = self.delegate.initialResponseTime ?? requestCompletedTime + guard let fileURL = fileURL else { + throw AFError.responseSerializationFailed(reason: .inputFileNil) + } - return Timeline( - requestStartTime: requestStartTime, - initialResponseTime: initialResponseTime, - requestCompletedTime: requestCompletedTime, - serializationCompletedTime: CFAbsoluteTimeGetCurrent() - ) + let data: Data + do { + data = try Data(contentsOf: fileURL) + } catch { + throw AFError.responseSerializationFailed(reason: .inputFileReadFailed(at: fileURL)) + } + + do { + return try serialize(request: request, response: response, data: data, error: error) + } catch { + throw error + } } } @@ -106,25 +103,26 @@ extension Request { extension DataRequest { /// Adds a handler to be called once the request has finished. /// - /// - parameter queue: The queue on which the completion handler is dispatched. - /// - parameter completionHandler: The code to be executed once the request has finished. - /// - /// - returns: The request. + /// - Parameters: + /// - queue: The queue on which the completion handler is dispatched. Defaults to `nil`, which means + /// the handler is called on `.main`. + /// - completionHandler: The code to be executed once the request has finished. + /// - Returns: The request. @discardableResult - public func response(queue: DispatchQueue? = nil, completionHandler: @escaping (DefaultDataResponse) -> Void) -> Self { - delegate.queue.addOperation { - (queue ?? DispatchQueue.main).async { - var dataResponse = DefaultDataResponse( - request: self.request, - response: self.response, - data: self.delegate.data, - error: self.delegate.error, - timeline: self.timeline - ) - - dataResponse.add(self.delegate.metrics) - - completionHandler(dataResponse) + public func response(queue: DispatchQueue? = nil, completionHandler: @escaping (DataResponse) -> Void) -> Self { + internalQueue.addOperation { + self.serializationQueue.async { + let result = Result(value: self.data, error: self.error) + let response = DataResponse(request: self.request, + response: self.response, + data: self.data, + metrics: self.metrics, + serializationDuration: 0, + result: result) + + self.eventMonitor?.request(self, didParseResponse: response) + + (queue ?? .main).async { completionHandler(response) } } } @@ -133,38 +131,39 @@ extension DataRequest { /// Adds a handler to be called once the request has finished. /// - /// - parameter queue: The queue on which the completion handler is dispatched. - /// - parameter responseSerializer: The response serializer responsible for serializing the request, response, - /// and data. - /// - parameter completionHandler: The code to be executed once the request has finished. - /// - /// - returns: The request. + /// - Parameters: + /// - queue: The queue on which the completion handler is dispatched. Defaults to `nil`, which means + /// the handler is called on `.main`. + /// - responseSerializer: The response serializer responsible for serializing the request, response, and data. + /// - completionHandler: The code to be executed once the request has finished. + /// - Returns: The request. @discardableResult - public func response( + public func response( queue: DispatchQueue? = nil, - responseSerializer: T, - completionHandler: @escaping (DataResponse) -> Void) + responseSerializer: Serializer, + completionHandler: @escaping (DataResponse) -> Void) -> Self { - delegate.queue.addOperation { - let result = responseSerializer.serializeResponse( - self.request, - self.response, - self.delegate.data, - self.delegate.error - ) - - var dataResponse = DataResponse( - request: self.request, - response: self.response, - data: self.delegate.data, - result: result, - timeline: self.timeline - ) - - dataResponse.add(self.delegate.metrics) - - (queue ?? DispatchQueue.main).async { completionHandler(dataResponse) } + internalQueue.addOperation { + self.serializationQueue.async { + let start = CFAbsoluteTimeGetCurrent() + let result = Result { try responseSerializer.serialize(request: self.request, + response: self.response, + data: self.data, + error: self.error) } + let end = CFAbsoluteTimeGetCurrent() + + let response = DataResponse(request: self.request, + response: self.response, + data: self.data, + metrics: self.metrics, + serializationDuration: (end - start), + result: result) + + self.eventMonitor?.request(self, didParseResponse: response) + + (queue ?? .main).async { completionHandler(response) } + } } return self @@ -174,31 +173,29 @@ extension DataRequest { extension DownloadRequest { /// Adds a handler to be called once the request has finished. /// - /// - parameter queue: The queue on which the completion handler is dispatched. - /// - parameter completionHandler: The code to be executed once the request has finished. - /// - /// - returns: The request. + /// - Parameters: + /// - queue: The queue on which the completion handler is dispatched. Defaults to `nil`, which means + /// the handler is called on `.main`. + /// - completionHandler: The code to be executed once the request has finished. + /// - Returns: The request. @discardableResult public func response( queue: DispatchQueue? = nil, - completionHandler: @escaping (DefaultDownloadResponse) -> Void) + completionHandler: @escaping (DownloadResponse) -> Void) -> Self { - delegate.queue.addOperation { - (queue ?? DispatchQueue.main).async { - var downloadResponse = DefaultDownloadResponse( - request: self.request, - response: self.response, - temporaryURL: self.downloadDelegate.temporaryURL, - destinationURL: self.downloadDelegate.destinationURL, - resumeData: self.downloadDelegate.resumeData, - error: self.downloadDelegate.error, - timeline: self.timeline - ) - - downloadResponse.add(self.delegate.metrics) - - completionHandler(downloadResponse) + internalQueue.addOperation { + self.serializationQueue.async { + let result = Result(value: self.fileURL , error: self.error) + let response = DownloadResponse(request: self.request, + response: self.response, + fileURL: self.fileURL, + resumeData: self.resumeData, + metrics: self.metrics, + serializationDuration: 0, + result: result) + + (queue ?? .main).async { completionHandler(response) } } } @@ -207,12 +204,13 @@ extension DownloadRequest { /// Adds a handler to be called once the request has finished. /// - /// - parameter queue: The queue on which the completion handler is dispatched. - /// - parameter responseSerializer: The response serializer responsible for serializing the request, response, - /// and data contained in the destination url. - /// - parameter completionHandler: The code to be executed once the request has finished. - /// - /// - returns: The request. + /// - Parameters: + /// - queue: The queue on which the completion handler is dispatched. Defaults to `nil`, which means + /// the handler is called on `.main`. + /// - responseSerializer: The response serializer responsible for serializing the request, response, and data + /// contained in the destination url. + /// - completionHandler: The code to be executed once the request has finished. + /// - Returns: The request. @discardableResult public func response( queue: DispatchQueue? = nil, @@ -220,27 +218,25 @@ extension DownloadRequest { completionHandler: @escaping (DownloadResponse) -> Void) -> Self { - delegate.queue.addOperation { - let result = responseSerializer.serializeResponse( - self.request, - self.response, - self.downloadDelegate.fileURL, - self.downloadDelegate.error - ) - - var downloadResponse = DownloadResponse( - request: self.request, - response: self.response, - temporaryURL: self.downloadDelegate.temporaryURL, - destinationURL: self.downloadDelegate.destinationURL, - resumeData: self.downloadDelegate.resumeData, - result: result, - timeline: self.timeline - ) - - downloadResponse.add(self.delegate.metrics) - - (queue ?? DispatchQueue.main).async { completionHandler(downloadResponse) } + internalQueue.addOperation { + self.serializationQueue.async { + let start = CFAbsoluteTimeGetCurrent() + let result = Result { try responseSerializer.serializeDownload(request: self.request, + response: self.response, + fileURL: self.fileURL, + error: self.error) } + let end = CFAbsoluteTimeGetCurrent() + + let response = DownloadResponse(request: self.request, + response: self.response, + fileURL: self.fileURL, + resumeData: self.resumeData, + metrics: self.metrics, + serializationDuration: (end - start), + result: result) + + (queue ?? .main).async { completionHandler(response) } + } } return self @@ -249,82 +245,70 @@ extension DownloadRequest { // MARK: - Data -extension Request { - /// Returns a result data type that contains the response data as-is. - /// - /// - parameter response: The response from the server. - /// - parameter data: The data returned from the server. - /// - parameter error: The error already encountered if it exists. - /// - /// - returns: The result data type. - public static func serializeResponseData(response: HTTPURLResponse?, data: Data?, error: Error?) -> Result { - guard error == nil else { return .failure(error!) } - - if let response = response, emptyDataStatusCodes.contains(response.statusCode) { return .success(Data()) } - - guard let validData = data else { - return .failure(AFError.responseSerializationFailed(reason: .inputDataNil)) - } - - return .success(validData) - } -} - extension DataRequest { - /// Creates a response serializer that returns the associated data as-is. - /// - /// - returns: A data response serializer. - public static func dataResponseSerializer() -> DataResponseSerializer { - return DataResponseSerializer { _, response, data, error in - return Request.serializeResponseData(response: response, data: data, error: error) - } - } - /// Adds a handler to be called once the request has finished. /// - /// - parameter completionHandler: The code to be executed once the request has finished. - /// - /// - returns: The request. + /// - Parameters: + /// - queue: The queue on which the completion handler is dispatched. Defaults to `nil`, which means + /// the handler is called on `.main`. + /// - completionHandler: The code to be executed once the request has finished. + /// - Returns: The request. @discardableResult public func responseData( queue: DispatchQueue? = nil, completionHandler: @escaping (DataResponse) -> Void) -> Self { - return response( - queue: queue, - responseSerializer: DataRequest.dataResponseSerializer(), - completionHandler: completionHandler - ) + return response(queue: queue, + responseSerializer: DataResponseSerializer(), + completionHandler: completionHandler) } } -extension DownloadRequest { - /// Creates a response serializer that returns the associated data as-is. - /// - /// - returns: A data response serializer. - public static func dataResponseSerializer() -> DownloadResponseSerializer { - return DownloadResponseSerializer { _, response, fileURL, error in - guard error == nil else { return .failure(error!) } +/// A `ResponseSerializer` that performs minimal reponse checking and returns any response data as-is. By default, a +/// request returning `nil` or no data is considered an error. However, if the response is has a status code valid for +/// empty responses (`204`, `205`), then an empty `Data` value is returned. +public final class DataResponseSerializer: ResponseSerializer { + /// HTTP response codes for which empty responses are allowed. + public let emptyResponseCodes: Set + /// HTTP request methods for which empty responses are allowed. + public let emptyRequestMethods: Set + + /// Creates an instance using the provided values. + /// + /// - Parameters: + /// - emptyResponseCodes: The HTTP response codes for which empty responses are allowed. Defaults to + /// `[204, 205]`. + /// - emptyRequestMethods: The HTTP request methods for which empty responses are allowed. Defaults to `[.head]`. + public init(emptyResponseCodes: Set = DataResponseSerializer.defaultEmptyResponseCodes, + emptyRequestMethods: Set = DataResponseSerializer.defaultEmptyRequestMethods) { + self.emptyResponseCodes = emptyResponseCodes + self.emptyRequestMethods = emptyRequestMethods + } - guard let fileURL = fileURL else { - return .failure(AFError.responseSerializationFailed(reason: .inputFileNil)) - } + public func serialize(request: URLRequest?, response: HTTPURLResponse?, data: Data?, error: Error?) throws -> Data { + guard error == nil else { throw error! } - do { - let data = try Data(contentsOf: fileURL) - return Request.serializeResponseData(response: response, data: data, error: error) - } catch { - return .failure(AFError.responseSerializationFailed(reason: .inputFileReadFailed(at: fileURL))) + guard let data = data, !data.isEmpty else { + guard emptyResponseAllowed(forRequest: request, response: response) else { + throw AFError.responseSerializationFailed(reason: .inputDataNilOrZeroLength) } + + return Data() } + + return data } +} +extension DownloadRequest { /// Adds a handler to be called once the request has finished. /// - /// - parameter completionHandler: The code to be executed once the request has finished. - /// - /// - returns: The request. + /// - Parameters: + /// - queue: The queue on which the completion handler is dispatched. Defaults to `nil`, which means + /// the handler is called on `.main`. + /// - completionHandler: The code to be executed once the request has finished. + /// - Returns: The request. @discardableResult public func responseData( queue: DispatchQueue? = nil, @@ -333,7 +317,7 @@ extension DownloadRequest { { return response( queue: queue, - responseSerializer: DownloadRequest.dataResponseSerializer(), + responseSerializer: DataResponseSerializer(), completionHandler: completionHandler ) } @@ -341,119 +325,92 @@ extension DownloadRequest { // MARK: - String -extension Request { - /// Returns a result string type initialized from the response data with the specified string encoding. - /// - /// - parameter encoding: The string encoding. If `nil`, the string encoding will be determined from the server - /// response, falling back to the default HTTP default character set, ISO-8859-1. - /// - parameter response: The response from the server. - /// - parameter data: The data returned from the server. - /// - parameter error: The error already encountered if it exists. - /// - /// - returns: The result data type. - public static func serializeResponseString( - encoding: String.Encoding?, - response: HTTPURLResponse?, - data: Data?, - error: Error?) - -> Result - { - guard error == nil else { return .failure(error!) } +/// A `ResponseSerializer` that decodes the response data as a `String`. By default, a request returning `nil` or no +/// data is considered an error. However, if the response is has a status code valid for empty responses (`204`, `205`), +/// then an empty `String` is returned. +public final class StringResponseSerializer: ResponseSerializer { + /// Optional string encoding used to validate the response. + public let encoding: String.Encoding? + /// HTTP response codes for which empty responses are allowed. + public let emptyResponseCodes: Set + /// HTTP request methods for which empty responses are allowed. + public let emptyRequestMethods: Set + + /// Creates an instance with the provided values. + /// + /// - Parameters: + /// - encoding: A string encoding. Defaults to `nil`, in which case the encoding will be determined + /// from the server response, falling back to the default HTTP character set, `ISO-8859-1`. + /// - emptyResponseCodes: The HTTP response codes for which empty responses are allowed. Defaults to + /// `[204, 205]`. + /// - emptyRequestMethods: The HTTP request methods for which empty responses are allowed. Defaults to `[.head]`. + public init(encoding: String.Encoding? = nil, + emptyResponseCodes: Set = StringResponseSerializer.defaultEmptyResponseCodes, + emptyRequestMethods: Set = StringResponseSerializer.defaultEmptyRequestMethods) { + self.encoding = encoding + self.emptyResponseCodes = emptyResponseCodes + self.emptyRequestMethods = emptyRequestMethods + } + + public func serialize(request: URLRequest?, response: HTTPURLResponse?, data: Data?, error: Error?) throws -> String { + guard error == nil else { throw error! } - if let response = response, emptyDataStatusCodes.contains(response.statusCode) { return .success("") } + guard let data = data, !data.isEmpty else { + guard emptyResponseAllowed(forRequest: request, response: response) else { + throw AFError.responseSerializationFailed(reason: .inputDataNilOrZeroLength) + } - guard let validData = data else { - return .failure(AFError.responseSerializationFailed(reason: .inputDataNil)) + return "" } var convertedEncoding = encoding if let encodingName = response?.textEncodingName as CFString?, convertedEncoding == nil { - convertedEncoding = String.Encoding(rawValue: CFStringConvertEncodingToNSStringEncoding( - CFStringConvertIANACharSetNameToEncoding(encodingName)) - ) + let ianaCharSet = CFStringConvertIANACharSetNameToEncoding(encodingName) + let nsStringEncoding = CFStringConvertEncodingToNSStringEncoding(ianaCharSet) + convertedEncoding = String.Encoding(rawValue: nsStringEncoding) } let actualEncoding = convertedEncoding ?? .isoLatin1 - if let string = String(data: validData, encoding: actualEncoding) { - return .success(string) - } else { - return .failure(AFError.responseSerializationFailed(reason: .stringSerializationFailed(encoding: actualEncoding))) + guard let string = String(data: data, encoding: actualEncoding) else { + throw AFError.responseSerializationFailed(reason: .stringSerializationFailed(encoding: actualEncoding)) } + + return string } } extension DataRequest { - /// Creates a response serializer that returns a result string type initialized from the response data with - /// the specified string encoding. - /// - /// - parameter encoding: The string encoding. If `nil`, the string encoding will be determined from the server - /// response, falling back to the default HTTP default character set, ISO-8859-1. - /// - /// - returns: A string response serializer. - public static func stringResponseSerializer(encoding: String.Encoding? = nil) -> DataResponseSerializer { - return DataResponseSerializer { _, response, data, error in - return Request.serializeResponseString(encoding: encoding, response: response, data: data, error: error) - } - } - /// Adds a handler to be called once the request has finished. /// - /// - parameter encoding: The string encoding. If `nil`, the string encoding will be determined from the - /// server response, falling back to the default HTTP default character set, - /// ISO-8859-1. - /// - parameter completionHandler: A closure to be executed once the request has finished. - /// - /// - returns: The request. + /// - Parameters: + /// - queue: The queue on which the completion handler is dispatched. Defaults to `nil`, which means + /// the handler is called on `.main`. + /// - encoding: The string encoding. Defaults to `nil`, in which case the encoding will be determined from + /// the server response, falling back to the default HTTP character set, `ISO-8859-1`. + /// - completionHandler: A closure to be executed once the request has finished. + /// - Returns: The request. @discardableResult - public func responseString( - queue: DispatchQueue? = nil, - encoding: String.Encoding? = nil, - completionHandler: @escaping (DataResponse) -> Void) - -> Self - { - return response( - queue: queue, - responseSerializer: DataRequest.stringResponseSerializer(encoding: encoding), - completionHandler: completionHandler - ) + public func responseString(queue: DispatchQueue? = nil, + encoding: String.Encoding? = nil, + completionHandler: @escaping (DataResponse) -> Void) -> Self { + return response(queue: queue, + responseSerializer: StringResponseSerializer(encoding: encoding), + completionHandler: completionHandler) } } extension DownloadRequest { - /// Creates a response serializer that returns a result string type initialized from the response data with - /// the specified string encoding. - /// - /// - parameter encoding: The string encoding. If `nil`, the string encoding will be determined from the server - /// response, falling back to the default HTTP default character set, ISO-8859-1. - /// - /// - returns: A string response serializer. - public static func stringResponseSerializer(encoding: String.Encoding? = nil) -> DownloadResponseSerializer { - return DownloadResponseSerializer { _, response, fileURL, error in - guard error == nil else { return .failure(error!) } - - guard let fileURL = fileURL else { - return .failure(AFError.responseSerializationFailed(reason: .inputFileNil)) - } - - do { - let data = try Data(contentsOf: fileURL) - return Request.serializeResponseString(encoding: encoding, response: response, data: data, error: error) - } catch { - return .failure(AFError.responseSerializationFailed(reason: .inputFileReadFailed(at: fileURL))) - } - } - } - /// Adds a handler to be called once the request has finished. /// - /// - parameter encoding: The string encoding. If `nil`, the string encoding will be determined from the - /// server response, falling back to the default HTTP default character set, - /// ISO-8859-1. - /// - parameter completionHandler: A closure to be executed once the request has finished. - /// - /// - returns: The request. + /// - Parameters: + /// - queue: The queue on which the completion handler is dispatched. Defaults to `nil`, which means + /// the handler is called on `.main`. + /// - encoding: The string encoding. Defaults to `nil`, in which case the encoding will be determined from + /// the server response, falling back to the default HTTP character set, `ISO-8859-1`. + /// - completionHandler: A closure to be executed once the request has finished. + /// - Returns: The request. @discardableResult public func responseString( queue: DispatchQueue? = nil, @@ -463,7 +420,7 @@ extension DownloadRequest { { return response( queue: queue, - responseSerializer: DownloadRequest.stringResponseSerializer(encoding: encoding), + responseSerializer: StringResponseSerializer(encoding: encoding), completionHandler: completionHandler ) } @@ -471,110 +428,79 @@ extension DownloadRequest { // MARK: - JSON -extension Request { - /// Returns a JSON object contained in a result type constructed from the response data using `JSONSerialization` - /// with the specified reading options. - /// - /// - parameter options: The JSON serialization reading options. Defaults to `.allowFragments`. - /// - parameter response: The response from the server. - /// - parameter data: The data returned from the server. - /// - parameter error: The error already encountered if it exists. - /// - /// - returns: The result data type. - public static func serializeResponseJSON( - options: JSONSerialization.ReadingOptions, - response: HTTPURLResponse?, - data: Data?, - error: Error?) - -> Result - { - guard error == nil else { return .failure(error!) } +/// A `ResponseSerializer` that decodes the response data using `JSONSerialization`. By default, a request returning +/// `nil` or no data is considered an error. However, if the response is has a status code valid for empty responses +/// (`204`, `205`), then an `NSNull` value is returned. +public final class JSONResponseSerializer: ResponseSerializer { + /// `JSONSerialization.ReadingOptions` used when serializing a response. + public let options: JSONSerialization.ReadingOptions + /// HTTP response codes for which empty responses are allowed. + public let emptyResponseCodes: Set + /// HTTP request methods for which empty responses are allowed. + public let emptyRequestMethods: Set + + /// Creates an instance with the provided values. + /// + /// - Parameters: + /// - options: The options to use. Defaults to `.allowFragments`. + /// - emptyResponseCodes: The HTTP response codes for which empty responses are allowed. Defaults to + /// `[204, 205]`. + /// - emptyRequestMethods: The HTTP request methods for which empty responses are allowed. Defaults to `[.head]`. + public init(options: JSONSerialization.ReadingOptions = .allowFragments, + emptyResponseCodes: Set = JSONResponseSerializer.defaultEmptyResponseCodes, + emptyRequestMethods: Set = JSONResponseSerializer.defaultEmptyRequestMethods) { + self.options = options + self.emptyResponseCodes = emptyResponseCodes + self.emptyRequestMethods = emptyRequestMethods + } - if let response = response, emptyDataStatusCodes.contains(response.statusCode) { return .success(NSNull()) } + public func serialize(request: URLRequest?, response: HTTPURLResponse?, data: Data?, error: Error?) throws -> Any { + guard error == nil else { throw error! } - guard let validData = data, validData.count > 0 else { - return .failure(AFError.responseSerializationFailed(reason: .inputDataNilOrZeroLength)) + guard let data = data, !data.isEmpty else { + guard emptyResponseAllowed(forRequest: request, response: response) else { + throw AFError.responseSerializationFailed(reason: .inputDataNilOrZeroLength) + } + + return NSNull() } do { - let json = try JSONSerialization.jsonObject(with: validData, options: options) - return .success(json) + return try JSONSerialization.jsonObject(with: data, options: options) } catch { - return .failure(AFError.responseSerializationFailed(reason: .jsonSerializationFailed(error: error))) + throw AFError.responseSerializationFailed(reason: .jsonSerializationFailed(error: error)) } } } extension DataRequest { - /// Creates a response serializer that returns a JSON object result type constructed from the response data using - /// `JSONSerialization` with the specified reading options. - /// - /// - parameter options: The JSON serialization reading options. Defaults to `.allowFragments`. - /// - /// - returns: A JSON object response serializer. - public static func jsonResponseSerializer( - options: JSONSerialization.ReadingOptions = .allowFragments) - -> DataResponseSerializer - { - return DataResponseSerializer { _, response, data, error in - return Request.serializeResponseJSON(options: options, response: response, data: data, error: error) - } - } - /// Adds a handler to be called once the request has finished. /// - /// - parameter options: The JSON serialization reading options. Defaults to `.allowFragments`. - /// - parameter completionHandler: A closure to be executed once the request has finished. - /// - /// - returns: The request. + /// - Parameters: + /// - queue: The queue on which the completion handler is dispatched. Defaults to `nil`, which means + /// the handler is called on `.main`. + /// - options: The JSON serialization reading options. Defaults to `.allowFragments`. + /// - completionHandler: A closure to be executed once the request has finished. + /// - Returns: The request. @discardableResult - public func responseJSON( - queue: DispatchQueue? = nil, - options: JSONSerialization.ReadingOptions = .allowFragments, - completionHandler: @escaping (DataResponse) -> Void) - -> Self - { - return response( - queue: queue, - responseSerializer: DataRequest.jsonResponseSerializer(options: options), - completionHandler: completionHandler - ) + public func responseJSON(queue: DispatchQueue? = nil, + options: JSONSerialization.ReadingOptions = .allowFragments, + completionHandler: @escaping (DataResponse) -> Void) -> Self { + return response(queue: queue, + responseSerializer: JSONResponseSerializer(options: options), + completionHandler: completionHandler) } } extension DownloadRequest { - /// Creates a response serializer that returns a JSON object result type constructed from the response data using - /// `JSONSerialization` with the specified reading options. - /// - /// - parameter options: The JSON serialization reading options. Defaults to `.allowFragments`. - /// - /// - returns: A JSON object response serializer. - public static func jsonResponseSerializer( - options: JSONSerialization.ReadingOptions = .allowFragments) - -> DownloadResponseSerializer - { - return DownloadResponseSerializer { _, response, fileURL, error in - guard error == nil else { return .failure(error!) } - - guard let fileURL = fileURL else { - return .failure(AFError.responseSerializationFailed(reason: .inputFileNil)) - } - - do { - let data = try Data(contentsOf: fileURL) - return Request.serializeResponseJSON(options: options, response: response, data: data, error: error) - } catch { - return .failure(AFError.responseSerializationFailed(reason: .inputFileReadFailed(at: fileURL))) - } - } - } - /// Adds a handler to be called once the request has finished. /// - /// - parameter options: The JSON serialization reading options. Defaults to `.allowFragments`. - /// - parameter completionHandler: A closure to be executed once the request has finished. - /// - /// - returns: The request. + /// - Parameters: + /// - queue: The queue on which the completion handler is dispatched. Defaults to `nil`, which means + /// the handler is called on `.main`. + /// - options: The JSON serialization reading options. Defaults to `.allowFragments`. + /// - completionHandler: A closure to be executed once the request has finished. + /// - Returns: The request. @discardableResult public func responseJSON( queue: DispatchQueue? = nil, @@ -582,134 +508,104 @@ extension DownloadRequest { completionHandler: @escaping (DownloadResponse) -> Void) -> Self { - return response( - queue: queue, - responseSerializer: DownloadRequest.jsonResponseSerializer(options: options), - completionHandler: completionHandler - ) + return response(queue: queue, + responseSerializer: JSONResponseSerializer(options: options), + completionHandler: completionHandler) } } -// MARK: - Property List - -extension Request { - /// Returns a plist object contained in a result type constructed from the response data using - /// `PropertyListSerialization` with the specified reading options. - /// - /// - parameter options: The property list reading options. Defaults to `[]`. - /// - parameter response: The response from the server. - /// - parameter data: The data returned from the server. - /// - parameter error: The error already encountered if it exists. - /// - /// - returns: The result data type. - public static func serializeResponsePropertyList( - options: PropertyListSerialization.ReadOptions, - response: HTTPURLResponse?, - data: Data?, - error: Error?) - -> Result - { - guard error == nil else { return .failure(error!) } - - if let response = response, emptyDataStatusCodes.contains(response.statusCode) { return .success(NSNull()) } - - guard let validData = data, validData.count > 0 else { - return .failure(AFError.responseSerializationFailed(reason: .inputDataNilOrZeroLength)) - } +// MARK: - Empty - do { - let plist = try PropertyListSerialization.propertyList(from: validData, options: options, format: nil) - return .success(plist) - } catch { - return .failure(AFError.responseSerializationFailed(reason: .propertyListSerializationFailed(error: error))) - } - } +/// A type representing an empty response. Use `Empty.value` to get the instance. +public struct Empty: Decodable { + public static let value = Empty() } -extension DataRequest { - /// Creates a response serializer that returns an object constructed from the response data using - /// `PropertyListSerialization` with the specified reading options. - /// - /// - parameter options: The property list reading options. Defaults to `[]`. - /// - /// - returns: A property list object response serializer. - public static func propertyListResponseSerializer( - options: PropertyListSerialization.ReadOptions = []) - -> DataResponseSerializer - { - return DataResponseSerializer { _, response, data, error in - return Request.serializeResponsePropertyList(options: options, response: response, data: data, error: error) - } - } +// MARK: - DataDecoder Protocol - /// Adds a handler to be called once the request has finished. +/// Any type which can decode `Data`. +public protocol DataDecoder { + /// Decode `Data` into the provided type. /// - /// - parameter options: The property list reading options. Defaults to `[]`. - /// - parameter completionHandler: A closure to be executed once the request has finished. - /// - /// - returns: The request. - @discardableResult - public func responsePropertyList( - queue: DispatchQueue? = nil, - options: PropertyListSerialization.ReadOptions = [], - completionHandler: @escaping (DataResponse) -> Void) - -> Self - { - return response( - queue: queue, - responseSerializer: DataRequest.propertyListResponseSerializer(options: options), - completionHandler: completionHandler - ) - } + /// - Parameters: + /// - type: The `Type` to be decoded. + /// - data: The `Data` + /// - Returns: The decoded value of type `D`. + /// - Throws: Any error that occurs during decode. + func decode(_ type: D.Type, from data: Data) throws -> D } -extension DownloadRequest { - /// Creates a response serializer that returns an object constructed from the response data using - /// `PropertyListSerialization` with the specified reading options. - /// - /// - parameter options: The property list reading options. Defaults to `[]`. - /// - /// - returns: A property list object response serializer. - public static func propertyListResponseSerializer( - options: PropertyListSerialization.ReadOptions = []) - -> DownloadResponseSerializer - { - return DownloadResponseSerializer { _, response, fileURL, error in - guard error == nil else { return .failure(error!) } +/// `JSONDecoder` automatically conforms to `DataDecoder`. +extension JSONDecoder: DataDecoder { } + +// MARK: - Decodable + +/// A `ResponseSerializer` that decodes the response data as a generic value using any type that conforms to +/// `DataDecoder`. By default, this is an instance of `JSONDecoder`. Additionally, a request returning `nil` or no data +/// is considered an error. However, if the response is has a status code valid for empty responses (`204`, `205`), then +/// the `Empty.value` value is returned. +public final class DecodableResponseSerializer: ResponseSerializer { + /// The `JSONDecoder` instance used to decode responses. + public let decoder: DataDecoder + /// HTTP response codes for which empty responses are allowed. + public let emptyResponseCodes: Set + /// HTTP request methods for which empty responses are allowed. + public let emptyRequestMethods: Set + + /// Creates an instance using the values provided. + /// + /// - Parameters: + /// - decoder: The `JSONDecoder`. Defaults to a `JSONDecoder()`. + /// - emptyResponseCodes: The HTTP response codes for which empty responses are allowed. Defaults to + /// `[204, 205]`. + /// - emptyRequestMethods: The HTTP request methods for which empty responses are allowed. Defaults to `[.head]`. + public init(decoder: DataDecoder = JSONDecoder(), + emptyResponseCodes: Set = DecodableResponseSerializer.defaultEmptyResponseCodes, + emptyRequestMethods: Set = DecodableResponseSerializer.defaultEmptyRequestMethods) { + self.decoder = decoder + self.emptyResponseCodes = emptyResponseCodes + self.emptyRequestMethods = emptyRequestMethods + } + + public func serialize(request: URLRequest?, response: HTTPURLResponse?, data: Data?, error: Error?) throws -> T { + guard error == nil else { throw error! } - guard let fileURL = fileURL else { - return .failure(AFError.responseSerializationFailed(reason: .inputFileNil)) + guard let data = data, !data.isEmpty else { + guard emptyResponseAllowed(forRequest: request, response: response) else { + throw AFError.responseSerializationFailed(reason: .inputDataNilOrZeroLength) } - do { - let data = try Data(contentsOf: fileURL) - return Request.serializeResponsePropertyList(options: options, response: response, data: data, error: error) - } catch { - return .failure(AFError.responseSerializationFailed(reason: .inputFileReadFailed(at: fileURL))) + guard let emptyValue = Empty.value as? T else { + throw AFError.responseSerializationFailed(reason: .invalidEmptyResponse(type: "\(T.self)")) } + + return emptyValue + } + + do { + return try decoder.decode(T.self, from: data) + } catch { + throw AFError.responseSerializationFailed(reason: .decodingFailed(error: error)) } } +} +extension DataRequest { /// Adds a handler to be called once the request has finished. /// - /// - parameter options: The property list reading options. Defaults to `[]`. - /// - parameter completionHandler: A closure to be executed once the request has finished. - /// - /// - returns: The request. + /// - Parameters: + /// - queue: The queue on which the completion handler is dispatched. Defaults to `nil`, which means + /// the handler is called on `.main`. + /// - decoder: The `DataDecoder` to use to decode the response. Defaults to a `JSONDecoder` with default + /// settings. + /// - completionHandler: A closure to be executed once the request has finished. + /// - Returns: The request. @discardableResult - public func responsePropertyList( - queue: DispatchQueue? = nil, - options: PropertyListSerialization.ReadOptions = [], - completionHandler: @escaping (DownloadResponse) -> Void) - -> Self - { - return response( - queue: queue, - responseSerializer: DownloadRequest.propertyListResponseSerializer(options: options), - completionHandler: completionHandler - ) + public func responseDecodable(queue: DispatchQueue? = nil, + decoder: DataDecoder = JSONDecoder(), + completionHandler: @escaping (DataResponse) -> Void) -> Self { + return response(queue: queue, + responseSerializer: DecodableResponseSerializer(decoder: decoder), + completionHandler: completionHandler) } } - -/// A set of HTTP response status code that do not contain response data. -private let emptyDataStatusCodes: Set = [204, 205] diff --git a/Source/Result.swift b/Source/Result.swift index 95aba9bb7..b68d9550b 100644 --- a/Source/Result.swift +++ b/Source/Result.swift @@ -35,6 +35,19 @@ public enum Result { case success(Value) case failure(Error) + /// Initializes a `Result` from value or error. Returns `.failure` if the error is non-nil, `.success` otherwise. + /// + /// - Parameters: + /// - value: A value. + /// - error: An `Error`. + init(value: Value, error: Error?) { + if let error = error { + self = .failure(error) + } else { + self = .success(value) + } + } + /// Returns `true` if the result is a success, `false` otherwise. public var isSuccess: Bool { switch self { diff --git a/Source/ServerTrustEvaluation.swift b/Source/ServerTrustEvaluation.swift new file mode 100644 index 000000000..17fede880 --- /dev/null +++ b/Source/ServerTrustEvaluation.swift @@ -0,0 +1,499 @@ +// +// ServerTrustPolicy.swift +// +// Copyright (c) 2014-2016 Alamofire Software Foundation (http://alamofire.org/) +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +import Foundation + +/// Responsible for managing the mapping of `ServerTrustEvaluating` values to given hosts. +open class ServerTrustManager { + /// Determines whether all hosts for this `ServerTrustManager` must be evaluated. Defaults to `true`. + public let allHostsMustBeEvaluated: Bool + + /// The dictionary of policies mapped to a particular host. + public let evaluators: [String: ServerTrustEvaluating] + + /// Initializes the `ServerTrustManager` instance with the given evaluators. + /// + /// Since different servers and web services can have different leaf certificates, intermediate and even root + /// certficates, it is important to have the flexibility to specify evaluation policies on a per host basis. This + /// allows for scenarios such as using default evaluation for host1, certificate pinning for host2, public key + /// pinning for host3 and disabling evaluation for host4. + /// + /// - Parameters: + /// - allHostsMustBeEvaluated: The value determining whether all hosts for this instance must be evaluated. + /// Defaults to `true`. + /// - evaluators: A dictionary of evaluators mappend to hosts. + public init(allHostsMustBeEvaluated: Bool = true, evaluators: [String: ServerTrustEvaluating]) { + self.allHostsMustBeEvaluated = allHostsMustBeEvaluated + self.evaluators = evaluators + } + + /// Returns the `ServerTrustEvaluating` value for the given host, if one is set. + /// + /// By default, this method will return the policy that perfectly matches the given host. Subclasses could override + /// this method and implement more complex mapping implementations such as wildcards. + /// + /// - Parameter host: The host to use when searching for a matching policy. + /// - Returns: The `ServerTrustEvaluating` value for the given host if found, `nil` otherwise. + /// - Throws: `AFError.serverTrustEvaluationFailed` if `allHostsMustBeEvaluated` is `true` and no matching + /// evaluators are found. + open func serverTrustEvaluator(forHost host: String) throws -> ServerTrustEvaluating? { + guard let evaluator = evaluators[host] else { + if allHostsMustBeEvaluated { + throw AFError.serverTrustEvaluationFailed(reason: .noRequiredEvaluator(host: host)) + } + + return nil + } + + return evaluator + } +} + +/// A protocol describing the API used to evaluate server trusts. +public protocol ServerTrustEvaluating { + #if os(Linux) + // Implement this once Linux has API for evaluating server trusts. + #else + /// Evaluates the given `SecTrust` value for the given `host`. + /// + /// - Parameters: + /// - trust: The `SecTrust` value to evaluate. + /// - host: The host for which to evaluate the `SecTrust` value. + /// - Returns: A `Bool` indicating whether the evaluator considers the `SecTrust` value valid for `host`. + func evaluate(_ trust: SecTrust, forHost host: String) throws + #endif +} + +extension Array where Element == ServerTrustEvaluating { + #if os(Linux) + // Add this same convenience method for Linux. + #else + /// Evaluates the given `SecTrust` value for the given `host`. + /// + /// - Parameters: + /// - trust: The `SecTrust` value to evaluate. + /// - host: The host for which to evaluate the `SecTrust` value. + /// - Returns: Whether or not the evaluator considers the `SecTrust` value valid for `host`. + func evaluate(_ trust: SecTrust, forHost host: String) throws { + for evaluator in self { + try evaluator.evaluate(trust, forHost: host) + } + } + #endif +} + +// MARK: - Server Trust Evaluators + +/// An evaluator which uses the default server trust evaluation while allowing you to control whether to validate the +/// host provided by the challenge. Applications are encouraged to always validate the host in production environments +/// to guarantee the validity of the server's certificate chain. +public final class DefaultTrustEvaluator: ServerTrustEvaluating { + private let validateHost: Bool + + /// Creates a `DefaultTrustEvalutor`. + /// + /// - Parameter validateHost: Determines whether or not the evaluator should validate the host. Defaults to `true`. + public init(validateHost: Bool = true) { + self.validateHost = validateHost + } + + public func evaluate(_ trust: SecTrust, forHost host: String) throws { + if validateHost { + try trust.validateHost(host) + } + + try trust.performDefaultEvaluation(forHost: host) + } +} + +/// An evaluator which Uses the default and revoked server trust evaluations allowing you to control whether to validate +/// the host provided by the challenge as well as specify the revocation flags for testing for revoked certificates. +/// Apple platforms did not start testing for revoked certificates automatically until iOS 10.1, macOS 10.12 and tvOS +/// 10.1 which is demonstrated in our TLS tests. Applications are encouraged to always validate the host in production +/// environments to guarantee the validity of the server's certificate chain. +public final class RevocationTrustEvaluator: ServerTrustEvaluating { + /// Represents the options to be use when evaluating the status of a certificate. + /// Only Revocation Policy Constants are valid, and can be found in [Apple's documentation](https://developer.apple.com/documentation/security/certificate_key_and_trust_services/policies/1563600-revocation_policy_constants). + public struct Options: OptionSet { + /// Perform revocation checking using the CRL (Certification Revocation List) method. + public static let crl = Options(rawValue: kSecRevocationCRLMethod) + /// Consult only locally cached replies; do not use network access. + public static let networkAccessDisabled = Options(rawValue: kSecRevocationNetworkAccessDisabled) + /// Perform revocation checking using OCSP (Online Certificate Status Protocol). + public static let ocsp = Options(rawValue: kSecRevocationOCSPMethod) + /// Prefer CRL revocation checking over OCSP; by default, OCSP is preferred. + public static let preferCRL = Options(rawValue: kSecRevocationPreferCRL) + /// Require a positive response to pass the policy. If the flag is not set, revocation checking is done on a + /// "best attempt" basis, where failure to reach the server is not considered fatal. + public static let requirePositiveResponse = Options(rawValue: kSecRevocationRequirePositiveResponse) + /// Perform either OCSP or CRL checking. The checking is performed according to the method(s) specified in the + /// certificate and the value of `preferCRL`. + public static let any = Options(rawValue: kSecRevocationUseAnyAvailableMethod) + + /// The raw value of the option. + public let rawValue: CFOptionFlags + + /// Creates an `Options` value with the given `CFOptionFlags`. + /// + /// - Parameter rawValue: The `CFOptionFlags` value to initialize with. + public init(rawValue: CFOptionFlags) { + self.rawValue = rawValue + } + } + + private let performDefaultValidation: Bool + private let validateHost: Bool + private let options: Options + + /// Creates a `RevocationTrustEvaluator`. + /// + /// - Note: Default and host validation will fail when using this evaluator with self-signed certificates. Use + /// `PinnedCertificatesTrustEvaluator` if you need to use self-signed certificates. + /// + /// - Parameters: + /// - performDefaultValidation: Determines whether default validation should be performed in addition to + /// evaluating the pinned certificates. Defaults to `true`. + /// - validateHost: Determines whether or not the evaluator should validate the host, in addition + /// to performing the default evaluation, even if `performDefaultValidation` is + /// `false`. Defaults to `true`. + /// - options: The `Options` to use to check the revocation status of the certificate. Defaults to `.any`. + public init(performDefaultValidation: Bool = true, validateHost: Bool = true, options: Options = .any) { + self.performDefaultValidation = performDefaultValidation + self.validateHost = validateHost + self.options = options + } + + public func evaluate(_ trust: SecTrust, forHost host: String) throws { + if performDefaultValidation { + try trust.performDefaultEvaluation(forHost: host) + } + + if validateHost { + try trust.validateHost(host) + } + + try trust.validate(policy: .revocation(options: options)) { (status, result) in + AFError.serverTrustEvaluationFailed(reason: .revocationCheckFailed(output: .init(host, trust, status, result), options: options)) + } + } +} + +/// Uses the pinned certificates to validate the server trust. The server trust is considered valid if one of the pinned +/// certificates match one of the server certificates. By validating both the certificate chain and host, certificate +/// pinning provides a very secure form of server trust validation mitigating most, if not all, MITM attacks. +/// Applications are encouraged to always validate the host and require a valid certificate chain in production +/// environments. +public final class PinnedCertificatesTrustEvaluator: ServerTrustEvaluating { + private let certificates: [SecCertificate] + private let acceptSelfSignedCertificates: Bool + private let performDefaultValidation: Bool + private let validateHost: Bool + + /// Creates a `PinnedCertificatesTrustEvaluator`. + /// + /// - Parameters: + /// - certificates: The certificates to use to evalute the trust. Defaults to all `cer`, `crt`, + /// `der` certificates in `Bundle.main`. + /// - acceptSelfSignedCertificates: Adds the provided certificates as anchors for the trust evaulation, allowing + /// self-signed certificates to pass. Defaults to `false`. THIS SETTING SHOULD BE + /// FALSE IN PRODUCTION! + /// - performDefaultValidation: Determines whether default validation should be performed in addition to + /// evaluating the pinned certificates. Defaults to `true`. + /// - validateHost: Determines whether or not the evaluator should validate the host, in addition + /// to performing the default evaluation, even if `performDefaultValidation` is + /// `false`. Defaults to `true`. + public init(certificates: [SecCertificate] = Bundle.main.certificates, + acceptSelfSignedCertificates: Bool = false, + performDefaultValidation: Bool = true, + validateHost: Bool = true) { + self.certificates = certificates + self.acceptSelfSignedCertificates = acceptSelfSignedCertificates + self.performDefaultValidation = performDefaultValidation + self.validateHost = validateHost + } + + public func evaluate(_ trust: SecTrust, forHost host: String) throws { + guard !certificates.isEmpty else { + throw AFError.serverTrustEvaluationFailed(reason: .noCertificatesFound) + } + + if acceptSelfSignedCertificates { + try trust.setAnchorCertificates(certificates) + } + + if performDefaultValidation { + try trust.performDefaultEvaluation(forHost: host) + } + + if validateHost { + try trust.validateHost(host) + } + + let serverCertificatesData = Set(trust.certificateData) + let pinnedCertificatesData = Set(certificates.data) + let pinnedCertificatesInServerData = !serverCertificatesData.isDisjoint(with: pinnedCertificatesData) + if !pinnedCertificatesInServerData { + throw AFError.serverTrustEvaluationFailed(reason: .certificatePinningFailed(host: host, + trust: trust, + pinnedCertificates: certificates, + serverCertificates: trust.certificates)) + } + } +} + +/// Uses the pinned public keys to validate the server trust. The server trust is considered valid if one of the pinned +/// public keys match one of the server certificate public keys. By validating both the certificate chain and host, +/// public key pinning provides a very secure form of server trust validation mitigating most, if not all, MITM attacks. +/// Applications are encouraged to always validate the host and require a valid certificate chain in production +/// environments. +public final class PublicKeysTrustEvaluator: ServerTrustEvaluating { + private let keys: [SecKey] + private let performDefaultValidation: Bool + private let validateHost: Bool + + /// Creates a `PublicKeysTrustEvaluator`. + /// + /// - Note: Default and host validation will fail when using this evaluator with self-signed certificates. Use + /// `PinnedCertificatesTrustEvaluator` if you need to use self-signed certificates. + /// + /// - Parameters: + /// - keys: The `SecKey`s to use to validate public keys. Defaults to the public keys of all + /// certificates included in the main bundle. + /// - performDefaultValidation: Determines whether default validation should be performed in addition to + /// evaluating the pinned certificates. Defaults to `true`. + /// - validateHost: Determines whether or not the evaluator should validate the host, in addition to + /// performing the default evaluation, even if `performDefaultValidation` is `false`. + /// Defaults to `true`. + public init(keys: [SecKey] = Bundle.main.publicKeys, + performDefaultValidation: Bool = true, + validateHost: Bool = true) { + self.keys = keys + self.performDefaultValidation = performDefaultValidation + self.validateHost = validateHost + } + + public func evaluate(_ trust: SecTrust, forHost host: String) throws { + guard !keys.isEmpty else { + throw AFError.serverTrustEvaluationFailed(reason: .noPublicKeysFound) + } + + if performDefaultValidation { + try trust.performDefaultEvaluation(forHost: host) + } + + if validateHost { + try trust.validateHost(host) + } + + let pinnedKeysInServerKeys: Bool = { + for serverPublicKey in trust.publicKeys as [AnyHashable] { + for pinnedPublicKey in keys as [AnyHashable] { + if serverPublicKey == pinnedPublicKey { + return true + } + } + } + return false + }() + + if !pinnedKeysInServerKeys { + throw AFError.serverTrustEvaluationFailed(reason: .publicKeyPinningFailed(host: host, + trust: trust, + pinnedKeys: keys, + serverKeys: trust.publicKeys)) + } + } +} + +/// Uses the provided evaluators to validate the server trust. The trust is only considered valid if all of the +/// evaluators consider it valid. +public final class CompositeTrustEvaluator: ServerTrustEvaluating { + private let evaluators: [ServerTrustEvaluating] + + /// Creates a `CompositeTrustEvaluator`. + /// + /// - Parameter evaluators: The `ServerTrustEvaluating` values used to evaluate the server trust. + public init(evaluators: [ServerTrustEvaluating]) { + self.evaluators = evaluators + } + + public func evaluate(_ trust: SecTrust, forHost host: String) throws { + try evaluators.evaluate(trust, forHost: host) + } +} + +/// Disables all evaluation which in turn will always consider any server trust as valid. +/// +/// THIS EVALUATOR SHOULD NEVER BE USED IN PRODUCTION! +public final class DisabledEvaluator: ServerTrustEvaluating { + public init() { } + + public func evaluate(_ trust: SecTrust, forHost host: String) throws { } +} + +extension Bundle { + /// Returns all valid `cer`, `crt`, and `der` certificates in the bundle. + public var certificates: [SecCertificate] { + return paths(forResourcesOfTypes: [".cer", ".CER", ".crt", ".CRT", ".der", ".DER"]).compactMap { path in + guard + let certificateData = try? Data(contentsOf: URL(fileURLWithPath: path)) as CFData, + let certificate = SecCertificateCreateWithData(nil, certificateData) else { return nil } + + return certificate + } + } + + /// Returns all public keys for the valid certificates in the bundle. + public var publicKeys: [SecKey] { + return certificates.publicKeys + } + + /// Returns all pathnames for the resources identified by the provided file extensions. + /// + /// - Parameter types: The filename extensions locate. + /// - Returns: All pathnames for the given filename extensions. + func paths(forResourcesOfTypes types: [String]) -> [String] { + return Array(Set(types.flatMap { paths(forResourcesOfType: $0, inDirectory: nil) })) + } +} + +public extension SecTrust { + func validate(policy: SecPolicy, errorProducer: (_ status: OSStatus, _ result: SecTrustResultType) -> Error) throws { + try apply(policy: policy).validate(errorProducer: errorProducer) + } + + func validate(errorProducer: (_ status: OSStatus, _ result: SecTrustResultType) -> Error) throws { + var result = SecTrustResultType.invalid + let status = SecTrustEvaluate(self, &result) + + guard status.isSuccess && result.isSuccess else { + throw errorProducer(status, result) + } + } + + func apply(policy: SecPolicy) throws -> SecTrust { + let status = SecTrustSetPolicies(self, policy) + + guard status.isSuccess else { + throw AFError.serverTrustEvaluationFailed(reason: .policyApplicationFailed(trust: self, + policy: policy, + status: status)) + } + + return self + } + + func setAnchorCertificates(_ certificates: [SecCertificate]) throws { + // Add additional anchor certificates. + let status = SecTrustSetAnchorCertificates(self, certificates as CFArray) + guard status.isSuccess else { + throw AFError.serverTrustEvaluationFailed(reason: .settingAnchorCertificatesFailed(status: status, + certificates: certificates)) + } + + // Reenable system anchor certificates. + let systemStatus = SecTrustSetAnchorCertificatesOnly(self, true) + guard systemStatus.isSuccess else { + throw AFError.serverTrustEvaluationFailed(reason: .settingAnchorCertificatesFailed(status: systemStatus, + certificates: certificates)) + } + } + + /// The public keys contained in `self`. + var publicKeys: [SecKey] { + return certificates.publicKeys + } + + /// The `Data` values for all certificates contained in `self`. + var certificateData: [Data] { + return certificates.data + } + + var certificates: [SecCertificate] { + return (0.. SecPolicy { + return SecPolicyCreateSSL(true, hostname as CFString) + } + static func revocation(options: RevocationTrustEvaluator.Options) throws -> SecPolicy { + guard let policy = SecPolicyCreateRevocation(options.rawValue) else { + throw AFError.serverTrustEvaluationFailed(reason: .revocationPolicyCreationFailed) + } + + return policy + } +} + +extension Array where Element == SecCertificate { + /// All `Data` values for the contained `SecCertificate`s. + var data: [Data] { + return map { SecCertificateCopyData($0) as Data } + } + + /// All public `SecKey` values for the contained `SecCertificate`s. + public var publicKeys: [SecKey] { + return compactMap { $0.publicKey } + } +} + +extension SecCertificate { + /// The public key for `self`, if it can be extracted. + var publicKey: SecKey? { + let policy = SecPolicyCreateBasicX509() + var trust: SecTrust? + let trustCreationStatus = SecTrustCreateWithCertificates(self, policy, &trust) + + guard let createdTrust = trust, trustCreationStatus == errSecSuccess else { return nil } + + return SecTrustCopyPublicKey(createdTrust) + } +} + +extension OSStatus { + var isSuccess: Bool { return self == errSecSuccess } +} + +extension SecTrustResultType { + var isSuccess: Bool { + return (self == .unspecified || self == .proceed) + } +} diff --git a/Source/ServerTrustPolicy.swift b/Source/ServerTrustPolicy.swift deleted file mode 100644 index 7f44c8d2c..000000000 --- a/Source/ServerTrustPolicy.swift +++ /dev/null @@ -1,307 +0,0 @@ -// -// ServerTrustPolicy.swift -// -// Copyright (c) 2014-2018 Alamofire Software Foundation (http://alamofire.org/) -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -// THE SOFTWARE. -// - -import Foundation - -/// Responsible for managing the mapping of `ServerTrustPolicy` objects to a given host. -open class ServerTrustPolicyManager { - /// The dictionary of policies mapped to a particular host. - public let policies: [String: ServerTrustPolicy] - - /// Initializes the `ServerTrustPolicyManager` instance with the given policies. - /// - /// Since different servers and web services can have different leaf certificates, intermediate and even root - /// certficates, it is important to have the flexibility to specify evaluation policies on a per host basis. This - /// allows for scenarios such as using default evaluation for host1, certificate pinning for host2, public key - /// pinning for host3 and disabling evaluation for host4. - /// - /// - parameter policies: A dictionary of all policies mapped to a particular host. - /// - /// - returns: The new `ServerTrustPolicyManager` instance. - public init(policies: [String: ServerTrustPolicy]) { - self.policies = policies - } - - /// Returns the `ServerTrustPolicy` for the given host if applicable. - /// - /// By default, this method will return the policy that perfectly matches the given host. Subclasses could override - /// this method and implement more complex mapping implementations such as wildcards. - /// - /// - parameter host: The host to use when searching for a matching policy. - /// - /// - returns: The server trust policy for the given host if found. - open func serverTrustPolicy(forHost host: String) -> ServerTrustPolicy? { - return policies[host] - } -} - -// MARK: - - -extension URLSession { - private struct AssociatedKeys { - static var managerKey = "URLSession.ServerTrustPolicyManager" - } - - var serverTrustPolicyManager: ServerTrustPolicyManager? { - get { - return objc_getAssociatedObject(self, &AssociatedKeys.managerKey) as? ServerTrustPolicyManager - } - set (manager) { - objc_setAssociatedObject(self, &AssociatedKeys.managerKey, manager, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) - } - } -} - -// MARK: - ServerTrustPolicy - -/// The `ServerTrustPolicy` evaluates the server trust generally provided by an `NSURLAuthenticationChallenge` when -/// connecting to a server over a secure HTTPS connection. The policy configuration then evaluates the server trust -/// with a given set of criteria to determine whether the server trust is valid and the connection should be made. -/// -/// Using pinned certificates or public keys for evaluation helps prevent man-in-the-middle (MITM) attacks and other -/// vulnerabilities. Applications dealing with sensitive customer data or financial information are strongly encouraged -/// to route all communication over an HTTPS connection with pinning enabled. -/// -/// - performDefaultEvaluation: Uses the default server trust evaluation while allowing you to control whether to -/// validate the host provided by the challenge. Applications are encouraged to always -/// validate the host in production environments to guarantee the validity of the server's -/// certificate chain. -/// -/// - performRevokedEvaluation: Uses the default and revoked server trust evaluations allowing you to control whether to -/// validate the host provided by the challenge as well as specify the revocation flags for -/// testing for revoked certificates. Apple platforms did not start testing for revoked -/// certificates automatically until iOS 10.1, macOS 10.12 and tvOS 10.1 which is -/// demonstrated in our TLS tests. Applications are encouraged to always validate the host -/// in production environments to guarantee the validity of the server's certificate chain. -/// -/// - pinCertificates: Uses the pinned certificates to validate the server trust. The server trust is -/// considered valid if one of the pinned certificates match one of the server certificates. -/// By validating both the certificate chain and host, certificate pinning provides a very -/// secure form of server trust validation mitigating most, if not all, MITM attacks. -/// Applications are encouraged to always validate the host and require a valid certificate -/// chain in production environments. -/// -/// - pinPublicKeys: Uses the pinned public keys to validate the server trust. The server trust is considered -/// valid if one of the pinned public keys match one of the server certificate public keys. -/// By validating both the certificate chain and host, public key pinning provides a very -/// secure form of server trust validation mitigating most, if not all, MITM attacks. -/// Applications are encouraged to always validate the host and require a valid certificate -/// chain in production environments. -/// -/// - disableEvaluation: Disables all evaluation which in turn will always consider any server trust as valid. -/// -/// - customEvaluation: Uses the associated closure to evaluate the validity of the server trust. -public enum ServerTrustPolicy { - case performDefaultEvaluation(validateHost: Bool) - case performRevokedEvaluation(validateHost: Bool, revocationFlags: CFOptionFlags) - case pinCertificates(certificates: [SecCertificate], validateCertificateChain: Bool, validateHost: Bool) - case pinPublicKeys(publicKeys: [SecKey], validateCertificateChain: Bool, validateHost: Bool) - case disableEvaluation - case customEvaluation((_ serverTrust: SecTrust, _ host: String) -> Bool) - - // MARK: - Bundle Location - - /// Returns all certificates within the given bundle with a `.cer` file extension. - /// - /// - parameter bundle: The bundle to search for all `.cer` files. - /// - /// - returns: All certificates within the given bundle. - public static func certificates(in bundle: Bundle = Bundle.main) -> [SecCertificate] { - var certificates: [SecCertificate] = [] - - let paths = Set([".cer", ".CER", ".crt", ".CRT", ".der", ".DER"].map { fileExtension in - bundle.paths(forResourcesOfType: fileExtension, inDirectory: nil) - }.joined()) - - for path in paths { - if - let certificateData = try? Data(contentsOf: URL(fileURLWithPath: path)) as CFData, - let certificate = SecCertificateCreateWithData(nil, certificateData) - { - certificates.append(certificate) - } - } - - return certificates - } - - /// Returns all public keys within the given bundle with a `.cer` file extension. - /// - /// - parameter bundle: The bundle to search for all `*.cer` files. - /// - /// - returns: All public keys within the given bundle. - public static func publicKeys(in bundle: Bundle = Bundle.main) -> [SecKey] { - var publicKeys: [SecKey] = [] - - for certificate in certificates(in: bundle) { - if let publicKey = publicKey(for: certificate) { - publicKeys.append(publicKey) - } - } - - return publicKeys - } - - // MARK: - Evaluation - - /// Evaluates whether the server trust is valid for the given host. - /// - /// - parameter serverTrust: The server trust to evaluate. - /// - parameter host: The host of the challenge protection space. - /// - /// - returns: Whether the server trust is valid. - public func evaluate(_ serverTrust: SecTrust, forHost host: String) -> Bool { - var serverTrustIsValid = false - - switch self { - case let .performDefaultEvaluation(validateHost): - let policy = SecPolicyCreateSSL(true, validateHost ? host as CFString : nil) - SecTrustSetPolicies(serverTrust, policy) - - serverTrustIsValid = trustIsValid(serverTrust) - case let .performRevokedEvaluation(validateHost, revocationFlags): - let defaultPolicy = SecPolicyCreateSSL(true, validateHost ? host as CFString : nil) - let revokedPolicy = SecPolicyCreateRevocation(revocationFlags) - SecTrustSetPolicies(serverTrust, [defaultPolicy, revokedPolicy] as CFTypeRef) - - serverTrustIsValid = trustIsValid(serverTrust) - case let .pinCertificates(pinnedCertificates, validateCertificateChain, validateHost): - if validateCertificateChain { - let policy = SecPolicyCreateSSL(true, validateHost ? host as CFString : nil) - SecTrustSetPolicies(serverTrust, policy) - - SecTrustSetAnchorCertificates(serverTrust, pinnedCertificates as CFArray) - SecTrustSetAnchorCertificatesOnly(serverTrust, true) - - serverTrustIsValid = trustIsValid(serverTrust) - } else { - let serverCertificatesDataArray = certificateData(for: serverTrust) - let pinnedCertificatesDataArray = certificateData(for: pinnedCertificates) - - outerLoop: for serverCertificateData in serverCertificatesDataArray { - for pinnedCertificateData in pinnedCertificatesDataArray { - if serverCertificateData == pinnedCertificateData { - serverTrustIsValid = true - break outerLoop - } - } - } - } - case let .pinPublicKeys(pinnedPublicKeys, validateCertificateChain, validateHost): - var certificateChainEvaluationPassed = true - - if validateCertificateChain { - let policy = SecPolicyCreateSSL(true, validateHost ? host as CFString : nil) - SecTrustSetPolicies(serverTrust, policy) - - certificateChainEvaluationPassed = trustIsValid(serverTrust) - } - - if certificateChainEvaluationPassed { - outerLoop: for serverPublicKey in ServerTrustPolicy.publicKeys(for: serverTrust) as [AnyObject] { - for pinnedPublicKey in pinnedPublicKeys as [AnyObject] { - if serverPublicKey.isEqual(pinnedPublicKey) { - serverTrustIsValid = true - break outerLoop - } - } - } - } - case .disableEvaluation: - serverTrustIsValid = true - case let .customEvaluation(closure): - serverTrustIsValid = closure(serverTrust, host) - } - - return serverTrustIsValid - } - - // MARK: - Private - Trust Validation - - private func trustIsValid(_ trust: SecTrust) -> Bool { - var isValid = false - - var result = SecTrustResultType.invalid - let status = SecTrustEvaluate(trust, &result) - - if status == errSecSuccess { - let unspecified = SecTrustResultType.unspecified - let proceed = SecTrustResultType.proceed - - - isValid = result == unspecified || result == proceed - } - - return isValid - } - - // MARK: - Private - Certificate Data - - private func certificateData(for trust: SecTrust) -> [Data] { - var certificates: [SecCertificate] = [] - - for index in 0.. [Data] { - return certificates.map { SecCertificateCopyData($0) as Data } - } - - // MARK: - Private - Public Key Extraction - - private static func publicKeys(for trust: SecTrust) -> [SecKey] { - var publicKeys: [SecKey] = [] - - for index in 0.. SecKey? { - var publicKey: SecKey? - - let policy = SecPolicyCreateBasicX509() - var trust: SecTrust? - let trustCreationStatus = SecTrustCreateWithCertificates(certificate, policy, &trust) - - if let trust = trust, trustCreationStatus == errSecSuccess { - publicKey = SecTrustCopyPublicKey(trust) - } - - return publicKey - } -} diff --git a/Source/Session.swift b/Source/Session.swift new file mode 100644 index 000000000..3c09267bf --- /dev/null +++ b/Source/Session.swift @@ -0,0 +1,543 @@ +// +// SessionManager.swift +// +// Copyright (c) 2014-2018 Alamofire Software Foundation (http://alamofire.org/) +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +import Foundation + +open class Session { + public static let `default` = Session() + + public let delegate: SessionDelegate + public let rootQueue: DispatchQueue + public let requestQueue: DispatchQueue + public let serializationQueue: DispatchQueue + public let adapter: RequestAdapter? + public let retrier: RequestRetrier? + public let serverTrustManager: ServerTrustManager? + + public let session: URLSession + public let eventMonitor: CompositeEventMonitor + public let defaultEventMonitors: [EventMonitor] = [AlamofireNotifications()] + + var requestTaskMap = RequestTaskMap() + public let startRequestsImmediately: Bool + + public init(startRequestsImmediately: Bool = true, + session: URLSession, + delegate: SessionDelegate, + rootQueue: DispatchQueue, + requestQueue: DispatchQueue? = nil, + serializationQueue: DispatchQueue? = nil, + adapter: RequestAdapter? = nil, + serverTrustManager: ServerTrustManager? = nil, + retrier: RequestRetrier? = nil, + eventMonitors: [EventMonitor] = []) { + precondition(session.delegate === delegate, + "SessionManager(session:) initializer must be passed the delegate that has been assigned to the URLSession as the SessionDataProvider.") + precondition(session.delegateQueue.underlyingQueue === rootQueue, + "SessionManager(session:) intializer must be passed the DispatchQueue used as the delegateQueue's underlyingQueue as rootQueue.") + + self.startRequestsImmediately = startRequestsImmediately + self.session = session + self.delegate = delegate + self.rootQueue = rootQueue + self.requestQueue = requestQueue ?? DispatchQueue(label: "\(rootQueue.label).requestQueue", target: rootQueue) + self.serializationQueue = serializationQueue ?? DispatchQueue(label: "\(rootQueue.label).serializationQueue", target: rootQueue) + self.adapter = adapter + self.retrier = retrier + self.serverTrustManager = serverTrustManager + eventMonitor = CompositeEventMonitor(monitors: defaultEventMonitors + eventMonitors) + delegate.eventMonitor = eventMonitor + delegate.stateProvider = self + } + + public convenience init(startRequestsImmediately: Bool = true, + configuration: URLSessionConfiguration = .alamofireDefault, + delegate: SessionDelegate = SessionDelegate(), + rootQueue: DispatchQueue = DispatchQueue(label: "org.alamofire.sessionManager.rootQueue"), + requestQueue: DispatchQueue? = nil, + serializationQueue: DispatchQueue? = nil, + adapter: RequestAdapter? = nil, + serverTrustManager: ServerTrustManager? = nil, + retrier: RequestRetrier? = nil, + eventMonitors: [EventMonitor] = []) { + let delegateQueue = OperationQueue(maxConcurrentOperationCount: 1, underlyingQueue: rootQueue, name: "org.alamofire.sessionManager.sessionDelegateQueue") + let session = URLSession(configuration: configuration, delegate: delegate, delegateQueue: delegateQueue) + self.init(startRequestsImmediately: startRequestsImmediately, + session: session, + delegate: delegate, + rootQueue: rootQueue, + requestQueue: requestQueue, + serializationQueue: serializationQueue, + adapter: adapter, + serverTrustManager: serverTrustManager, + retrier: retrier, + eventMonitors: eventMonitors) + } + + deinit { + session.invalidateAndCancel() + } + + // MARK: - Request + + struct RequestConvertible: URLRequestConvertible { + let url: URLConvertible + let method: HTTPMethod + let parameters: Parameters? + let encoding: ParameterEncoding + let headers: HTTPHeaders? + + func asURLRequest() throws -> URLRequest { + let request = try URLRequest(url: url, method: method, headers: headers) + return try encoding.encode(request, with: parameters) + } + } + + open func request(_ url: URLConvertible, + method: HTTPMethod = .get, + parameters: Parameters? = nil, + encoding: ParameterEncoding = URLEncoding.default, + headers: HTTPHeaders? = nil) -> DataRequest { + let convertible = RequestConvertible(url: url, + method: method, + parameters: parameters, + encoding: encoding, + headers: headers) + return request(convertible) + } + + struct RequestEncodableConvertible: URLRequestConvertible { + let url: URLConvertible + let method: HTTPMethod + let parameters: Parameters? + let encoder: ParameterEncoder + let headers: HTTPHeaders? + + func asURLRequest() throws -> URLRequest { + let request = try URLRequest(url: url, method: method, headers: headers) + + return try parameters.map { try encoder.encode($0, into: request) } ?? request + } + } + + open func request(_ url: URLConvertible, + method: HTTPMethod = .get, + parameters: Parameters? = nil, + encoder: ParameterEncoder = JSONParameterEncoder.default, + headers: HTTPHeaders? = nil) -> DataRequest { + let convertible = RequestEncodableConvertible(url: url, + method: method, + parameters: parameters, + encoder: encoder, + headers: headers) + + return request(convertible) + } + + open func request(_ convertible: URLRequestConvertible) -> DataRequest { + let request = DataRequest(convertible: convertible, + underlyingQueue: rootQueue, + serializationQueue: serializationQueue, + eventMonitor: eventMonitor, + delegate: self) + + perform(request) + + return request + } + + // MARK: - Download + + open func download(_ convertible: URLConvertible, + method: HTTPMethod = .get, + parameters: Parameters? = nil, + encoding: ParameterEncoding = URLEncoding.default, + headers: HTTPHeaders? = nil, + to destination: DownloadRequest.Destination? = nil) -> DownloadRequest { + let convertible = RequestConvertible(url: convertible, + method: method, + parameters: parameters, + encoding: encoding, + headers: headers) + + return download(convertible, to: destination) + } + + open func download(_ convertible: URLConvertible, + method: HTTPMethod = .get, + parameters: Parameters? = nil, + encoder: ParameterEncoder = JSONParameterEncoder.default, + headers: HTTPHeaders? = nil, + to destination: DownloadRequest.Destination? = nil) -> DownloadRequest { + let convertible = RequestEncodableConvertible(url: convertible, + method: method, + parameters: parameters, + encoder: encoder, + headers: headers) + + return download(convertible, to: destination) + } + + open func download(_ convertible: URLRequestConvertible, + to destination: DownloadRequest.Destination? = nil) -> DownloadRequest { + let request = DownloadRequest(downloadable: .request(convertible), + underlyingQueue: rootQueue, + serializationQueue: serializationQueue, + eventMonitor: eventMonitor, + delegate: self, + destination: destination) + + perform(request) + + return request + } + + open func download(resumingWith data: Data, + to destination: DownloadRequest.Destination? = nil) -> DownloadRequest { + let request = DownloadRequest(downloadable: .resumeData(data), + underlyingQueue: rootQueue, + serializationQueue: serializationQueue, + eventMonitor: eventMonitor, + delegate: self, + destination: destination) + + perform(request) + + return request + } + + // MARK: - Upload + + struct ParameterlessRequestConvertible: URLRequestConvertible { + let url: URLConvertible + let method: HTTPMethod + let headers: HTTPHeaders? + + func asURLRequest() throws -> URLRequest { + return try URLRequest(url: url, method: method, headers: headers) + } + } + + struct Upload: UploadConvertible { + let request: URLRequestConvertible + let uploadable: UploadableConvertible + + func createUploadable() throws -> UploadRequest.Uploadable { + return try uploadable.createUploadable() + } + + func asURLRequest() throws -> URLRequest { + return try request.asURLRequest() + } + } + + open func upload(_ data: Data, + to convertible: URLConvertible, + method: HTTPMethod = .post, + headers: HTTPHeaders? = nil) -> UploadRequest { + let convertible = ParameterlessRequestConvertible(url: convertible, method: method, headers: headers) + + return upload(data, with: convertible) + } + + open func upload(_ data: Data, with convertible: URLRequestConvertible) -> UploadRequest { + return upload(.data(data), with: convertible) + } + + open func upload(_ fileURL: URL, + to convertible: URLConvertible, + method: HTTPMethod = .post, + headers: HTTPHeaders? = nil) -> UploadRequest { + let convertible = ParameterlessRequestConvertible(url: convertible, method: method, headers: headers) + + return upload(fileURL, with: convertible) + } + + open func upload(_ fileURL: URL, with convertible: URLRequestConvertible) -> UploadRequest { + return upload(.file(fileURL, shouldRemove: false), with: convertible) + } + + open func upload(_ stream: InputStream, + to convertible: URLConvertible, + method: HTTPMethod = .post, + headers: HTTPHeaders? = nil) -> UploadRequest { + let convertible = ParameterlessRequestConvertible(url: convertible, method: method, headers: headers) + + return upload(stream, with: convertible) + } + + open func upload(_ stream: InputStream, with convertible: URLRequestConvertible) -> UploadRequest { + return upload(.stream(stream), with: convertible) + } + + open func upload(multipartFormData: @escaping (MultipartFormData) -> Void, + usingThreshold encodingMemoryThreshold: UInt64 = MultipartUpload.encodingMemoryThreshold, + fileManager: FileManager = .default, + to url: URLConvertible, + method: HTTPMethod = .post, + headers: HTTPHeaders? = nil) -> UploadRequest { + let convertible = ParameterlessRequestConvertible(url: url, method: method, headers: headers) + + return upload(multipartFormData: multipartFormData, usingThreshold: encodingMemoryThreshold, with: convertible) + } + + open func upload(multipartFormData: @escaping (MultipartFormData) -> Void, + usingThreshold encodingMemoryThreshold: UInt64 = MultipartUpload.encodingMemoryThreshold, + fileManager: FileManager = .default, + with request: URLRequestConvertible) -> UploadRequest { + let multipartUpload = MultipartUpload(isInBackgroundSession: (session.configuration.identifier != nil), + encodingMemoryThreshold: encodingMemoryThreshold, + request: request, + fileManager: fileManager, + multipartBuilder: multipartFormData) + + return upload(multipartUpload) + } + + // MARK: - Internal API + + // MARK: Uploadable + + func upload(_ uploadable: UploadRequest.Uploadable, with convertible: URLRequestConvertible) -> UploadRequest { + let uploadable = Upload(request: convertible, uploadable: uploadable) + + return upload(uploadable) + } + + func upload(_ upload: UploadConvertible) -> UploadRequest { + let request = UploadRequest(convertible: upload, + underlyingQueue: rootQueue, + serializationQueue: serializationQueue, + eventMonitor: eventMonitor, + delegate: self) + + perform(request) + + return request + } + + // MARK: Perform + + func perform(_ request: Request) { + switch request { + case let r as DataRequest: perform(r) + case let r as UploadRequest: perform(r) + case let r as DownloadRequest: perform(r) + default: fatalError("Attempted to perform unsupported Request subclass: \(type(of: request))") + } + } + + func perform(_ request: DataRequest) { + requestQueue.async { + guard !request.isCancelled else { return } + + self.performSetupOperations(for: request, convertible: request.convertible) + } + } + + func perform(_ request: UploadRequest) { + requestQueue.async { + guard !request.isCancelled else { return } + + do { + let uploadable = try request.upload.createUploadable() + self.rootQueue.async { request.didCreateUploadable(uploadable) } + + self.performSetupOperations(for: request, convertible: request.convertible) + } catch { + self.rootQueue.async { request.didFailToCreateUploadable(with: error) } + } + } + } + + func perform(_ request: DownloadRequest) { + requestQueue.async { + switch request.downloadable { + case let .request(convertible): + self.performSetupOperations(for: request, convertible: convertible) + case let .resumeData(resumeData): + self.rootQueue.async { self.didReceiveResumeData(resumeData, for: request) } + } + } + } + + func performSetupOperations(for request: Request, convertible: URLRequestConvertible) { + do { + let initialRequest = try convertible.asURLRequest() + rootQueue.async { request.didCreateURLRequest(initialRequest) } + + guard !request.isCancelled else { return } + + if let adapter = adapter { + adapter.adapt(initialRequest) { (result) in + do { + let adaptedRequest = try result.unwrap() + self.rootQueue.async { + request.didAdaptInitialRequest(initialRequest, to: adaptedRequest) + self.didCreateURLRequest(adaptedRequest, for: request) + } + } catch { + self.rootQueue.async { request.didFailToAdaptURLRequest(initialRequest, withError: error) } + } + } + } else { + rootQueue.async { self.didCreateURLRequest(initialRequest, for: request) } + } + } catch { + rootQueue.async { request.didFailToCreateURLRequest(with: error) } + } + } + + // MARK: - Task Handling + + func didCreateURLRequest(_ urlRequest: URLRequest, for request: Request) { + guard !request.isCancelled else { return } + + let task = request.task(for: urlRequest, using: session) + requestTaskMap[request] = task + request.didCreateTask(task) + + resumeOrSuspendTask(task, ifNecessaryForRequest: request) + } + + func didReceiveResumeData(_ data: Data, for request: DownloadRequest) { + guard !request.isCancelled else { return } + + let task = request.task(forResumeData: data, using: session) + requestTaskMap[request] = task + request.didCreateTask(task) + + resumeOrSuspendTask(task, ifNecessaryForRequest: request) + } + + func resumeOrSuspendTask(_ task: URLSessionTask, ifNecessaryForRequest request: Request) { + if startRequestsImmediately || request.isResumed { + task.resume() + request.didResume() + } + + if request.isSuspended { + task.suspend() + request.didSuspend() + } + } +} + +// MARK: - RequestDelegate + +extension Session: RequestDelegate { + public var sessionConfiguration: URLSessionConfiguration { + return session.configuration + } + + public func willRetryRequest(_ request: Request) -> Bool { + return (retrier != nil) + } + + public func retryRequest(_ request: Request, ifNecessaryWithError error: Error) { + guard let retrier = retrier else { return } + + retrier.should(self, retry: request, with: error) { (shouldRetry, retryInterval) in + guard !request.isCancelled else { return } + + self.rootQueue.async { + guard !request.isCancelled else { return } + + guard shouldRetry else { request.finish(); return } + + self.rootQueue.after(retryInterval) { + guard !request.isCancelled else { return } + + request.requestIsRetrying() + self.perform(request) + } + } + } + } + + public func cancelRequest(_ request: Request) { + rootQueue.async { + guard let task = self.requestTaskMap[request] else { + request.didCancel() + request.finish() + return + } + + task.cancel() + request.didCancel() + } + } + + public func cancelDownloadRequest(_ request: DownloadRequest, byProducingResumeData: @escaping (Data?) -> Void) { + rootQueue.async { + guard let downloadTask = self.requestTaskMap[request] as? URLSessionDownloadTask else { + request.didCancel() + request.finish() + return + } + + downloadTask.cancel { (data) in + self.rootQueue.async { + byProducingResumeData(data) + request.didCancel() + } + } + } + } + + public func suspendRequest(_ request: Request) { + rootQueue.async { + guard !request.isCancelled, let task = self.requestTaskMap[request] else { return } + + task.suspend() + request.didSuspend() + } + } + + public func resumeRequest(_ request: Request) { + rootQueue.async { + guard !request.isCancelled, let task = self.requestTaskMap[request] else { return } + + task.resume() + request.didResume() + } + } +} + +// MARK: - SessionDelegateDelegate + +extension Session: SessionStateProvider { + public func request(for task: URLSessionTask) -> Request? { + return requestTaskMap[task] + } + + public func didCompleteTask(_ task: URLSessionTask) { + requestTaskMap[task] = nil + } + + public func credential(for task: URLSessionTask, protectionSpace: URLProtectionSpace) -> URLCredential? { + return requestTaskMap[task]?.credential ?? + session.configuration.urlCredentialStorage?.defaultCredential(for: protectionSpace) + } +} diff --git a/Source/SessionDelegate.swift b/Source/SessionDelegate.swift deleted file mode 100644 index 03bcb7ced..000000000 --- a/Source/SessionDelegate.swift +++ /dev/null @@ -1,725 +0,0 @@ -// -// SessionDelegate.swift -// -// Copyright (c) 2014-2018 Alamofire Software Foundation (http://alamofire.org/) -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -// THE SOFTWARE. -// - -import Foundation - -/// Responsible for handling all delegate callbacks for the underlying session. -open class SessionDelegate: NSObject { - - // MARK: URLSessionDelegate Overrides - - /// Overrides default behavior for URLSessionDelegate method `urlSession(_:didBecomeInvalidWithError:)`. - open var sessionDidBecomeInvalidWithError: ((URLSession, Error?) -> Void)? - - /// Overrides default behavior for URLSessionDelegate method `urlSession(_:didReceive:completionHandler:)`. - open var sessionDidReceiveChallenge: ((URLSession, URLAuthenticationChallenge) -> (URLSession.AuthChallengeDisposition, URLCredential?))? - - /// Overrides all behavior for URLSessionDelegate method `urlSession(_:didReceive:completionHandler:)` and requires the caller to call the `completionHandler`. - open var sessionDidReceiveChallengeWithCompletion: ((URLSession, URLAuthenticationChallenge, @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) -> Void)? - - /// Overrides default behavior for URLSessionDelegate method `urlSessionDidFinishEvents(forBackgroundURLSession:)`. - open var sessionDidFinishEventsForBackgroundURLSession: ((URLSession) -> Void)? - - // MARK: URLSessionTaskDelegate Overrides - - /// Overrides default behavior for URLSessionTaskDelegate method `urlSession(_:task:willPerformHTTPRedirection:newRequest:completionHandler:)`. - open var taskWillPerformHTTPRedirection: ((URLSession, URLSessionTask, HTTPURLResponse, URLRequest) -> URLRequest?)? - - /// Overrides all behavior for URLSessionTaskDelegate method `urlSession(_:task:willPerformHTTPRedirection:newRequest:completionHandler:)` and - /// requires the caller to call the `completionHandler`. - open var taskWillPerformHTTPRedirectionWithCompletion: ((URLSession, URLSessionTask, HTTPURLResponse, URLRequest, @escaping (URLRequest?) -> Void) -> Void)? - - /// Overrides default behavior for URLSessionTaskDelegate method `urlSession(_:task:didReceive:completionHandler:)`. - open var taskDidReceiveChallenge: ((URLSession, URLSessionTask, URLAuthenticationChallenge) -> (URLSession.AuthChallengeDisposition, URLCredential?))? - - /// Overrides all behavior for URLSessionTaskDelegate method `urlSession(_:task:didReceive:completionHandler:)` and - /// requires the caller to call the `completionHandler`. - open var taskDidReceiveChallengeWithCompletion: ((URLSession, URLSessionTask, URLAuthenticationChallenge, @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) -> Void)? - - /// Overrides default behavior for URLSessionTaskDelegate method `urlSession(_:task:needNewBodyStream:)`. - open var taskNeedNewBodyStream: ((URLSession, URLSessionTask) -> InputStream?)? - - /// Overrides all behavior for URLSessionTaskDelegate method `urlSession(_:task:needNewBodyStream:)` and - /// requires the caller to call the `completionHandler`. - open var taskNeedNewBodyStreamWithCompletion: ((URLSession, URLSessionTask, @escaping (InputStream?) -> Void) -> Void)? - - /// Overrides default behavior for URLSessionTaskDelegate method `urlSession(_:task:didSendBodyData:totalBytesSent:totalBytesExpectedToSend:)`. - open var taskDidSendBodyData: ((URLSession, URLSessionTask, Int64, Int64, Int64) -> Void)? - - /// Overrides default behavior for URLSessionTaskDelegate method `urlSession(_:task:didCompleteWithError:)`. - open var taskDidComplete: ((URLSession, URLSessionTask, Error?) -> Void)? - - // MARK: URLSessionDataDelegate Overrides - - /// Overrides default behavior for URLSessionDataDelegate method `urlSession(_:dataTask:didReceive:completionHandler:)`. - open var dataTaskDidReceiveResponse: ((URLSession, URLSessionDataTask, URLResponse) -> URLSession.ResponseDisposition)? - - /// Overrides all behavior for URLSessionDataDelegate method `urlSession(_:dataTask:didReceive:completionHandler:)` and - /// requires caller to call the `completionHandler`. - open var dataTaskDidReceiveResponseWithCompletion: ((URLSession, URLSessionDataTask, URLResponse, @escaping (URLSession.ResponseDisposition) -> Void) -> Void)? - - /// Overrides default behavior for URLSessionDataDelegate method `urlSession(_:dataTask:didBecome:)`. - open var dataTaskDidBecomeDownloadTask: ((URLSession, URLSessionDataTask, URLSessionDownloadTask) -> Void)? - - /// Overrides default behavior for URLSessionDataDelegate method `urlSession(_:dataTask:didReceive:)`. - open var dataTaskDidReceiveData: ((URLSession, URLSessionDataTask, Data) -> Void)? - - /// Overrides default behavior for URLSessionDataDelegate method `urlSession(_:dataTask:willCacheResponse:completionHandler:)`. - open var dataTaskWillCacheResponse: ((URLSession, URLSessionDataTask, CachedURLResponse) -> CachedURLResponse?)? - - /// Overrides all behavior for URLSessionDataDelegate method `urlSession(_:dataTask:willCacheResponse:completionHandler:)` and - /// requires caller to call the `completionHandler`. - open var dataTaskWillCacheResponseWithCompletion: ((URLSession, URLSessionDataTask, CachedURLResponse, @escaping (CachedURLResponse?) -> Void) -> Void)? - - // MARK: URLSessionDownloadDelegate Overrides - - /// Overrides default behavior for URLSessionDownloadDelegate method `urlSession(_:downloadTask:didFinishDownloadingTo:)`. - open var downloadTaskDidFinishDownloadingToURL: ((URLSession, URLSessionDownloadTask, URL) -> Void)? - - /// Overrides default behavior for URLSessionDownloadDelegate method `urlSession(_:downloadTask:didWriteData:totalBytesWritten:totalBytesExpectedToWrite:)`. - open var downloadTaskDidWriteData: ((URLSession, URLSessionDownloadTask, Int64, Int64, Int64) -> Void)? - - /// Overrides default behavior for URLSessionDownloadDelegate method `urlSession(_:downloadTask:didResumeAtOffset:expectedTotalBytes:)`. - open var downloadTaskDidResumeAtOffset: ((URLSession, URLSessionDownloadTask, Int64, Int64) -> Void)? - - // MARK: URLSessionStreamDelegate Overrides - -#if !os(watchOS) - - /// Overrides default behavior for URLSessionStreamDelegate method `urlSession(_:readClosedFor:)`. - @available(iOS 9.0, macOS 10.11, tvOS 9.0, *) - open var streamTaskReadClosed: ((URLSession, URLSessionStreamTask) -> Void)? { - get { - return _streamTaskReadClosed as? (URLSession, URLSessionStreamTask) -> Void - } - set { - _streamTaskReadClosed = newValue - } - } - - /// Overrides default behavior for URLSessionStreamDelegate method `urlSession(_:writeClosedFor:)`. - @available(iOS 9.0, macOS 10.11, tvOS 9.0, *) - open var streamTaskWriteClosed: ((URLSession, URLSessionStreamTask) -> Void)? { - get { - return _streamTaskWriteClosed as? (URLSession, URLSessionStreamTask) -> Void - } - set { - _streamTaskWriteClosed = newValue - } - } - - /// Overrides default behavior for URLSessionStreamDelegate method `urlSession(_:betterRouteDiscoveredFor:)`. - @available(iOS 9.0, macOS 10.11, tvOS 9.0, *) - open var streamTaskBetterRouteDiscovered: ((URLSession, URLSessionStreamTask) -> Void)? { - get { - return _streamTaskBetterRouteDiscovered as? (URLSession, URLSessionStreamTask) -> Void - } - set { - _streamTaskBetterRouteDiscovered = newValue - } - } - - /// Overrides default behavior for URLSessionStreamDelegate method `urlSession(_:streamTask:didBecome:outputStream:)`. - @available(iOS 9.0, macOS 10.11, tvOS 9.0, *) - open var streamTaskDidBecomeInputAndOutputStreams: ((URLSession, URLSessionStreamTask, InputStream, OutputStream) -> Void)? { - get { - return _streamTaskDidBecomeInputStream as? (URLSession, URLSessionStreamTask, InputStream, OutputStream) -> Void - } - set { - _streamTaskDidBecomeInputStream = newValue - } - } - - var _streamTaskReadClosed: Any? - var _streamTaskWriteClosed: Any? - var _streamTaskBetterRouteDiscovered: Any? - var _streamTaskDidBecomeInputStream: Any? - -#endif - - // MARK: Properties - - var retrier: RequestRetrier? - weak var sessionManager: SessionManager? - - var requests: [Int: Request] = [:] - private let lock = NSLock() - - /// Access the task delegate for the specified task in a thread-safe manner. - open subscript(task: URLSessionTask) -> Request? { - get { - lock.lock() ; defer { lock.unlock() } - return requests[task.taskIdentifier] - } - set { - lock.lock() ; defer { lock.unlock() } - requests[task.taskIdentifier] = newValue - } - } - - // MARK: Lifecycle - - /// Initializes the `SessionDelegate` instance. - /// - /// - returns: The new `SessionDelegate` instance. - public override init() { - super.init() - } - - // MARK: NSObject Overrides - - /// Returns a `Bool` indicating whether the `SessionDelegate` implements or inherits a method that can respond - /// to a specified message. - /// - /// - parameter selector: A selector that identifies a message. - /// - /// - returns: `true` if the receiver implements or inherits a method that can respond to selector, otherwise `false`. - open override func responds(to selector: Selector) -> Bool { - #if !os(macOS) - if selector == #selector(URLSessionDelegate.urlSessionDidFinishEvents(forBackgroundURLSession:)) { - return sessionDidFinishEventsForBackgroundURLSession != nil - } - #endif - - #if !os(watchOS) - if #available(iOS 9.0, macOS 10.11, tvOS 9.0, *) { - switch selector { - case #selector(URLSessionStreamDelegate.urlSession(_:readClosedFor:)): - return streamTaskReadClosed != nil - case #selector(URLSessionStreamDelegate.urlSession(_:writeClosedFor:)): - return streamTaskWriteClosed != nil - case #selector(URLSessionStreamDelegate.urlSession(_:betterRouteDiscoveredFor:)): - return streamTaskBetterRouteDiscovered != nil - case #selector(URLSessionStreamDelegate.urlSession(_:streamTask:didBecome:outputStream:)): - return streamTaskDidBecomeInputAndOutputStreams != nil - default: - break - } - } - #endif - - switch selector { - case #selector(URLSessionDelegate.urlSession(_:didBecomeInvalidWithError:)): - return sessionDidBecomeInvalidWithError != nil - case #selector(URLSessionDelegate.urlSession(_:didReceive:completionHandler:)): - return (sessionDidReceiveChallenge != nil || sessionDidReceiveChallengeWithCompletion != nil) - case #selector(URLSessionTaskDelegate.urlSession(_:task:willPerformHTTPRedirection:newRequest:completionHandler:)): - return (taskWillPerformHTTPRedirection != nil || taskWillPerformHTTPRedirectionWithCompletion != nil) - case #selector(URLSessionDataDelegate.urlSession(_:dataTask:didReceive:completionHandler:)): - return (dataTaskDidReceiveResponse != nil || dataTaskDidReceiveResponseWithCompletion != nil) - default: - return type(of: self).instancesRespond(to: selector) - } - } -} - -// MARK: - URLSessionDelegate - -extension SessionDelegate: URLSessionDelegate { - /// Tells the delegate that the session has been invalidated. - /// - /// - parameter session: The session object that was invalidated. - /// - parameter error: The error that caused invalidation, or nil if the invalidation was explicit. - open func urlSession(_ session: URLSession, didBecomeInvalidWithError error: Error?) { - sessionDidBecomeInvalidWithError?(session, error) - } - - /// Requests credentials from the delegate in response to a session-level authentication request from the - /// remote server. - /// - /// - parameter session: The session containing the task that requested authentication. - /// - parameter challenge: An object that contains the request for authentication. - /// - parameter completionHandler: A handler that your delegate method must call providing the disposition - /// and credential. - open func urlSession( - _ session: URLSession, - didReceive challenge: URLAuthenticationChallenge, - completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) - { - guard sessionDidReceiveChallengeWithCompletion == nil else { - sessionDidReceiveChallengeWithCompletion?(session, challenge, completionHandler) - return - } - - var disposition: URLSession.AuthChallengeDisposition = .performDefaultHandling - var credential: URLCredential? - - if let sessionDidReceiveChallenge = sessionDidReceiveChallenge { - (disposition, credential) = sessionDidReceiveChallenge(session, challenge) - } else if challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodServerTrust { - let host = challenge.protectionSpace.host - - if - let serverTrustPolicy = session.serverTrustPolicyManager?.serverTrustPolicy(forHost: host), - let serverTrust = challenge.protectionSpace.serverTrust - { - if serverTrustPolicy.evaluate(serverTrust, forHost: host) { - disposition = .useCredential - credential = URLCredential(trust: serverTrust) - } else { - disposition = .cancelAuthenticationChallenge - } - } - } - - completionHandler(disposition, credential) - } - -#if !os(macOS) - - /// Tells the delegate that all messages enqueued for a session have been delivered. - /// - /// - parameter session: The session that no longer has any outstanding requests. - open func urlSessionDidFinishEvents(forBackgroundURLSession session: URLSession) { - sessionDidFinishEventsForBackgroundURLSession?(session) - } - -#endif -} - -// MARK: - URLSessionTaskDelegate - -extension SessionDelegate: URLSessionTaskDelegate { - /// Tells the delegate that the remote server requested an HTTP redirect. - /// - /// - parameter session: The session containing the task whose request resulted in a redirect. - /// - parameter task: The task whose request resulted in a redirect. - /// - parameter response: An object containing the server’s response to the original request. - /// - parameter request: A URL request object filled out with the new location. - /// - parameter completionHandler: A closure that your handler should call with either the value of the request - /// parameter, a modified URL request object, or NULL to refuse the redirect and - /// return the body of the redirect response. - open func urlSession( - _ session: URLSession, - task: URLSessionTask, - willPerformHTTPRedirection response: HTTPURLResponse, - newRequest request: URLRequest, - completionHandler: @escaping (URLRequest?) -> Void) - { - guard taskWillPerformHTTPRedirectionWithCompletion == nil else { - taskWillPerformHTTPRedirectionWithCompletion?(session, task, response, request, completionHandler) - return - } - - var redirectRequest: URLRequest? = request - - if let taskWillPerformHTTPRedirection = taskWillPerformHTTPRedirection { - redirectRequest = taskWillPerformHTTPRedirection(session, task, response, request) - } - - completionHandler(redirectRequest) - } - - /// Requests credentials from the delegate in response to an authentication request from the remote server. - /// - /// - parameter session: The session containing the task whose request requires authentication. - /// - parameter task: The task whose request requires authentication. - /// - parameter challenge: An object that contains the request for authentication. - /// - parameter completionHandler: A handler that your delegate method must call providing the disposition - /// and credential. - open func urlSession( - _ session: URLSession, - task: URLSessionTask, - didReceive challenge: URLAuthenticationChallenge, - completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) - { - guard taskDidReceiveChallengeWithCompletion == nil else { - taskDidReceiveChallengeWithCompletion?(session, task, challenge, completionHandler) - return - } - - if let taskDidReceiveChallenge = taskDidReceiveChallenge { - let result = taskDidReceiveChallenge(session, task, challenge) - completionHandler(result.0, result.1) - } else if let delegate = self[task]?.delegate { - delegate.urlSession( - session, - task: task, - didReceive: challenge, - completionHandler: completionHandler - ) - } else { - urlSession(session, didReceive: challenge, completionHandler: completionHandler) - } - } - - /// Tells the delegate when a task requires a new request body stream to send to the remote server. - /// - /// - parameter session: The session containing the task that needs a new body stream. - /// - parameter task: The task that needs a new body stream. - /// - parameter completionHandler: A completion handler that your delegate method should call with the new body stream. - open func urlSession( - _ session: URLSession, - task: URLSessionTask, - needNewBodyStream completionHandler: @escaping (InputStream?) -> Void) - { - guard taskNeedNewBodyStreamWithCompletion == nil else { - taskNeedNewBodyStreamWithCompletion?(session, task, completionHandler) - return - } - - if let taskNeedNewBodyStream = taskNeedNewBodyStream { - completionHandler(taskNeedNewBodyStream(session, task)) - } else if let delegate = self[task]?.delegate { - delegate.urlSession(session, task: task, needNewBodyStream: completionHandler) - } - } - - /// Periodically informs the delegate of the progress of sending body content to the server. - /// - /// - parameter session: The session containing the data task. - /// - parameter task: The data task. - /// - parameter bytesSent: The number of bytes sent since the last time this delegate method was called. - /// - parameter totalBytesSent: The total number of bytes sent so far. - /// - parameter totalBytesExpectedToSend: The expected length of the body data. - open func urlSession( - _ session: URLSession, - task: URLSessionTask, - didSendBodyData bytesSent: Int64, - totalBytesSent: Int64, - totalBytesExpectedToSend: Int64) - { - if let taskDidSendBodyData = taskDidSendBodyData { - taskDidSendBodyData(session, task, bytesSent, totalBytesSent, totalBytesExpectedToSend) - } else if let delegate = self[task]?.delegate as? UploadTaskDelegate { - delegate.URLSession( - session, - task: task, - didSendBodyData: bytesSent, - totalBytesSent: totalBytesSent, - totalBytesExpectedToSend: totalBytesExpectedToSend - ) - } - } - -#if !os(watchOS) - - /// Tells the delegate that the session finished collecting metrics for the task. - /// - /// - parameter session: The session collecting the metrics. - /// - parameter task: The task whose metrics have been collected. - /// - parameter metrics: The collected metrics. - @available(iOS 10.0, macOS 10.12, tvOS 10.0, *) - @objc(URLSession:task:didFinishCollectingMetrics:) - open func urlSession(_ session: URLSession, task: URLSessionTask, didFinishCollecting metrics: URLSessionTaskMetrics) { - self[task]?.delegate.metrics = metrics - } - -#endif - - /// Tells the delegate that the task finished transferring data. - /// - /// - parameter session: The session containing the task whose request finished transferring data. - /// - parameter task: The task whose request finished transferring data. - /// - parameter error: If an error occurred, an error object indicating how the transfer failed, otherwise nil. - open func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) { - /// Executed after it is determined that the request is not going to be retried - let completeTask: (URLSession, URLSessionTask, Error?) -> Void = { [weak self] session, task, error in - guard let strongSelf = self else { return } - - strongSelf.taskDidComplete?(session, task, error) - - strongSelf[task]?.delegate.urlSession(session, task: task, didCompleteWithError: error) - - var userInfo: [String: Any] = [Notification.Key.Task: task] - - if let data = (strongSelf[task]?.delegate as? DataTaskDelegate)?.data { - userInfo[Notification.Key.ResponseData] = data - } - - NotificationCenter.default.post( - name: Notification.Name.Task.DidComplete, - object: strongSelf, - userInfo: userInfo - ) - - strongSelf[task] = nil - } - - guard let request = self[task], let sessionManager = sessionManager else { - completeTask(session, task, error) - return - } - - // Run all validations on the request before checking if an error occurred - request.validations.forEach { $0() } - - // Determine whether an error has occurred - var error: Error? = error - - if request.delegate.error != nil { - error = request.delegate.error - } - - /// If an error occurred and the retrier is set, asynchronously ask the retrier if the request - /// should be retried. Otherwise, complete the task by notifying the task delegate. - if let retrier = retrier, let error = error { - retrier.should(sessionManager, retry: request, with: error) { [weak self] shouldRetry, timeDelay in - guard shouldRetry else { completeTask(session, task, error) ; return } - - DispatchQueue.utility.after(timeDelay) { [weak self] in - guard let strongSelf = self else { return } - - let retrySucceeded = strongSelf.sessionManager?.retry(request) ?? false - - if retrySucceeded, let task = request.task { - strongSelf[task] = request - return - } else { - completeTask(session, task, error) - } - } - } - } else { - completeTask(session, task, error) - } - } -} - -// MARK: - URLSessionDataDelegate - -extension SessionDelegate: URLSessionDataDelegate { - /// Tells the delegate that the data task received the initial reply (headers) from the server. - /// - /// - parameter session: The session containing the data task that received an initial reply. - /// - parameter dataTask: The data task that received an initial reply. - /// - parameter response: A URL response object populated with headers. - /// - parameter completionHandler: A completion handler that your code calls to continue the transfer, passing a - /// constant to indicate whether the transfer should continue as a data task or - /// should become a download task. - open func urlSession( - _ session: URLSession, - dataTask: URLSessionDataTask, - didReceive response: URLResponse, - completionHandler: @escaping (URLSession.ResponseDisposition) -> Void) - { - guard dataTaskDidReceiveResponseWithCompletion == nil else { - dataTaskDidReceiveResponseWithCompletion?(session, dataTask, response, completionHandler) - return - } - - var disposition: URLSession.ResponseDisposition = .allow - - if let dataTaskDidReceiveResponse = dataTaskDidReceiveResponse { - disposition = dataTaskDidReceiveResponse(session, dataTask, response) - } - - completionHandler(disposition) - } - - /// Tells the delegate that the data task was changed to a download task. - /// - /// - parameter session: The session containing the task that was replaced by a download task. - /// - parameter dataTask: The data task that was replaced by a download task. - /// - parameter downloadTask: The new download task that replaced the data task. - open func urlSession( - _ session: URLSession, - dataTask: URLSessionDataTask, - didBecome downloadTask: URLSessionDownloadTask) - { - if let dataTaskDidBecomeDownloadTask = dataTaskDidBecomeDownloadTask { - dataTaskDidBecomeDownloadTask(session, dataTask, downloadTask) - } else { - self[downloadTask]?.delegate = DownloadTaskDelegate(task: downloadTask) - } - } - - /// Tells the delegate that the data task has received some of the expected data. - /// - /// - parameter session: The session containing the data task that provided data. - /// - parameter dataTask: The data task that provided data. - /// - parameter data: A data object containing the transferred data. - open func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) { - if let dataTaskDidReceiveData = dataTaskDidReceiveData { - dataTaskDidReceiveData(session, dataTask, data) - } else if let delegate = self[dataTask]?.delegate as? DataTaskDelegate { - delegate.urlSession(session, dataTask: dataTask, didReceive: data) - } - } - - /// Asks the delegate whether the data (or upload) task should store the response in the cache. - /// - /// - parameter session: The session containing the data (or upload) task. - /// - parameter dataTask: The data (or upload) task. - /// - parameter proposedResponse: The default caching behavior. This behavior is determined based on the current - /// caching policy and the values of certain received headers, such as the Pragma - /// and Cache-Control headers. - /// - parameter completionHandler: A block that your handler must call, providing either the original proposed - /// response, a modified version of that response, or NULL to prevent caching the - /// response. If your delegate implements this method, it must call this completion - /// handler; otherwise, your app leaks memory. - open func urlSession( - _ session: URLSession, - dataTask: URLSessionDataTask, - willCacheResponse proposedResponse: CachedURLResponse, - completionHandler: @escaping (CachedURLResponse?) -> Void) - { - guard dataTaskWillCacheResponseWithCompletion == nil else { - dataTaskWillCacheResponseWithCompletion?(session, dataTask, proposedResponse, completionHandler) - return - } - - if let dataTaskWillCacheResponse = dataTaskWillCacheResponse { - completionHandler(dataTaskWillCacheResponse(session, dataTask, proposedResponse)) - } else if let delegate = self[dataTask]?.delegate as? DataTaskDelegate { - delegate.urlSession( - session, - dataTask: dataTask, - willCacheResponse: proposedResponse, - completionHandler: completionHandler - ) - } else { - completionHandler(proposedResponse) - } - } -} - -// MARK: - URLSessionDownloadDelegate - -extension SessionDelegate: URLSessionDownloadDelegate { - /// Tells the delegate that a download task has finished downloading. - /// - /// - parameter session: The session containing the download task that finished. - /// - parameter downloadTask: The download task that finished. - /// - parameter location: A file URL for the temporary file. Because the file is temporary, you must either - /// open the file for reading or move it to a permanent location in your app’s sandbox - /// container directory before returning from this delegate method. - open func urlSession( - _ session: URLSession, - downloadTask: URLSessionDownloadTask, - didFinishDownloadingTo location: URL) - { - if let downloadTaskDidFinishDownloadingToURL = downloadTaskDidFinishDownloadingToURL { - downloadTaskDidFinishDownloadingToURL(session, downloadTask, location) - } else if let delegate = self[downloadTask]?.delegate as? DownloadTaskDelegate { - delegate.urlSession(session, downloadTask: downloadTask, didFinishDownloadingTo: location) - } - } - - /// Periodically informs the delegate about the download’s progress. - /// - /// - parameter session: The session containing the download task. - /// - parameter downloadTask: The download task. - /// - parameter bytesWritten: The number of bytes transferred since the last time this delegate - /// method was called. - /// - parameter totalBytesWritten: The total number of bytes transferred so far. - /// - parameter totalBytesExpectedToWrite: The expected length of the file, as provided by the Content-Length - /// header. If this header was not provided, the value is - /// `NSURLSessionTransferSizeUnknown`. - open func urlSession( - _ session: URLSession, - downloadTask: URLSessionDownloadTask, - didWriteData bytesWritten: Int64, - totalBytesWritten: Int64, - totalBytesExpectedToWrite: Int64) - { - if let downloadTaskDidWriteData = downloadTaskDidWriteData { - downloadTaskDidWriteData(session, downloadTask, bytesWritten, totalBytesWritten, totalBytesExpectedToWrite) - } else if let delegate = self[downloadTask]?.delegate as? DownloadTaskDelegate { - delegate.urlSession( - session, - downloadTask: downloadTask, - didWriteData: bytesWritten, - totalBytesWritten: totalBytesWritten, - totalBytesExpectedToWrite: totalBytesExpectedToWrite - ) - } - } - - /// Tells the delegate that the download task has resumed downloading. - /// - /// - parameter session: The session containing the download task that finished. - /// - parameter downloadTask: The download task that resumed. See explanation in the discussion. - /// - parameter fileOffset: If the file's cache policy or last modified date prevents reuse of the - /// existing content, then this value is zero. Otherwise, this value is an - /// integer representing the number of bytes on disk that do not need to be - /// retrieved again. - /// - parameter expectedTotalBytes: The expected length of the file, as provided by the Content-Length header. - /// If this header was not provided, the value is NSURLSessionTransferSizeUnknown. - open func urlSession( - _ session: URLSession, - downloadTask: URLSessionDownloadTask, - didResumeAtOffset fileOffset: Int64, - expectedTotalBytes: Int64) - { - if let downloadTaskDidResumeAtOffset = downloadTaskDidResumeAtOffset { - downloadTaskDidResumeAtOffset(session, downloadTask, fileOffset, expectedTotalBytes) - } else if let delegate = self[downloadTask]?.delegate as? DownloadTaskDelegate { - delegate.urlSession( - session, - downloadTask: downloadTask, - didResumeAtOffset: fileOffset, - expectedTotalBytes: expectedTotalBytes - ) - } - } -} - -// MARK: - URLSessionStreamDelegate - -#if !os(watchOS) - -@available(iOS 9.0, macOS 10.11, tvOS 9.0, *) -extension SessionDelegate: URLSessionStreamDelegate { - /// Tells the delegate that the read side of the connection has been closed. - /// - /// - parameter session: The session. - /// - parameter streamTask: The stream task. - open func urlSession(_ session: URLSession, readClosedFor streamTask: URLSessionStreamTask) { - streamTaskReadClosed?(session, streamTask) - } - - /// Tells the delegate that the write side of the connection has been closed. - /// - /// - parameter session: The session. - /// - parameter streamTask: The stream task. - open func urlSession(_ session: URLSession, writeClosedFor streamTask: URLSessionStreamTask) { - streamTaskWriteClosed?(session, streamTask) - } - - /// Tells the delegate that the system has determined that a better route to the host is available. - /// - /// - parameter session: The session. - /// - parameter streamTask: The stream task. - open func urlSession(_ session: URLSession, betterRouteDiscoveredFor streamTask: URLSessionStreamTask) { - streamTaskBetterRouteDiscovered?(session, streamTask) - } - - /// Tells the delegate that the stream task has been completed and provides the unopened stream objects. - /// - /// - parameter session: The session. - /// - parameter streamTask: The stream task. - /// - parameter inputStream: The new input stream. - /// - parameter outputStream: The new output stream. - open func urlSession( - _ session: URLSession, - streamTask: URLSessionStreamTask, - didBecome inputStream: InputStream, - outputStream: OutputStream) - { - streamTaskDidBecomeInputAndOutputStreams?(session, streamTask, inputStream, outputStream) - } -} - -#endif diff --git a/Source/SessionManager.swift b/Source/SessionManager.swift deleted file mode 100644 index c9c0e38e4..000000000 --- a/Source/SessionManager.swift +++ /dev/null @@ -1,899 +0,0 @@ -// -// SessionManager.swift -// -// Copyright (c) 2014-2018 Alamofire Software Foundation (http://alamofire.org/) -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -// THE SOFTWARE. -// - -import Foundation - -/// Responsible for creating and managing `Request` objects, as well as their underlying `NSURLSession`. -open class SessionManager { - - // MARK: - Helper Types - - /// Defines whether the `MultipartFormData` encoding was successful and contains result of the encoding as - /// associated values. - /// - /// - Success: Represents a successful `MultipartFormData` encoding and contains the new `UploadRequest` along with - /// streaming information. - /// - Failure: Used to represent a failure in the `MultipartFormData` encoding and also contains the encoding - /// error. - public enum MultipartFormDataEncodingResult { - case success(request: UploadRequest, streamingFromDisk: Bool, streamFileURL: URL?) - case failure(Error) - } - - // MARK: - Properties - - /// A default instance of `SessionManager`, used by top-level Alamofire request methods, and suitable for use - /// directly for any ad hoc requests. - public static let `default`: SessionManager = { - let configuration = URLSessionConfiguration.default - configuration.httpAdditionalHeaders = SessionManager.defaultHTTPHeaders - - return SessionManager(configuration: configuration) - }() - - /// Creates default values for the "Accept-Encoding", "Accept-Language" and "User-Agent" headers. - public static let defaultHTTPHeaders: HTTPHeaders = { - // Accept-Encoding HTTP Header; see https://tools.ietf.org/html/rfc7230#section-4.2.3 - let acceptEncoding: String = "gzip;q=1.0, compress;q=0.5" - - // Accept-Language HTTP Header; see https://tools.ietf.org/html/rfc7231#section-5.3.5 - let acceptLanguage = Locale.preferredLanguages.prefix(6).enumerated().map { index, languageCode in - let quality = 1.0 - (Double(index) * 0.1) - return "\(languageCode);q=\(quality)" - }.joined(separator: ", ") - - // User-Agent Header; see https://tools.ietf.org/html/rfc7231#section-5.5.3 - // Example: `iOS Example/1.0 (org.alamofire.iOS-Example; build:1; iOS 10.0.0) Alamofire/4.0.0` - let userAgent: String = { - if let info = Bundle.main.infoDictionary { - let executable = info[kCFBundleExecutableKey as String] as? String ?? "Unknown" - let bundle = info[kCFBundleIdentifierKey as String] as? String ?? "Unknown" - let appVersion = info["CFBundleShortVersionString"] as? String ?? "Unknown" - let appBuild = info[kCFBundleVersionKey as String] as? String ?? "Unknown" - - let osNameVersion: String = { - let version = ProcessInfo.processInfo.operatingSystemVersion - let versionString = "\(version.majorVersion).\(version.minorVersion).\(version.patchVersion)" - - let osName: String = { - #if os(iOS) - return "iOS" - #elseif os(watchOS) - return "watchOS" - #elseif os(tvOS) - return "tvOS" - #elseif os(macOS) - return "OS X" - #elseif os(Linux) - return "Linux" - #else - return "Unknown" - #endif - }() - - return "\(osName) \(versionString)" - }() - - let alamofireVersion: String = { - guard - let afInfo = Bundle(for: SessionManager.self).infoDictionary, - let build = afInfo["CFBundleShortVersionString"] - else { return "Unknown" } - - return "Alamofire/\(build)" - }() - - return "\(executable)/\(appVersion) (\(bundle); build:\(appBuild); \(osNameVersion)) \(alamofireVersion)" - } - - return "Alamofire" - }() - - return [ - "Accept-Encoding": acceptEncoding, - "Accept-Language": acceptLanguage, - "User-Agent": userAgent - ] - }() - - /// Default memory threshold used when encoding `MultipartFormData` in bytes. - public static let multipartFormDataEncodingMemoryThreshold: UInt64 = 10_000_000 - - /// The underlying session. - public let session: URLSession - - /// The session delegate handling all the task and session delegate callbacks. - public let delegate: SessionDelegate - - /// Whether to start requests immediately after being constructed. `true` by default. - open var startRequestsImmediately: Bool = true - - /// The request adapter called each time a new request is created. - open var adapter: RequestAdapter? - - /// The request retrier called each time a request encounters an error to determine whether to retry the request. - open var retrier: RequestRetrier? { - get { return delegate.retrier } - set { delegate.retrier = newValue } - } - - /// The background completion handler closure provided by the UIApplicationDelegate - /// `application:handleEventsForBackgroundURLSession:completionHandler:` method. By setting the background - /// completion handler, the SessionDelegate `sessionDidFinishEventsForBackgroundURLSession` closure implementation - /// will automatically call the handler. - /// - /// If you need to handle your own events before the handler is called, then you need to override the - /// SessionDelegate `sessionDidFinishEventsForBackgroundURLSession` and manually call the handler when finished. - /// - /// `nil` by default. - open var backgroundCompletionHandler: (() -> Void)? - - let queue = DispatchQueue(label: "org.alamofire.session-manager." + UUID().uuidString) - - // MARK: - Lifecycle - - /// Creates an instance with the specified `configuration`, `delegate` and `serverTrustPolicyManager`. - /// - /// - parameter configuration: The configuration used to construct the managed session. - /// `URLSessionConfiguration.default` by default. - /// - parameter delegate: The delegate used when initializing the session. `SessionDelegate()` by - /// default. - /// - parameter serverTrustPolicyManager: The server trust policy manager to use for evaluating all server trust - /// challenges. `nil` by default. - /// - /// - returns: The new `SessionManager` instance. - public init( - configuration: URLSessionConfiguration = URLSessionConfiguration.default, - delegate: SessionDelegate = SessionDelegate(), - serverTrustPolicyManager: ServerTrustPolicyManager? = nil) - { - self.delegate = delegate - self.session = URLSession(configuration: configuration, delegate: delegate, delegateQueue: nil) - - commonInit(serverTrustPolicyManager: serverTrustPolicyManager) - } - - /// Creates an instance with the specified `session`, `delegate` and `serverTrustPolicyManager`. - /// - /// - parameter session: The URL session. - /// - parameter delegate: The delegate of the URL session. Must equal the URL session's delegate. - /// - parameter serverTrustPolicyManager: The server trust policy manager to use for evaluating all server trust - /// challenges. `nil` by default. - /// - /// - returns: The new `SessionManager` instance if the URL session's delegate matches; `nil` otherwise. - public init?( - session: URLSession, - delegate: SessionDelegate, - serverTrustPolicyManager: ServerTrustPolicyManager? = nil) - { - guard delegate === session.delegate else { return nil } - - self.delegate = delegate - self.session = session - - commonInit(serverTrustPolicyManager: serverTrustPolicyManager) - } - - private func commonInit(serverTrustPolicyManager: ServerTrustPolicyManager?) { - session.serverTrustPolicyManager = serverTrustPolicyManager - - delegate.sessionManager = self - - delegate.sessionDidFinishEventsForBackgroundURLSession = { [weak self] session in - guard let strongSelf = self else { return } - DispatchQueue.main.async { strongSelf.backgroundCompletionHandler?() } - } - } - - deinit { - session.invalidateAndCancel() - } - - // MARK: - Data Request - - /// Creates a `DataRequest` to retrieve the contents of the specified `url`, `method`, `parameters`, `encoding` - /// and `headers`. - /// - /// - parameter url: The URL. - /// - parameter method: The HTTP method. `.get` by default. - /// - parameter parameters: The parameters. `nil` by default. - /// - parameter encoding: The parameter encoding. `URLEncoding.default` by default. - /// - parameter headers: The HTTP headers. `nil` by default. - /// - /// - returns: The created `DataRequest`. - @discardableResult - open func request( - _ url: URLConvertible, - method: HTTPMethod = .get, - parameters: Parameters? = nil, - encoding: ParameterEncoding = URLEncoding.default, - headers: HTTPHeaders? = nil) - -> DataRequest - { - var originalRequest: URLRequest? - - do { - originalRequest = try URLRequest(url: url, method: method, headers: headers) - let encodedURLRequest = try encoding.encode(originalRequest!, with: parameters) - return request(encodedURLRequest) - } catch { - return request(originalRequest, failedWith: error) - } - } - - /// Creates a `DataRequest` to retrieve the contents of a URL based on the specified `urlRequest`. - /// - /// If `startRequestsImmediately` is `true`, the request will have `resume()` called before being returned. - /// - /// - parameter urlRequest: The URL request. - /// - /// - returns: The created `DataRequest`. - @discardableResult - open func request(_ urlRequest: URLRequestConvertible) -> DataRequest { - var originalRequest: URLRequest? - - do { - originalRequest = try urlRequest.asURLRequest() - let originalTask = DataRequest.Requestable(urlRequest: originalRequest!) - - let task = try originalTask.task(session: session, adapter: adapter, queue: queue) - let request = DataRequest(session: session, requestTask: .data(originalTask, task)) - - delegate[task] = request - - if startRequestsImmediately { request.resume() } - - return request - } catch { - return request(originalRequest, failedWith: error) - } - } - - // MARK: Private - Request Implementation - - private func request(_ urlRequest: URLRequest?, failedWith error: Error) -> DataRequest { - var requestTask: Request.RequestTask = .data(nil, nil) - - if let urlRequest = urlRequest { - let originalTask = DataRequest.Requestable(urlRequest: urlRequest) - requestTask = .data(originalTask, nil) - } - - let underlyingError = error.underlyingAdaptError ?? error - let request = DataRequest(session: session, requestTask: requestTask, error: underlyingError) - - if let retrier = retrier, error is AdaptError { - allowRetrier(retrier, toRetry: request, with: underlyingError) - } else { - if startRequestsImmediately { request.resume() } - } - - return request - } - - // MARK: - Download Request - - // MARK: URL Request - - /// Creates a `DownloadRequest` to retrieve the contents the specified `url`, `method`, `parameters`, `encoding`, - /// `headers` and save them to the `destination`. - /// - /// If `destination` is not specified, the contents will remain in the temporary location determined by the - /// underlying URL session. - /// - /// If `startRequestsImmediately` is `true`, the request will have `resume()` called before being returned. - /// - /// - parameter url: The URL. - /// - parameter method: The HTTP method. `.get` by default. - /// - parameter parameters: The parameters. `nil` by default. - /// - parameter encoding: The parameter encoding. `URLEncoding.default` by default. - /// - parameter headers: The HTTP headers. `nil` by default. - /// - parameter destination: The closure used to determine the destination of the downloaded file. `nil` by default. - /// - /// - returns: The created `DownloadRequest`. - @discardableResult - open func download( - _ url: URLConvertible, - method: HTTPMethod = .get, - parameters: Parameters? = nil, - encoding: ParameterEncoding = URLEncoding.default, - headers: HTTPHeaders? = nil, - to destination: DownloadRequest.DownloadFileDestination? = nil) - -> DownloadRequest - { - do { - let urlRequest = try URLRequest(url: url, method: method, headers: headers) - let encodedURLRequest = try encoding.encode(urlRequest, with: parameters) - return download(encodedURLRequest, to: destination) - } catch { - return download(nil, to: destination, failedWith: error) - } - } - - /// Creates a `DownloadRequest` to retrieve the contents of a URL based on the specified `urlRequest` and save - /// them to the `destination`. - /// - /// If `destination` is not specified, the contents will remain in the temporary location determined by the - /// underlying URL session. - /// - /// If `startRequestsImmediately` is `true`, the request will have `resume()` called before being returned. - /// - /// - parameter urlRequest: The URL request - /// - parameter destination: The closure used to determine the destination of the downloaded file. `nil` by default. - /// - /// - returns: The created `DownloadRequest`. - @discardableResult - open func download( - _ urlRequest: URLRequestConvertible, - to destination: DownloadRequest.DownloadFileDestination? = nil) - -> DownloadRequest - { - do { - let urlRequest = try urlRequest.asURLRequest() - return download(.request(urlRequest), to: destination) - } catch { - return download(nil, to: destination, failedWith: error) - } - } - - // MARK: Resume Data - - /// Creates a `DownloadRequest` from the `resumeData` produced from a previous request cancellation to retrieve - /// the contents of the original request and save them to the `destination`. - /// - /// If `destination` is not specified, the contents will remain in the temporary location determined by the - /// underlying URL session. - /// - /// If `startRequestsImmediately` is `true`, the request will have `resume()` called before being returned. - /// - /// On the latest release of all the Apple platforms (iOS 10, macOS 10.12, tvOS 10, watchOS 3), `resumeData` is broken - /// on background URL session configurations. There's an underlying bug in the `resumeData` generation logic where the - /// data is written incorrectly and will always fail to resume the download. For more information about the bug and - /// possible workarounds, please refer to the following Stack Overflow post: - /// - /// - http://stackoverflow.com/a/39347461/1342462 - /// - /// - parameter resumeData: The resume data. This is an opaque data blob produced by `URLSessionDownloadTask` - /// when a task is cancelled. See `URLSession -downloadTask(withResumeData:)` for - /// additional information. - /// - parameter destination: The closure used to determine the destination of the downloaded file. `nil` by default. - /// - /// - returns: The created `DownloadRequest`. - @discardableResult - open func download( - resumingWith resumeData: Data, - to destination: DownloadRequest.DownloadFileDestination? = nil) - -> DownloadRequest - { - return download(.resumeData(resumeData), to: destination) - } - - // MARK: Private - Download Implementation - - private func download( - _ downloadable: DownloadRequest.Downloadable, - to destination: DownloadRequest.DownloadFileDestination?) - -> DownloadRequest - { - do { - let task = try downloadable.task(session: session, adapter: adapter, queue: queue) - let download = DownloadRequest(session: session, requestTask: .download(downloadable, task)) - - download.downloadDelegate.destination = destination - - delegate[task] = download - - if startRequestsImmediately { download.resume() } - - return download - } catch { - return download(downloadable, to: destination, failedWith: error) - } - } - - private func download( - _ downloadable: DownloadRequest.Downloadable?, - to destination: DownloadRequest.DownloadFileDestination?, - failedWith error: Error) - -> DownloadRequest - { - var downloadTask: Request.RequestTask = .download(nil, nil) - - if let downloadable = downloadable { - downloadTask = .download(downloadable, nil) - } - - let underlyingError = error.underlyingAdaptError ?? error - - let download = DownloadRequest(session: session, requestTask: downloadTask, error: underlyingError) - download.downloadDelegate.destination = destination - - if let retrier = retrier, error is AdaptError { - allowRetrier(retrier, toRetry: download, with: underlyingError) - } else { - if startRequestsImmediately { download.resume() } - } - - return download - } - - // MARK: - Upload Request - - // MARK: File - - /// Creates an `UploadRequest` from the specified `url`, `method` and `headers` for uploading the `file`. - /// - /// If `startRequestsImmediately` is `true`, the request will have `resume()` called before being returned. - /// - /// - parameter file: The file to upload. - /// - parameter url: The URL. - /// - parameter method: The HTTP method. `.post` by default. - /// - parameter headers: The HTTP headers. `nil` by default. - /// - /// - returns: The created `UploadRequest`. - @discardableResult - open func upload( - _ fileURL: URL, - to url: URLConvertible, - method: HTTPMethod = .post, - headers: HTTPHeaders? = nil) - -> UploadRequest - { - do { - let urlRequest = try URLRequest(url: url, method: method, headers: headers) - return upload(fileURL, with: urlRequest) - } catch { - return upload(nil, failedWith: error) - } - } - - /// Creates a `UploadRequest` from the specified `urlRequest` for uploading the `file`. - /// - /// If `startRequestsImmediately` is `true`, the request will have `resume()` called before being returned. - /// - /// - parameter file: The file to upload. - /// - parameter urlRequest: The URL request. - /// - /// - returns: The created `UploadRequest`. - @discardableResult - open func upload(_ fileURL: URL, with urlRequest: URLRequestConvertible) -> UploadRequest { - do { - let urlRequest = try urlRequest.asURLRequest() - return upload(.file(fileURL, urlRequest)) - } catch { - return upload(nil, failedWith: error) - } - } - - // MARK: Data - - /// Creates an `UploadRequest` from the specified `url`, `method` and `headers` for uploading the `data`. - /// - /// If `startRequestsImmediately` is `true`, the request will have `resume()` called before being returned. - /// - /// - parameter data: The data to upload. - /// - parameter url: The URL. - /// - parameter method: The HTTP method. `.post` by default. - /// - parameter headers: The HTTP headers. `nil` by default. - /// - /// - returns: The created `UploadRequest`. - @discardableResult - open func upload( - _ data: Data, - to url: URLConvertible, - method: HTTPMethod = .post, - headers: HTTPHeaders? = nil) - -> UploadRequest - { - do { - let urlRequest = try URLRequest(url: url, method: method, headers: headers) - return upload(data, with: urlRequest) - } catch { - return upload(nil, failedWith: error) - } - } - - /// Creates an `UploadRequest` from the specified `urlRequest` for uploading the `data`. - /// - /// If `startRequestsImmediately` is `true`, the request will have `resume()` called before being returned. - /// - /// - parameter data: The data to upload. - /// - parameter urlRequest: The URL request. - /// - /// - returns: The created `UploadRequest`. - @discardableResult - open func upload(_ data: Data, with urlRequest: URLRequestConvertible) -> UploadRequest { - do { - let urlRequest = try urlRequest.asURLRequest() - return upload(.data(data, urlRequest)) - } catch { - return upload(nil, failedWith: error) - } - } - - // MARK: InputStream - - /// Creates an `UploadRequest` from the specified `url`, `method` and `headers` for uploading the `stream`. - /// - /// If `startRequestsImmediately` is `true`, the request will have `resume()` called before being returned. - /// - /// - parameter stream: The stream to upload. - /// - parameter url: The URL. - /// - parameter method: The HTTP method. `.post` by default. - /// - parameter headers: The HTTP headers. `nil` by default. - /// - /// - returns: The created `UploadRequest`. - @discardableResult - open func upload( - _ stream: InputStream, - to url: URLConvertible, - method: HTTPMethod = .post, - headers: HTTPHeaders? = nil) - -> UploadRequest - { - do { - let urlRequest = try URLRequest(url: url, method: method, headers: headers) - return upload(stream, with: urlRequest) - } catch { - return upload(nil, failedWith: error) - } - } - - /// Creates an `UploadRequest` from the specified `urlRequest` for uploading the `stream`. - /// - /// If `startRequestsImmediately` is `true`, the request will have `resume()` called before being returned. - /// - /// - parameter stream: The stream to upload. - /// - parameter urlRequest: The URL request. - /// - /// - returns: The created `UploadRequest`. - @discardableResult - open func upload(_ stream: InputStream, with urlRequest: URLRequestConvertible) -> UploadRequest { - do { - let urlRequest = try urlRequest.asURLRequest() - return upload(.stream(stream, urlRequest)) - } catch { - return upload(nil, failedWith: error) - } - } - - // MARK: MultipartFormData - - /// Encodes `multipartFormData` using `encodingMemoryThreshold` and calls `encodingCompletion` with new - /// `UploadRequest` using the `url`, `method` and `headers`. - /// - /// It is important to understand the memory implications of uploading `MultipartFormData`. If the cummulative - /// payload is small, encoding the data in-memory and directly uploading to a server is the by far the most - /// efficient approach. However, if the payload is too large, encoding the data in-memory could cause your app to - /// be terminated. Larger payloads must first be written to disk using input and output streams to keep the memory - /// footprint low, then the data can be uploaded as a stream from the resulting file. Streaming from disk MUST be - /// used for larger payloads such as video content. - /// - /// The `encodingMemoryThreshold` parameter allows Alamofire to automatically determine whether to encode in-memory - /// or stream from disk. If the content length of the `MultipartFormData` is below the `encodingMemoryThreshold`, - /// encoding takes place in-memory. If the content length exceeds the threshold, the data is streamed to disk - /// during the encoding process. Then the result is uploaded as data or as a stream depending on which encoding - /// technique was used. - /// - /// If `startRequestsImmediately` is `true`, the request will have `resume()` called before being returned. - /// - /// - parameter multipartFormData: The closure used to append body parts to the `MultipartFormData`. - /// - parameter encodingMemoryThreshold: The encoding memory threshold in bytes. - /// `multipartFormDataEncodingMemoryThreshold` by default. - /// - parameter url: The URL. - /// - parameter method: The HTTP method. `.post` by default. - /// - parameter headers: The HTTP headers. `nil` by default. - /// - parameter encodingCompletion: The closure called when the `MultipartFormData` encoding is complete. - open func upload( - multipartFormData: @escaping (MultipartFormData) -> Void, - usingThreshold encodingMemoryThreshold: UInt64 = SessionManager.multipartFormDataEncodingMemoryThreshold, - to url: URLConvertible, - method: HTTPMethod = .post, - headers: HTTPHeaders? = nil, - queue: DispatchQueue? = nil, - encodingCompletion: ((MultipartFormDataEncodingResult) -> Void)?) - { - do { - let urlRequest = try URLRequest(url: url, method: method, headers: headers) - - return upload( - multipartFormData: multipartFormData, - usingThreshold: encodingMemoryThreshold, - with: urlRequest, - queue: queue, - encodingCompletion: encodingCompletion - ) - } catch { - (queue ?? DispatchQueue.main).async { encodingCompletion?(.failure(error)) } - } - } - - /// Encodes `multipartFormData` using `encodingMemoryThreshold` and calls `encodingCompletion` with new - /// `UploadRequest` using the `urlRequest`. - /// - /// It is important to understand the memory implications of uploading `MultipartFormData`. If the cummulative - /// payload is small, encoding the data in-memory and directly uploading to a server is the by far the most - /// efficient approach. However, if the payload is too large, encoding the data in-memory could cause your app to - /// be terminated. Larger payloads must first be written to disk using input and output streams to keep the memory - /// footprint low, then the data can be uploaded as a stream from the resulting file. Streaming from disk MUST be - /// used for larger payloads such as video content. - /// - /// The `encodingMemoryThreshold` parameter allows Alamofire to automatically determine whether to encode in-memory - /// or stream from disk. If the content length of the `MultipartFormData` is below the `encodingMemoryThreshold`, - /// encoding takes place in-memory. If the content length exceeds the threshold, the data is streamed to disk - /// during the encoding process. Then the result is uploaded as data or as a stream depending on which encoding - /// technique was used. - /// - /// If `startRequestsImmediately` is `true`, the request will have `resume()` called before being returned. - /// - /// - parameter multipartFormData: The closure used to append body parts to the `MultipartFormData`. - /// - parameter encodingMemoryThreshold: The encoding memory threshold in bytes. - /// `multipartFormDataEncodingMemoryThreshold` by default. - /// - parameter urlRequest: The URL request. - /// - parameter encodingCompletion: The closure called when the `MultipartFormData` encoding is complete. - open func upload( - multipartFormData: @escaping (MultipartFormData) -> Void, - usingThreshold encodingMemoryThreshold: UInt64 = SessionManager.multipartFormDataEncodingMemoryThreshold, - with urlRequest: URLRequestConvertible, - queue: DispatchQueue? = nil, - encodingCompletion: ((MultipartFormDataEncodingResult) -> Void)?) - { - DispatchQueue.global(qos: .utility).async { - let formData = MultipartFormData() - multipartFormData(formData) - - var tempFileURL: URL? - - do { - var urlRequestWithContentType = try urlRequest.asURLRequest() - urlRequestWithContentType.setValue(formData.contentType, forHTTPHeaderField: "Content-Type") - - let isBackgroundSession = self.session.configuration.identifier != nil - - if formData.contentLength < encodingMemoryThreshold && !isBackgroundSession { - let data = try formData.encode() - - let encodingResult = MultipartFormDataEncodingResult.success( - request: self.upload(data, with: urlRequestWithContentType), - streamingFromDisk: false, - streamFileURL: nil - ) - - (queue ?? DispatchQueue.main).async { encodingCompletion?(encodingResult) } - } else { - let fileManager = FileManager.default - let tempDirectoryURL = URL(fileURLWithPath: NSTemporaryDirectory()) - let directoryURL = tempDirectoryURL.appendingPathComponent("org.alamofire.manager/multipart.form.data") - let fileName = UUID().uuidString - let fileURL = directoryURL.appendingPathComponent(fileName) - - tempFileURL = fileURL - - var directoryError: Error? - - // Create directory inside serial queue to ensure two threads don't do this in parallel - self.queue.sync { - do { - try fileManager.createDirectory(at: directoryURL, withIntermediateDirectories: true, attributes: nil) - } catch { - directoryError = error - } - } - - if let directoryError = directoryError { throw directoryError } - - try formData.writeEncodedData(to: fileURL) - - let upload = self.upload(fileURL, with: urlRequestWithContentType) - - // Cleanup the temp file once the upload is complete - upload.delegate.queue.addOperation { - do { - try FileManager.default.removeItem(at: fileURL) - } catch { - // No-op - } - } - - (queue ?? DispatchQueue.main).async { - let encodingResult = MultipartFormDataEncodingResult.success( - request: upload, - streamingFromDisk: true, - streamFileURL: fileURL - ) - - encodingCompletion?(encodingResult) - } - } - } catch { - // Cleanup the temp file in the event that the multipart form data encoding failed - if let tempFileURL = tempFileURL { - do { - try FileManager.default.removeItem(at: tempFileURL) - } catch { - // No-op - } - } - - (queue ?? DispatchQueue.main).async { encodingCompletion?(.failure(error)) } - } - } - } - - // MARK: Private - Upload Implementation - - private func upload(_ uploadable: UploadRequest.Uploadable) -> UploadRequest { - do { - let task = try uploadable.task(session: session, adapter: adapter, queue: queue) - let upload = UploadRequest(session: session, requestTask: .upload(uploadable, task)) - - if case let .stream(inputStream, _) = uploadable { - upload.delegate.taskNeedNewBodyStream = { _, _ in inputStream } - } - - delegate[task] = upload - - if startRequestsImmediately { upload.resume() } - - return upload - } catch { - return upload(uploadable, failedWith: error) - } - } - - private func upload(_ uploadable: UploadRequest.Uploadable?, failedWith error: Error) -> UploadRequest { - var uploadTask: Request.RequestTask = .upload(nil, nil) - - if let uploadable = uploadable { - uploadTask = .upload(uploadable, nil) - } - - let underlyingError = error.underlyingAdaptError ?? error - let upload = UploadRequest(session: session, requestTask: uploadTask, error: underlyingError) - - if let retrier = retrier, error is AdaptError { - allowRetrier(retrier, toRetry: upload, with: underlyingError) - } else { - if startRequestsImmediately { upload.resume() } - } - - return upload - } - -#if !os(watchOS) - - // MARK: - Stream Request - - // MARK: Hostname and Port - - /// Creates a `StreamRequest` for bidirectional streaming using the `hostname` and `port`. - /// - /// If `startRequestsImmediately` is `true`, the request will have `resume()` called before being returned. - /// - /// - parameter hostName: The hostname of the server to connect to. - /// - parameter port: The port of the server to connect to. - /// - /// - returns: The created `StreamRequest`. - @discardableResult - @available(iOS 9.0, macOS 10.11, tvOS 9.0, *) - open func stream(withHostName hostName: String, port: Int) -> StreamRequest { - return stream(.stream(hostName: hostName, port: port)) - } - - // MARK: NetService - - /// Creates a `StreamRequest` for bidirectional streaming using the `netService`. - /// - /// If `startRequestsImmediately` is `true`, the request will have `resume()` called before being returned. - /// - /// - parameter netService: The net service used to identify the endpoint. - /// - /// - returns: The created `StreamRequest`. - @discardableResult - @available(iOS 9.0, macOS 10.11, tvOS 9.0, *) - open func stream(with netService: NetService) -> StreamRequest { - return stream(.netService(netService)) - } - - // MARK: Private - Stream Implementation - - @available(iOS 9.0, macOS 10.11, tvOS 9.0, *) - private func stream(_ streamable: StreamRequest.Streamable) -> StreamRequest { - do { - let task = try streamable.task(session: session, adapter: adapter, queue: queue) - let request = StreamRequest(session: session, requestTask: .stream(streamable, task)) - - delegate[task] = request - - if startRequestsImmediately { request.resume() } - - return request - } catch { - return stream(failedWith: error) - } - } - - @available(iOS 9.0, macOS 10.11, tvOS 9.0, *) - private func stream(failedWith error: Error) -> StreamRequest { - let stream = StreamRequest(session: session, requestTask: .stream(nil, nil), error: error) - if startRequestsImmediately { stream.resume() } - return stream - } - -#endif - - // MARK: - Internal - Retry Request - - func retry(_ request: Request) -> Bool { - guard let originalTask = request.originalTask else { return false } - - do { - let task = try originalTask.task(session: session, adapter: adapter, queue: queue) - - if let originalTask = request.task { - delegate[originalTask] = nil // removes the old request to avoid endless growth - } - - request.delegate.task = task // resets all task delegate data - - request.retryCount += 1 - request.startTime = CFAbsoluteTimeGetCurrent() - request.endTime = nil - - task.resume() - - return true - } catch { - request.delegate.error = error.underlyingAdaptError ?? error - return false - } - } - - private func allowRetrier(_ retrier: RequestRetrier, toRetry request: Request, with error: Error) { - DispatchQueue.utility.async { [weak self] in - guard let strongSelf = self else { return } - - retrier.should(strongSelf, retry: request, with: error) { shouldRetry, timeDelay in - guard let strongSelf = self else { return } - - guard shouldRetry else { - if strongSelf.startRequestsImmediately { request.resume() } - return - } - - DispatchQueue.utility.after(timeDelay) { - guard let strongSelf = self else { return } - - let retrySucceeded = strongSelf.retry(request) - - if retrySucceeded, let task = request.task { - strongSelf.delegate[task] = request - } else { - if strongSelf.startRequestsImmediately { request.resume() } - } - } - } - } - } -} diff --git a/Source/SessionStateProvider.swift b/Source/SessionStateProvider.swift new file mode 100644 index 000000000..a9b2ba69b --- /dev/null +++ b/Source/SessionStateProvider.swift @@ -0,0 +1,261 @@ +// +// SessionStateProvider.swift +// +// Copyright (c) 2014-2018 Alamofire Software Foundation (http://alamofire.org/) +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +import Foundation + +public protocol SessionStateProvider: AnyObject { + func request(for task: URLSessionTask) -> Request? + func didCompleteTask(_ task: URLSessionTask) + var serverTrustManager: ServerTrustManager? { get } + func credential(for task: URLSessionTask, protectionSpace: URLProtectionSpace) -> URLCredential? +} + +open class SessionDelegate: NSObject { + private let fileManager: FileManager + + weak var stateProvider: SessionStateProvider? + var eventMonitor: EventMonitor? + + public init(fileManager: FileManager = .default) { + self.fileManager = fileManager + } +} + +extension SessionDelegate: URLSessionDelegate { + open func urlSession(_ session: URLSession, didBecomeInvalidWithError error: Error?) { + eventMonitor?.urlSession(session, didBecomeInvalidWithError: error) + } +} + +extension SessionDelegate: URLSessionTaskDelegate { + /// Result of a `URLAuthenticationChallenge` evaluation. + typealias ChallengeEvaluation = (disposition: URLSession.AuthChallengeDisposition, credential: URLCredential?, error: Error?) + + open func urlSession(_ session: URLSession, + task: URLSessionTask, + didReceive challenge: URLAuthenticationChallenge, + completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) { + eventMonitor?.urlSession(session, task: task, didReceive: challenge) + + let evaluation: ChallengeEvaluation + switch challenge.protectionSpace.authenticationMethod { + case NSURLAuthenticationMethodServerTrust: + evaluation = attemptServerTrustAuthentication(with: challenge) + case NSURLAuthenticationMethodHTTPBasic, NSURLAuthenticationMethodHTTPDigest: + evaluation = attemptHTTPAuthentication(for: challenge, belongingTo: task) + // case NSURLAuthenticationMethodClientCertificate: + default: + evaluation = (.performDefaultHandling, nil, nil) + } + + if let error = evaluation.error { + stateProvider?.request(for: task)?.didFailTask(task, earlyWithError: error) + } + + completionHandler(evaluation.disposition, evaluation.credential) + } + + func attemptServerTrustAuthentication(with challenge: URLAuthenticationChallenge) -> ChallengeEvaluation { + let host = challenge.protectionSpace.host + + guard challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodServerTrust, + let trust = challenge.protectionSpace.serverTrust + else { + return (.performDefaultHandling, nil, nil) + } + + do { + guard let evaluator = try stateProvider?.serverTrustManager?.serverTrustEvaluator(forHost: host) else { + return (.performDefaultHandling, nil, nil) + } + + try evaluator.evaluate(trust, forHost: host) + + return (.useCredential, URLCredential(trust: trust), nil) + } catch { + return (.cancelAuthenticationChallenge, nil, error) + } + } + + func attemptHTTPAuthentication(for challenge: URLAuthenticationChallenge, + belongingTo task: URLSessionTask) -> ChallengeEvaluation { + guard challenge.previousFailureCount == 0 else { + return (.rejectProtectionSpace, nil, nil) + } + + guard let credential = stateProvider?.credential(for: task, protectionSpace: challenge.protectionSpace) else { + return (.performDefaultHandling, nil, nil) + } + + return (.useCredential, credential, nil) + } + + open func urlSession(_ session: URLSession, + task: URLSessionTask, + didSendBodyData bytesSent: Int64, + totalBytesSent: Int64, + totalBytesExpectedToSend: Int64) { + eventMonitor?.urlSession(session, + task: task, + didSendBodyData: bytesSent, + totalBytesSent: totalBytesSent, + totalBytesExpectedToSend: totalBytesExpectedToSend) + + stateProvider?.request(for: task)?.updateUploadProgress(totalBytesSent: totalBytesSent, + totalBytesExpectedToSend: totalBytesExpectedToSend) + } + + open func urlSession(_ session: URLSession, + task: URLSessionTask, + needNewBodyStream completionHandler: @escaping (InputStream?) -> Void) { + eventMonitor?.urlSession(session, taskNeedsNewBodyStream: task) + + guard let request = stateProvider?.request(for: task) as? UploadRequest else { + fatalError("needNewBodyStream for request that isn't UploadRequest.") + } + + completionHandler(request.inputStream()) + } + + open func urlSession(_ session: URLSession, + task: URLSessionTask, + willPerformHTTPRedirection response: HTTPURLResponse, + newRequest request: URLRequest, + completionHandler: @escaping (URLRequest?) -> Void) { + eventMonitor?.urlSession(session, task: task, willPerformHTTPRedirection: response, newRequest: request) + + completionHandler(request) + } + + open func urlSession(_ session: URLSession, task: URLSessionTask, didFinishCollecting metrics: URLSessionTaskMetrics) { + eventMonitor?.urlSession(session, task: task, didFinishCollecting: metrics) + + stateProvider?.request(for: task)?.didGatherMetrics(metrics) + } + + open func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) { + eventMonitor?.urlSession(session, task: task, didCompleteWithError: error) + + stateProvider?.request(for: task)?.didCompleteTask(task, with: error) + + stateProvider?.didCompleteTask(task) + } + + @available(macOS 10.13, iOS 11.0, tvOS 11.0, watchOS 4.0, *) + open func urlSession(_ session: URLSession, taskIsWaitingForConnectivity task: URLSessionTask) { + eventMonitor?.urlSession(session, taskIsWaitingForConnectivity: task) + } +} + +extension SessionDelegate: URLSessionDataDelegate { + open func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) { + eventMonitor?.urlSession(session, dataTask: dataTask, didReceive: data) + + guard let request = stateProvider?.request(for: dataTask) as? DataRequest else { + fatalError("dataTask received data for incorrect Request subclass: \(String(describing: stateProvider?.request(for: dataTask)))") + } + + request.didReceive(data: data) + } + + open func urlSession(_ session: URLSession, + dataTask: URLSessionDataTask, + willCacheResponse proposedResponse: CachedURLResponse, + completionHandler: @escaping (CachedURLResponse?) -> Void) { + eventMonitor?.urlSession(session, dataTask: dataTask, willCacheResponse: proposedResponse) + + completionHandler(proposedResponse) + } +} + +extension SessionDelegate: URLSessionDownloadDelegate { + open func urlSession(_ session: URLSession, + downloadTask: URLSessionDownloadTask, + didResumeAtOffset fileOffset: Int64, + expectedTotalBytes: Int64) { + eventMonitor?.urlSession(session, + downloadTask: downloadTask, + didResumeAtOffset: fileOffset, + expectedTotalBytes: expectedTotalBytes) + + guard let downloadRequest = stateProvider?.request(for: downloadTask) as? DownloadRequest else { + fatalError("No DownloadRequest found for downloadTask: \(downloadTask)") + } + + downloadRequest.updateDownloadProgress(bytesWritten: fileOffset, + totalBytesExpectedToWrite: expectedTotalBytes) + } + + open func urlSession(_ session: URLSession, + downloadTask: URLSessionDownloadTask, + didWriteData bytesWritten: Int64, + totalBytesWritten: Int64, + totalBytesExpectedToWrite: Int64) { + eventMonitor?.urlSession(session, + downloadTask: downloadTask, + didWriteData: bytesWritten, + totalBytesWritten: totalBytesWritten, + totalBytesExpectedToWrite: totalBytesExpectedToWrite) + + guard let downloadRequest = stateProvider?.request(for: downloadTask) as? DownloadRequest else { + fatalError("No DownloadRequest found for downloadTask: \(downloadTask)") + } + + downloadRequest.updateDownloadProgress(bytesWritten: bytesWritten, + totalBytesExpectedToWrite: totalBytesExpectedToWrite) + } + + open func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) { + eventMonitor?.urlSession(session, downloadTask: downloadTask, didFinishDownloadingTo: location) + + guard let request = stateProvider?.request(for: downloadTask) as? DownloadRequest else { + fatalError("Download finished but either no request found or request wasn't DownloadRequest") + } + + guard let response = request.response else { + fatalError("URLSessionDownloadTask finished downloading with no response.") + } + + let (destination, options) = (request.destination ?? DownloadRequest.defaultDestination)(location, response) + + eventMonitor?.request(request, didCreateDestinationURL: destination) + + do { + if options.contains(.removePreviousFile), fileManager.fileExists(atPath: destination.path) { + try fileManager.removeItem(at: destination) + } + + if options.contains(.createIntermediateDirectories) { + let directory = destination.deletingLastPathComponent() + try fileManager.createDirectory(at: directory, withIntermediateDirectories: true) + } + + try fileManager.moveItem(at: location, to: destination) + + request.didFinishDownloading(using: downloadTask, with: .success(destination)) + } catch { + request.didFinishDownloading(using: downloadTask, with: .failure(error)) + } + } +} diff --git a/Source/TaskDelegate.swift b/Source/TaskDelegate.swift deleted file mode 100644 index 8e19888fc..000000000 --- a/Source/TaskDelegate.swift +++ /dev/null @@ -1,466 +0,0 @@ -// -// TaskDelegate.swift -// -// Copyright (c) 2014-2018 Alamofire Software Foundation (http://alamofire.org/) -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -// THE SOFTWARE. -// - -import Foundation - -/// The task delegate is responsible for handling all delegate callbacks for the underlying task as well as -/// executing all operations attached to the serial operation queue upon task completion. -open class TaskDelegate: NSObject { - - // MARK: Properties - - /// The serial operation queue used to execute all operations after the task completes. - public let queue: OperationQueue - - /// The data returned by the server. - public var data: Data? { return nil } - - /// The error generated throughout the lifecyle of the task. - public var error: Error? - - var task: URLSessionTask? { - set { - taskLock.lock(); defer { taskLock.unlock() } - _task = newValue - } - get { - taskLock.lock(); defer { taskLock.unlock() } - return _task - } - } - - var initialResponseTime: CFAbsoluteTime? - var credential: URLCredential? - var metrics: AnyObject? // URLSessionTaskMetrics - - private var _task: URLSessionTask? { - didSet { reset() } - } - - private let taskLock = NSLock() - - // MARK: Lifecycle - - init(task: URLSessionTask?) { - _task = task - - self.queue = { - let operationQueue = OperationQueue() - - operationQueue.maxConcurrentOperationCount = 1 - operationQueue.isSuspended = true - operationQueue.qualityOfService = .utility - - return operationQueue - }() - } - - func reset() { - error = nil - initialResponseTime = nil - } - - // MARK: URLSessionTaskDelegate - - var taskWillPerformHTTPRedirection: ((URLSession, URLSessionTask, HTTPURLResponse, URLRequest) -> URLRequest?)? - var taskDidReceiveChallenge: ((URLSession, URLSessionTask, URLAuthenticationChallenge) -> (URLSession.AuthChallengeDisposition, URLCredential?))? - var taskNeedNewBodyStream: ((URLSession, URLSessionTask) -> InputStream?)? - var taskDidCompleteWithError: ((URLSession, URLSessionTask, Error?) -> Void)? - - @objc(URLSession:task:willPerformHTTPRedirection:newRequest:completionHandler:) - func urlSession( - _ session: URLSession, - task: URLSessionTask, - willPerformHTTPRedirection response: HTTPURLResponse, - newRequest request: URLRequest, - completionHandler: @escaping (URLRequest?) -> Void) - { - var redirectRequest: URLRequest? = request - - if let taskWillPerformHTTPRedirection = taskWillPerformHTTPRedirection { - redirectRequest = taskWillPerformHTTPRedirection(session, task, response, request) - } - - completionHandler(redirectRequest) - } - - @objc(URLSession:task:didReceiveChallenge:completionHandler:) - func urlSession( - _ session: URLSession, - task: URLSessionTask, - didReceive challenge: URLAuthenticationChallenge, - completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) - { - var disposition: URLSession.AuthChallengeDisposition = .performDefaultHandling - var credential: URLCredential? - - if let taskDidReceiveChallenge = taskDidReceiveChallenge { - (disposition, credential) = taskDidReceiveChallenge(session, task, challenge) - } else if challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodServerTrust { - let host = challenge.protectionSpace.host - - if - let serverTrustPolicy = session.serverTrustPolicyManager?.serverTrustPolicy(forHost: host), - let serverTrust = challenge.protectionSpace.serverTrust - { - if serverTrustPolicy.evaluate(serverTrust, forHost: host) { - disposition = .useCredential - credential = URLCredential(trust: serverTrust) - } else { - disposition = .cancelAuthenticationChallenge - } - } - } else { - if challenge.previousFailureCount > 0 { - disposition = .rejectProtectionSpace - } else { - credential = self.credential ?? session.configuration.urlCredentialStorage?.defaultCredential(for: challenge.protectionSpace) - - if credential != nil { - disposition = .useCredential - } - } - } - - completionHandler(disposition, credential) - } - - @objc(URLSession:task:needNewBodyStream:) - func urlSession( - _ session: URLSession, - task: URLSessionTask, - needNewBodyStream completionHandler: @escaping (InputStream?) -> Void) - { - var bodyStream: InputStream? - - if let taskNeedNewBodyStream = taskNeedNewBodyStream { - bodyStream = taskNeedNewBodyStream(session, task) - } - - completionHandler(bodyStream) - } - - @objc(URLSession:task:didCompleteWithError:) - func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) { - if let taskDidCompleteWithError = taskDidCompleteWithError { - taskDidCompleteWithError(session, task, error) - } else { - if let error = error { - if self.error == nil { self.error = error } - - if - let downloadDelegate = self as? DownloadTaskDelegate, - let resumeData = (error as NSError).userInfo[NSURLSessionDownloadTaskResumeData] as? Data - { - downloadDelegate.resumeData = resumeData - } - } - - queue.isSuspended = false - } - } -} - -// MARK: - - -class DataTaskDelegate: TaskDelegate, URLSessionDataDelegate { - - // MARK: Properties - - var dataTask: URLSessionDataTask { return task as! URLSessionDataTask } - - override var data: Data? { - if dataStream != nil { - return nil - } else { - return mutableData - } - } - - var progress: Progress - var progressHandler: (closure: Request.ProgressHandler, queue: DispatchQueue)? - - var dataStream: ((_ data: Data) -> Void)? - - private var totalBytesReceived: Int64 = 0 - private var mutableData: Data - - private var expectedContentLength: Int64? - - // MARK: Lifecycle - - override init(task: URLSessionTask?) { - mutableData = Data() - progress = Progress(totalUnitCount: 0) - - super.init(task: task) - } - - override func reset() { - super.reset() - - progress = Progress(totalUnitCount: 0) - totalBytesReceived = 0 - mutableData = Data() - expectedContentLength = nil - } - - // MARK: URLSessionDataDelegate - - var dataTaskDidReceiveResponse: ((URLSession, URLSessionDataTask, URLResponse) -> URLSession.ResponseDisposition)? - var dataTaskDidBecomeDownloadTask: ((URLSession, URLSessionDataTask, URLSessionDownloadTask) -> Void)? - var dataTaskDidReceiveData: ((URLSession, URLSessionDataTask, Data) -> Void)? - var dataTaskWillCacheResponse: ((URLSession, URLSessionDataTask, CachedURLResponse) -> CachedURLResponse?)? - - func urlSession( - _ session: URLSession, - dataTask: URLSessionDataTask, - didReceive response: URLResponse, - completionHandler: @escaping (URLSession.ResponseDisposition) -> Void) - { - var disposition: URLSession.ResponseDisposition = .allow - - expectedContentLength = response.expectedContentLength - - if let dataTaskDidReceiveResponse = dataTaskDidReceiveResponse { - disposition = dataTaskDidReceiveResponse(session, dataTask, response) - } - - completionHandler(disposition) - } - - func urlSession( - _ session: URLSession, - dataTask: URLSessionDataTask, - didBecome downloadTask: URLSessionDownloadTask) - { - dataTaskDidBecomeDownloadTask?(session, dataTask, downloadTask) - } - - func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) { - if initialResponseTime == nil { initialResponseTime = CFAbsoluteTimeGetCurrent() } - - if let dataTaskDidReceiveData = dataTaskDidReceiveData { - dataTaskDidReceiveData(session, dataTask, data) - } else { - if let dataStream = dataStream { - dataStream(data) - } else { - mutableData.append(data) - } - - let bytesReceived = Int64(data.count) - totalBytesReceived += bytesReceived - let totalBytesExpected = dataTask.response?.expectedContentLength ?? NSURLSessionTransferSizeUnknown - - progress.totalUnitCount = totalBytesExpected - progress.completedUnitCount = totalBytesReceived - - if let progressHandler = progressHandler { - progressHandler.queue.async { progressHandler.closure(self.progress) } - } - } - } - - func urlSession( - _ session: URLSession, - dataTask: URLSessionDataTask, - willCacheResponse proposedResponse: CachedURLResponse, - completionHandler: @escaping (CachedURLResponse?) -> Void) - { - var cachedResponse: CachedURLResponse? = proposedResponse - - if let dataTaskWillCacheResponse = dataTaskWillCacheResponse { - cachedResponse = dataTaskWillCacheResponse(session, dataTask, proposedResponse) - } - - completionHandler(cachedResponse) - } -} - -// MARK: - - -class DownloadTaskDelegate: TaskDelegate, URLSessionDownloadDelegate { - - // MARK: Properties - - var downloadTask: URLSessionDownloadTask { return task as! URLSessionDownloadTask } - - var progress: Progress - var progressHandler: (closure: Request.ProgressHandler, queue: DispatchQueue)? - - var resumeData: Data? - override var data: Data? { return resumeData } - - var destination: DownloadRequest.DownloadFileDestination? - - var temporaryURL: URL? - var destinationURL: URL? - - var fileURL: URL? { return destination != nil ? destinationURL : temporaryURL } - - // MARK: Lifecycle - - override init(task: URLSessionTask?) { - progress = Progress(totalUnitCount: 0) - super.init(task: task) - } - - override func reset() { - super.reset() - - progress = Progress(totalUnitCount: 0) - resumeData = nil - } - - // MARK: URLSessionDownloadDelegate - - var downloadTaskDidFinishDownloadingToURL: ((URLSession, URLSessionDownloadTask, URL) -> URL)? - var downloadTaskDidWriteData: ((URLSession, URLSessionDownloadTask, Int64, Int64, Int64) -> Void)? - var downloadTaskDidResumeAtOffset: ((URLSession, URLSessionDownloadTask, Int64, Int64) -> Void)? - - func urlSession( - _ session: URLSession, - downloadTask: URLSessionDownloadTask, - didFinishDownloadingTo location: URL) - { - temporaryURL = location - - guard - let destination = destination, - let response = downloadTask.response as? HTTPURLResponse - else { return } - - let result = destination(location, response) - let destinationURL = result.destinationURL - let options = result.options - - self.destinationURL = destinationURL - - do { - if options.contains(.removePreviousFile), FileManager.default.fileExists(atPath: destinationURL.path) { - try FileManager.default.removeItem(at: destinationURL) - } - - if options.contains(.createIntermediateDirectories) { - let directory = destinationURL.deletingLastPathComponent() - try FileManager.default.createDirectory(at: directory, withIntermediateDirectories: true) - } - - try FileManager.default.moveItem(at: location, to: destinationURL) - } catch { - self.error = error - } - } - - func urlSession( - _ session: URLSession, - downloadTask: URLSessionDownloadTask, - didWriteData bytesWritten: Int64, - totalBytesWritten: Int64, - totalBytesExpectedToWrite: Int64) - { - if initialResponseTime == nil { initialResponseTime = CFAbsoluteTimeGetCurrent() } - - if let downloadTaskDidWriteData = downloadTaskDidWriteData { - downloadTaskDidWriteData( - session, - downloadTask, - bytesWritten, - totalBytesWritten, - totalBytesExpectedToWrite - ) - } else { - progress.totalUnitCount = totalBytesExpectedToWrite - progress.completedUnitCount = totalBytesWritten - - if let progressHandler = progressHandler { - progressHandler.queue.async { progressHandler.closure(self.progress) } - } - } - } - - func urlSession( - _ session: URLSession, - downloadTask: URLSessionDownloadTask, - didResumeAtOffset fileOffset: Int64, - expectedTotalBytes: Int64) - { - if let downloadTaskDidResumeAtOffset = downloadTaskDidResumeAtOffset { - downloadTaskDidResumeAtOffset(session, downloadTask, fileOffset, expectedTotalBytes) - } else { - progress.totalUnitCount = expectedTotalBytes - progress.completedUnitCount = fileOffset - } - } -} - -// MARK: - - -class UploadTaskDelegate: DataTaskDelegate { - - // MARK: Properties - - var uploadTask: URLSessionUploadTask { return task as! URLSessionUploadTask } - - var uploadProgress: Progress - var uploadProgressHandler: (closure: Request.ProgressHandler, queue: DispatchQueue)? - - // MARK: Lifecycle - - override init(task: URLSessionTask?) { - uploadProgress = Progress(totalUnitCount: 0) - super.init(task: task) - } - - override func reset() { - super.reset() - uploadProgress = Progress(totalUnitCount: 0) - } - - // MARK: URLSessionTaskDelegate - - var taskDidSendBodyData: ((URLSession, URLSessionTask, Int64, Int64, Int64) -> Void)? - - func URLSession( - _ session: URLSession, - task: URLSessionTask, - didSendBodyData bytesSent: Int64, - totalBytesSent: Int64, - totalBytesExpectedToSend: Int64) - { - if initialResponseTime == nil { initialResponseTime = CFAbsoluteTimeGetCurrent() } - - if let taskDidSendBodyData = taskDidSendBodyData { - taskDidSendBodyData(session, task, bytesSent, totalBytesSent, totalBytesExpectedToSend) - } else { - uploadProgress.totalUnitCount = totalBytesExpectedToSend - uploadProgress.completedUnitCount = totalBytesSent - - if let uploadProgressHandler = uploadProgressHandler { - uploadProgressHandler.queue.async { uploadProgressHandler.closure(self.uploadProgress) } - } - } - } -} diff --git a/Source/Timeline.swift b/Source/Timeline.swift deleted file mode 100644 index 181c9883c..000000000 --- a/Source/Timeline.swift +++ /dev/null @@ -1,136 +0,0 @@ -// -// Timeline.swift -// -// Copyright (c) 2014-2018 Alamofire Software Foundation (http://alamofire.org/) -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -// THE SOFTWARE. -// - -import Foundation - -/// Responsible for computing the timing metrics for the complete lifecycle of a `Request`. -public struct Timeline { - /// The time the request was initialized. - public let requestStartTime: CFAbsoluteTime - - /// The time the first bytes were received from or sent to the server. - public let initialResponseTime: CFAbsoluteTime - - /// The time when the request was completed. - public let requestCompletedTime: CFAbsoluteTime - - /// The time when the response serialization was completed. - public let serializationCompletedTime: CFAbsoluteTime - - /// The time interval in seconds from the time the request started to the initial response from the server. - public let latency: TimeInterval - - /// The time interval in seconds from the time the request started to the time the request completed. - public let requestDuration: TimeInterval - - /// The time interval in seconds from the time the request completed to the time response serialization completed. - public let serializationDuration: TimeInterval - - /// The time interval in seconds from the time the request started to the time response serialization completed. - public let totalDuration: TimeInterval - - /// Creates a new `Timeline` instance with the specified request times. - /// - /// - parameter requestStartTime: The time the request was initialized. Defaults to `0.0`. - /// - parameter initialResponseTime: The time the first bytes were received from or sent to the server. - /// Defaults to `0.0`. - /// - parameter requestCompletedTime: The time when the request was completed. Defaults to `0.0`. - /// - parameter serializationCompletedTime: The time when the response serialization was completed. Defaults - /// to `0.0`. - /// - /// - returns: The new `Timeline` instance. - public init( - requestStartTime: CFAbsoluteTime = 0.0, - initialResponseTime: CFAbsoluteTime = 0.0, - requestCompletedTime: CFAbsoluteTime = 0.0, - serializationCompletedTime: CFAbsoluteTime = 0.0) - { - self.requestStartTime = requestStartTime - self.initialResponseTime = initialResponseTime - self.requestCompletedTime = requestCompletedTime - self.serializationCompletedTime = serializationCompletedTime - - self.latency = initialResponseTime - requestStartTime - self.requestDuration = requestCompletedTime - requestStartTime - self.serializationDuration = serializationCompletedTime - requestCompletedTime - self.totalDuration = serializationCompletedTime - requestStartTime - } -} - -// MARK: - CustomStringConvertible - -extension Timeline: CustomStringConvertible { - /// The textual representation used when written to an output stream, which includes the latency, the request - /// duration and the total duration. - public var description: String { - let latency = String(format: "%.3f", self.latency) - let requestDuration = String(format: "%.3f", self.requestDuration) - let serializationDuration = String(format: "%.3f", self.serializationDuration) - let totalDuration = String(format: "%.3f", self.totalDuration) - - // NOTE: Had to move to string concatenation due to memory leak filed as rdar://26761490. Once memory leak is - // fixed, we should move back to string interpolation by reverting commit 7d4a43b1. - let timings = [ - "\"Latency\": " + latency + " secs", - "\"Request Duration\": " + requestDuration + " secs", - "\"Serialization Duration\": " + serializationDuration + " secs", - "\"Total Duration\": " + totalDuration + " secs" - ] - - return "Timeline: { " + timings.joined(separator: ", ") + " }" - } -} - -// MARK: - CustomDebugStringConvertible - -extension Timeline: CustomDebugStringConvertible { - /// The textual representation used when written to an output stream, which includes the request start time, the - /// initial response time, the request completed time, the serialization completed time, the latency, the request - /// duration and the total duration. - public var debugDescription: String { - let requestStartTime = String(format: "%.3f", self.requestStartTime) - let initialResponseTime = String(format: "%.3f", self.initialResponseTime) - let requestCompletedTime = String(format: "%.3f", self.requestCompletedTime) - let serializationCompletedTime = String(format: "%.3f", self.serializationCompletedTime) - let latency = String(format: "%.3f", self.latency) - let requestDuration = String(format: "%.3f", self.requestDuration) - let serializationDuration = String(format: "%.3f", self.serializationDuration) - let totalDuration = String(format: "%.3f", self.totalDuration) - - // NOTE: Had to move to string concatenation due to memory leak filed as rdar://26761490. Once memory leak is - // fixed, we should move back to string interpolation by reverting commit 7d4a43b1. - let timings = [ - "\"Request Start Time\": " + requestStartTime, - "\"Initial Response Time\": " + initialResponseTime, - "\"Request Completed Time\": " + requestCompletedTime, - "\"Serialization Completed Time\": " + serializationCompletedTime, - "\"Latency\": " + latency + " secs", - "\"Request Duration\": " + requestDuration + " secs", - "\"Serialization Duration\": " + serializationDuration + " secs", - "\"Total Duration\": " + totalDuration + " secs" - ] - - return "Timeline: { " + timings.joined(separator: ", ") + " }" - } -} diff --git a/Source/URLConvertible+URLRequestConvertible.swift b/Source/URLConvertible+URLRequestConvertible.swift new file mode 100644 index 000000000..369e8e4e1 --- /dev/null +++ b/Source/URLConvertible+URLRequestConvertible.swift @@ -0,0 +1,105 @@ +// +// URLConvertible+URLRequestConvertible.swift +// +// Copyright (c) 2014-2018 Alamofire Software Foundation (http://alamofire.org/) +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +import Foundation + +/// Types adopting the `URLConvertible` protocol can be used to construct `URL`s, which can then be used to construct +/// `URLRequests`. +public protocol URLConvertible { + /// Returns a `URL` from the conforming instance or throws. + /// + /// - Returns: The `URL` created from the instance. + /// - Throws: Any error thrown while creating the `URL`. + func asURL() throws -> URL +} + +extension String: URLConvertible { + /// Returns a `URL` if `self` can be used to initialize a `URL` instance, otherwise throws. + /// + /// - Returns: The `URL` initialized with `self`. + /// - Throws: An `AFError.invalidURL` instance. + public func asURL() throws -> URL { + guard let url = URL(string: self) else { throw AFError.invalidURL(url: self) } + + return url + } +} + +extension URL: URLConvertible { + /// Returns `self`. + public func asURL() throws -> URL { return self } +} + +extension URLComponents: URLConvertible { + /// Returns a `URL` if the `self`'s `url` is not nil, otherwise throws. + /// + /// - Returns: The `URL` from the `url` property. + /// - Throws: An `AFError.invalidURL` instance. + public func asURL() throws -> URL { + guard let url = url else { throw AFError.invalidURL(url: self) } + + return url + } +} + +// MARK: - + +/// Types adopting the `URLRequestConvertible` protocol can be used to safely construct `URLRequest`s. +public protocol URLRequestConvertible { + /// Returns a `URLRequest` or throws if an `Error` was encoutered. + /// + /// - Returns: A `URLRequest`. + /// - Throws: Any error thrown while constructing the `URLRequest`. + func asURLRequest() throws -> URLRequest +} + +extension URLRequestConvertible { + /// The `URLRequest` returned by discarding any `Error` encountered. + public var urlRequest: URLRequest? { return try? asURLRequest() } +} + +extension URLRequest: URLRequestConvertible { + /// Returns `self`. + public func asURLRequest() throws -> URLRequest { return self } +} + +// MARK: - + +extension URLRequest { + /// Creates an instance with the specified `url`, `method`, and `headers`. + /// + /// - Parameters: + /// - url: The `URLConvertible` value. + /// - method: The `HTTPMethod`. + /// - headers: The `HTTPHeaders`, `nil` by default. + /// - Throws: Any error thrown while converting the `URLConvertible` to a `URL`. + public init(url: URLConvertible, method: HTTPMethod, headers: HTTPHeaders? = nil) throws { + let url = try url.asURL() + + self.init(url: url) + + httpMethod = method.rawValue + allHTTPHeaderFields = headers?.dictionary + } +} diff --git a/Source/URLSessionConfiguration+Alamofire.swift b/Source/URLSessionConfiguration+Alamofire.swift new file mode 100644 index 000000000..eb8331768 --- /dev/null +++ b/Source/URLSessionConfiguration+Alamofire.swift @@ -0,0 +1,34 @@ +// +// URLSessionConfiguration+Alamofire.swift +// +// Copyright (c) 2014-2018 Alamofire Software Foundation (http://alamofire.org/) +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +import Foundation + +extension URLSessionConfiguration { + public static var alamofireDefault: URLSessionConfiguration { + let configuration = URLSessionConfiguration.default + configuration.httpHeaders = .default + + return configuration + } +} diff --git a/Source/Validation.swift b/Source/Validation.swift index ec2c5c35a..3ef2f4e32 100644 --- a/Source/Validation.swift +++ b/Source/Validation.swift @@ -30,14 +30,8 @@ extension Request { fileprivate typealias ErrorReason = AFError.ResponseValidationFailureReason - /// Used to represent whether validation was successful or encountered an error resulting in a failure. - /// - /// - success: The validation was successful. - /// - failure: The validation failed encountering the provided error. - public enum ValidationResult { - case success - case failure(Error) - } + /// Used to represent whether a validation succeeded or failed. + public typealias ValidationResult = Result fileprivate struct MIMEType { let type: String @@ -48,12 +42,7 @@ extension Request { init?(_ string: String) { let components: [String] = { let stripped = string.trimmingCharacters(in: .whitespacesAndNewlines) - - #if swift(>=3.2) let split = stripped[..<(stripped.range(of: ";")?.lowerBound ?? stripped.endIndex)] - #else - let split = stripped.substring(to: stripped.range(of: ";")?.lowerBound ?? stripped.endIndex) - #endif return split.components(separatedBy: "/") }() @@ -97,7 +86,7 @@ extension Request { where S.Iterator.Element == Int { if acceptableStatusCodes.contains(response.statusCode) { - return .success + return .success(Void()) } else { let reason: ErrorReason = .unacceptableStatusCode(code: response.statusCode) return .failure(AFError.responseValidationFailed(reason: reason)) @@ -113,7 +102,7 @@ extension Request { -> ValidationResult where S.Iterator.Element == String { - guard let data = data, data.count > 0 else { return .success } + guard let data = data, data.count > 0 else { return .success(Void()) } guard let responseContentType = response.mimeType, @@ -121,7 +110,7 @@ extension Request { else { for contentType in acceptableContentTypes { if let mimeType = MIMEType(contentType), mimeType.isWildcard { - return .success + return .success(Void()) } } @@ -135,7 +124,7 @@ extension Request { for contentType in acceptableContentTypes { if let acceptableMIMEType = MIMEType(contentType), acceptableMIMEType.matches(responseMIMEType) { - return .success + return .success(Void()) } } @@ -159,30 +148,6 @@ extension DataRequest { /// request was valid. public typealias Validation = (URLRequest?, HTTPURLResponse, Data?) -> ValidationResult - /// Validates the request, using the specified closure. - /// - /// If validation fails, subsequent calls to response handlers will have an associated error. - /// - /// - parameter validation: A closure to validate the request. - /// - /// - returns: The request. - @discardableResult - public func validate(_ validation: @escaping Validation) -> Self { - let validationExecution: () -> Void = { [unowned self] in - if - let response = self.response, - self.delegate.error == nil, - case let .failure(error) = validation(self.request, response, self.delegate.data) - { - self.delegate.error = error - } - } - - validations.append(validationExecution) - - return self - } - /// Validates that the response has a status code in the specified sequence. /// /// If validation fails, subsequent calls to response handlers will have an associated error. @@ -205,9 +170,9 @@ extension DataRequest { /// /// - returns: The request. @discardableResult - public func validate(contentType acceptableContentTypes: S) -> Self where S.Iterator.Element == String { + public func validate(contentType acceptableContentTypes: @escaping @autoclosure () -> S) -> Self where S.Iterator.Element == String { return validate { [unowned self] _, response, data in - return self.validate(contentType: acceptableContentTypes, response: response, data: data) + return self.validate(contentType: acceptableContentTypes(), response: response, data: data) } } @@ -231,38 +196,9 @@ extension DownloadRequest { public typealias Validation = ( _ request: URLRequest?, _ response: HTTPURLResponse, - _ temporaryURL: URL?, - _ destinationURL: URL?) + _ fileURL: URL?) -> ValidationResult - /// Validates the request, using the specified closure. - /// - /// If validation fails, subsequent calls to response handlers will have an associated error. - /// - /// - parameter validation: A closure to validate the request. - /// - /// - returns: The request. - @discardableResult - public func validate(_ validation: @escaping Validation) -> Self { - let validationExecution: () -> Void = { [unowned self] in - let request = self.request - let temporaryURL = self.downloadDelegate.temporaryURL - let destinationURL = self.downloadDelegate.destinationURL - - if - let response = self.response, - self.delegate.error == nil, - case let .failure(error) = validation(request, response, temporaryURL, destinationURL) - { - self.delegate.error = error - } - } - - validations.append(validationExecution) - - return self - } - /// Validates that the response has a status code in the specified sequence. /// /// If validation fails, subsequent calls to response handlers will have an associated error. @@ -272,7 +208,7 @@ extension DownloadRequest { /// - returns: The request. @discardableResult public func validate(statusCode acceptableStatusCodes: S) -> Self where S.Iterator.Element == Int { - return validate { [unowned self] _, response, _, _ in + return validate { [unowned self] (_, response, _) in return self.validate(statusCode: acceptableStatusCodes, response: response) } } @@ -285,17 +221,15 @@ extension DownloadRequest { /// /// - returns: The request. @discardableResult - public func validate(contentType acceptableContentTypes: S) -> Self where S.Iterator.Element == String { - return validate { [unowned self] _, response, _, _ in - let fileURL = self.downloadDelegate.fileURL - + public func validate(contentType acceptableContentTypes: @escaping @autoclosure () -> S) -> Self where S.Iterator.Element == String { + return validate { [unowned self] (_, response, fileURL) in guard let validFileURL = fileURL else { return .failure(AFError.responseValidationFailed(reason: .dataFileNil)) } do { let data = try Data(contentsOf: validFileURL) - return self.validate(contentType: acceptableContentTypes, response: response, data: data) + return self.validate(contentType: acceptableContentTypes(), response: response, data: data) } catch { return .failure(AFError.responseValidationFailed(reason: .dataFileReadFailed(at: validFileURL))) } diff --git a/Tests/AFError+AlamofireTests.swift b/Tests/AFError+AlamofireTests.swift index 49e5c946b..09fd03106 100644 --- a/Tests/AFError+AlamofireTests.swift +++ b/Tests/AFError+AlamofireTests.swift @@ -38,11 +38,6 @@ extension AFError { return false } - var isPropertyListEncodingFailed: Bool { - if case let .parameterEncodingFailed(reason) = self, reason.isPropertyListEncodingFailed { return true } - return false - } - // MultipartEncodingFailureReason var isBodyPartURLInvalid: Bool { @@ -112,11 +107,6 @@ extension AFError { // ResponseSerializationFailureReason - var isInputDataNil: Bool { - if case let .responseSerializationFailed(reason) = self, reason.isInputDataNil { return true } - return false - } - var isInputDataNilOrZeroLength: Bool { if case let .responseSerializationFailed(reason) = self, reason.isInputDataNilOrZeroLength { return true } return false @@ -142,8 +132,8 @@ extension AFError { return false } - var isPropertyListSerializationFailed: Bool { - if case let .responseSerializationFailed(reason) = self, reason.isPropertyListSerializationFailed { return true } + var isJSONDecodingFailed: Bool { + if case let .responseSerializationFailed(reason) = self, reason.isDecodingFailed { return true } return false } @@ -187,11 +177,6 @@ extension AFError.ParameterEncodingFailureReason { if case .jsonEncodingFailed = self { return true } return false } - - var isPropertyListEncodingFailed: Bool { - if case .propertyListEncodingFailed = self { return true } - return false - } } // MARK: - @@ -266,11 +251,6 @@ extension AFError.MultipartEncodingFailureReason { // MARK: - extension AFError.ResponseSerializationFailureReason { - var isInputDataNil: Bool { - if case .inputDataNil = self { return true } - return false - } - var isInputDataNilOrZeroLength: Bool { if case .inputDataNilOrZeroLength = self { return true } return false @@ -296,8 +276,8 @@ extension AFError.ResponseSerializationFailureReason { return false } - var isPropertyListSerializationFailed: Bool { - if case .propertyListSerializationFailed = self { return true } + var isDecodingFailed: Bool { + if case .decodingFailed = self { return true } return false } } @@ -330,3 +310,57 @@ extension AFError.ResponseValidationFailureReason { return false } } + +// MARK: - + +extension AFError.ServerTrustFailureReason { + var isNoRequiredEvaluator: Bool { + if case .noRequiredEvaluator = self { return true } + return false + } + + var isNoCertificatesFound: Bool { + if case .noCertificatesFound = self { return true } + return false + } + + var isNoPublicKeysFound: Bool { + if case .noPublicKeysFound = self { return true } + return false + } + + var isPolicyApplicationFailed: Bool { + if case .policyApplicationFailed = self { return true } + return false + } + + var isRevocationPolicyCreationFailed: Bool { + if case .revocationPolicyCreationFailed = self { return true } + return false + } + + var isDefaultEvaluationFailed: Bool { + if case .defaultEvaluationFailed = self { return true } + return false + } + + var isHostValidationFailed: Bool { + if case .hostValidationFailed = self { return true } + return false + } + + var isRevocationCheckFailed: Bool { + if case .revocationCheckFailed = self { return true } + return false + } + + var isCertificatePinningFailed: Bool { + if case .certificatePinningFailed = self { return true } + return false + } + + var isPublicKeyPinningFailed: Bool { + if case .publicKeyPinningFailed = self { return true } + return false + } +} diff --git a/Tests/AuthenticationTests.swift b/Tests/AuthenticationTests.swift index 66591942a..965473632 100644 --- a/Tests/AuthenticationTests.swift +++ b/Tests/AuthenticationTests.swift @@ -31,12 +31,12 @@ class AuthenticationTestCase: BaseTestCase { let password = "password" var urlString = "" - var manager: SessionManager! + var manager: Session! override func setUp() { super.setUp() - manager = SessionManager(configuration: .default) + manager = Session(configuration: .default) // Clear out credentials let credentialStorage = URLCredentialStorage.shared @@ -65,11 +65,11 @@ class BasicAuthenticationTestCase: AuthenticationTestCase { // Given let expectation = self.expectation(description: "\(urlString) 401") - var response: DefaultDataResponse? + var response: DataResponse? // When manager.request(urlString) - .authenticate(user: "invalid", password: "credentials") + .authenticate(username: "invalid", password: "credentials") .response { resp in response = resp expectation.fulfill() @@ -81,7 +81,7 @@ class BasicAuthenticationTestCase: AuthenticationTestCase { XCTAssertNotNil(response?.request) XCTAssertNotNil(response?.response) XCTAssertEqual(response?.response?.statusCode, 401) - XCTAssertNotNil(response?.data) + XCTAssertNil(response?.data) XCTAssertNil(response?.error) } @@ -89,11 +89,11 @@ class BasicAuthenticationTestCase: AuthenticationTestCase { // Given let expectation = self.expectation(description: "\(urlString) 200") - var response: DefaultDataResponse? + var response: DataResponse? // When manager.request(urlString) - .authenticate(user: user, password: password) + .authenticate(username: user, password: password) .response { resp in response = resp expectation.fulfill() @@ -113,14 +113,9 @@ class BasicAuthenticationTestCase: AuthenticationTestCase { // Given let urlString = "http://httpbin.org/hidden-basic-auth/\(user)/\(password)" let expectation = self.expectation(description: "\(urlString) 200") + let headers: HTTPHeaders = [.authorization(username: user, password: password)] - var headers: HTTPHeaders? - - if let authorizationHeader = Request.authorizationHeader(user: user, password: password) { - headers = [authorizationHeader.key: authorizationHeader.value] - } - - var response: DefaultDataResponse? + var response: DataResponse? // When manager.request(urlString, headers: headers) @@ -154,11 +149,11 @@ class HTTPDigestAuthenticationTestCase: AuthenticationTestCase { // Given let expectation = self.expectation(description: "\(urlString) 401") - var response: DefaultDataResponse? + var response: DataResponse? // When manager.request(urlString) - .authenticate(user: "invalid", password: "credentials") + .authenticate(username: "invalid", password: "credentials") .response { resp in response = resp expectation.fulfill() @@ -170,7 +165,7 @@ class HTTPDigestAuthenticationTestCase: AuthenticationTestCase { XCTAssertNotNil(response?.request) XCTAssertNotNil(response?.response) XCTAssertEqual(response?.response?.statusCode, 401) - XCTAssertNotNil(response?.data) + XCTAssertNil(response?.data) XCTAssertNil(response?.error) } @@ -178,11 +173,11 @@ class HTTPDigestAuthenticationTestCase: AuthenticationTestCase { // Given let expectation = self.expectation(description: "\(urlString) 200") - var response: DefaultDataResponse? + var response: DataResponse? // When manager.request(urlString) - .authenticate(user: user, password: password) + .authenticate(username: user, password: password) .response { resp in response = resp expectation.fulfill() diff --git a/Tests/BaseTestCase.swift b/Tests/BaseTestCase.swift index 18345b086..75fd82341 100644 --- a/Tests/BaseTestCase.swift +++ b/Tests/BaseTestCase.swift @@ -27,7 +27,7 @@ import Foundation import XCTest class BaseTestCase: XCTestCase { - let timeout: TimeInterval = 30.0 + let timeout: TimeInterval = 5 static var testDirectoryURL: URL { return FileManager.temporaryDirectoryURL.appendingPathComponent("org.alamofire.tests") } var testDirectoryURL: URL { return BaseTestCase.testDirectoryURL } @@ -43,4 +43,24 @@ class BaseTestCase: XCTestCase { let bundle = Bundle(for: BaseTestCase.self) return bundle.url(forResource: fileName, withExtension: ext)! } + + func assertErrorIsAFError(_ error: Error?, file: StaticString = #file, line: UInt = #line, evaluation: (_ error: AFError) -> Void) { + guard let error = error?.asAFError else { + XCTFail("error is not an AFError", file: file, line: line) + return + } + + evaluation(error) + } + + func assertErrorIsServerTrustEvaluationError(_ error: Error?, file: StaticString = #file, line: UInt = #line, evaluation: (_ reason: AFError.ServerTrustFailureReason) -> Void) { + assertErrorIsAFError(error, file: file, line: line) { (error) in + guard case let .serverTrustEvaluationFailed(reason) = error else { + XCTFail("error is not .serverTrustEvaluationFailed", file: file, line: line) + return + } + + evaluation(reason) + } + } } diff --git a/Tests/CacheTests.swift b/Tests/CacheTests.swift index 3ee9f2287..7b5a18951 100644 --- a/Tests/CacheTests.swift +++ b/Tests/CacheTests.swift @@ -71,7 +71,7 @@ class CacheTestCase: BaseTestCase { // MARK: - Properties var urlCache: URLCache! - var manager: SessionManager! + var manager: Session! let urlString = "https://httpbin.org/response-headers" let requestTimeout: TimeInterval = 30 @@ -94,14 +94,14 @@ class CacheTestCase: BaseTestCase { manager = { let configuration: URLSessionConfiguration = { let configuration = URLSessionConfiguration.default - configuration.httpAdditionalHeaders = SessionManager.defaultHTTPHeaders + configuration.httpHeaders = HTTPHeaders.default configuration.requestCachePolicy = .useProtocolCachePolicy configuration.urlCache = urlCache return configuration }() - let manager = SessionManager(configuration: configuration) + let manager = Session(configuration: configuration) return manager }() @@ -150,21 +150,21 @@ class CacheTestCase: BaseTestCase { } // Wait for all requests to complete - _ = dispatchGroup.wait(timeout: DispatchTime.now() + Double(Int64(30.0 * Float(NSEC_PER_SEC))) / Double(NSEC_PER_SEC)) + _ = dispatchGroup.wait(timeout: .now() + 30) - // Pause for 2 additional seconds to ensure all timestamps will be different + // Pause for 1 additional second to ensure all timestamps will be different dispatchGroup.enter() - serialQueue.asyncAfter(deadline: DispatchTime.now() + Double(Int64(2.0 * Float(NSEC_PER_SEC))) / Double(NSEC_PER_SEC)) { + serialQueue.asyncAfter(deadline: .now() + 1) { dispatchGroup.leave() } - // Wait for our 2 second pause to complete - _ = dispatchGroup.wait(timeout: DispatchTime.now() + Double(Int64(10.0 * Float(NSEC_PER_SEC))) / Double(NSEC_PER_SEC)) + // Wait for our 1 second pause to complete + _ = dispatchGroup.wait(timeout: .now() + 1.25) } // MARK: - Request Helper Methods - func urlRequest(cacheControl: String, cachePolicy: NSURLRequest.CachePolicy) -> URLRequest { + func urlRequest(cacheControl: String, cachePolicy: URLRequest.CachePolicy) -> URLRequest { let parameters = ["Cache-Control": cacheControl] let url = URL(string: urlString)! @@ -181,8 +181,8 @@ class CacheTestCase: BaseTestCase { @discardableResult func startRequest( cacheControl: String, - cachePolicy: NSURLRequest.CachePolicy = .useProtocolCachePolicy, - queue: DispatchQueue = DispatchQueue.main, + cachePolicy: URLRequest.CachePolicy = .useProtocolCachePolicy, + queue: DispatchQueue = .main, completion: @escaping (URLRequest?, HTTPURLResponse?) -> Void) -> URLRequest { @@ -202,7 +202,7 @@ class CacheTestCase: BaseTestCase { // MARK: - Test Execution and Verification func executeTest( - cachePolicy: NSURLRequest.CachePolicy, + cachePolicy: URLRequest.CachePolicy, cacheControl: String, shouldReturnCachedResponse: Bool) { @@ -239,20 +239,6 @@ class CacheTestCase: BaseTestCase { } } - // MARK: - Cache Helper Methods - - private func isCachedResponseForNoStoreHeaderExpected() -> Bool { - #if os(iOS) - if #available(iOS 8.3, *) { - return false - } else { - return true - } - #else - return false - #endif - } - // MARK: - Tests func testURLCacheContainsCachedResponsesForAllRequests() { @@ -278,16 +264,11 @@ class CacheTestCase: BaseTestCase { XCTAssertNotNil(maxAgeNonExpiredResponse, "\(CacheControl.maxAgeNonExpired) response should not be nil") XCTAssertNotNil(maxAgeExpiredResponse, "\(CacheControl.maxAgeExpired) response should not be nil") XCTAssertNotNil(noCacheResponse, "\(CacheControl.noCache) response should not be nil") - - if isCachedResponseForNoStoreHeaderExpected() { - XCTAssertNotNil(noStoreResponse, "\(CacheControl.noStore) response should not be nil") - } else { - XCTAssertNil(noStoreResponse, "\(CacheControl.noStore) response should be nil") - } + XCTAssertNil(noStoreResponse, "\(CacheControl.noStore) response should be nil") } func testDefaultCachePolicy() { - let cachePolicy: NSURLRequest.CachePolicy = .useProtocolCachePolicy + let cachePolicy: URLRequest.CachePolicy = .useProtocolCachePolicy executeTest(cachePolicy: cachePolicy, cacheControl: CacheControl.publicControl, shouldReturnCachedResponse: false) executeTest(cachePolicy: cachePolicy, cacheControl: CacheControl.privateControl, shouldReturnCachedResponse: false) @@ -298,7 +279,7 @@ class CacheTestCase: BaseTestCase { } func testIgnoreLocalCacheDataPolicy() { - let cachePolicy: NSURLRequest.CachePolicy = .reloadIgnoringLocalCacheData + let cachePolicy: URLRequest.CachePolicy = .reloadIgnoringLocalCacheData executeTest(cachePolicy: cachePolicy, cacheControl: CacheControl.publicControl, shouldReturnCachedResponse: false) executeTest(cachePolicy: cachePolicy, cacheControl: CacheControl.privateControl, shouldReturnCachedResponse: false) @@ -309,23 +290,18 @@ class CacheTestCase: BaseTestCase { } func testUseLocalCacheDataIfExistsOtherwiseLoadFromNetworkPolicy() { - let cachePolicy: NSURLRequest.CachePolicy = .returnCacheDataElseLoad + let cachePolicy: URLRequest.CachePolicy = .returnCacheDataElseLoad executeTest(cachePolicy: cachePolicy, cacheControl: CacheControl.publicControl, shouldReturnCachedResponse: true) executeTest(cachePolicy: cachePolicy, cacheControl: CacheControl.privateControl, shouldReturnCachedResponse: true) executeTest(cachePolicy: cachePolicy, cacheControl: CacheControl.maxAgeNonExpired, shouldReturnCachedResponse: true) executeTest(cachePolicy: cachePolicy, cacheControl: CacheControl.maxAgeExpired, shouldReturnCachedResponse: true) executeTest(cachePolicy: cachePolicy, cacheControl: CacheControl.noCache, shouldReturnCachedResponse: true) - - if isCachedResponseForNoStoreHeaderExpected() { - executeTest(cachePolicy: cachePolicy, cacheControl: CacheControl.noStore, shouldReturnCachedResponse: true) - } else { - executeTest(cachePolicy: cachePolicy, cacheControl: CacheControl.noStore, shouldReturnCachedResponse: false) - } + executeTest(cachePolicy: cachePolicy, cacheControl: CacheControl.noStore, shouldReturnCachedResponse: false) } func testUseLocalCacheDataAndDontLoadFromNetworkPolicy() { - let cachePolicy: NSURLRequest.CachePolicy = .returnCacheDataDontLoad + let cachePolicy: URLRequest.CachePolicy = .returnCacheDataDontLoad executeTest(cachePolicy: cachePolicy, cacheControl: CacheControl.publicControl, shouldReturnCachedResponse: true) executeTest(cachePolicy: cachePolicy, cacheControl: CacheControl.privateControl, shouldReturnCachedResponse: true) @@ -333,23 +309,19 @@ class CacheTestCase: BaseTestCase { executeTest(cachePolicy: cachePolicy, cacheControl: CacheControl.maxAgeExpired, shouldReturnCachedResponse: true) executeTest(cachePolicy: cachePolicy, cacheControl: CacheControl.noCache, shouldReturnCachedResponse: true) - if isCachedResponseForNoStoreHeaderExpected() { - executeTest(cachePolicy: cachePolicy, cacheControl: CacheControl.noStore, shouldReturnCachedResponse: true) - } else { - // Given - let expectation = self.expectation(description: "GET request to httpbin") - var response: HTTPURLResponse? - - // When - startRequest(cacheControl: CacheControl.noStore, cachePolicy: cachePolicy) { _, responseResponse in - response = responseResponse - expectation.fulfill() - } - - waitForExpectations(timeout: timeout, handler: nil) + // Given + let expectation = self.expectation(description: "GET request to httpbin") + var response: HTTPURLResponse? - // Then - XCTAssertNil(response, "response should be nil") + // When + startRequest(cacheControl: CacheControl.noStore, cachePolicy: cachePolicy) { _, responseResponse in + response = responseResponse + expectation.fulfill() } + + waitForExpectations(timeout: timeout, handler: nil) + + // Then + XCTAssertNil(response, "response should be nil") } } diff --git a/Tests/DownloadTests.swift b/Tests/DownloadTests.swift index 13a6974ba..bd2fb811a 100644 --- a/Tests/DownloadTests.swift +++ b/Tests/DownloadTests.swift @@ -29,32 +29,42 @@ import XCTest class DownloadInitializationTestCase: BaseTestCase { func testDownloadClassMethodWithMethodURLAndDestination() { // Given - let urlString = "https://httpbin.org/" + let urlString = "https://httpbin.org/get" + let expectation = self.expectation(description: "download should complete") // When - let request = Alamofire.download(urlString) + let request = AF.download(urlString).response { (resp) in + expectation.fulfill() + } + + waitForExpectations(timeout: timeout, handler: nil) // Then XCTAssertNotNil(request.request) XCTAssertEqual(request.request?.httpMethod, "GET") XCTAssertEqual(request.request?.url?.absoluteString, urlString) - XCTAssertNil(request.response) + XCTAssertNotNil(request.response) } func testDownloadClassMethodWithMethodURLHeadersAndDestination() { // Given - let urlString = "https://httpbin.org/" - let headers = ["Authorization": "123456"] + let urlString = "https://httpbin.org/get" + let headers: HTTPHeaders = ["Authorization": "123456"] + let expectation = self.expectation(description: "download should complete") // When - let request = Alamofire.download(urlString, headers: headers) + let request = AF.download(urlString, headers: headers).response { (resp) in + expectation.fulfill() + } + + waitForExpectations(timeout: timeout, handler: nil) // Then XCTAssertNotNil(request.request) XCTAssertEqual(request.request?.httpMethod, "GET") XCTAssertEqual(request.request?.url?.absoluteString, urlString) XCTAssertEqual(request.request?.value(forHTTPHeaderField: "Authorization"), "123456") - XCTAssertNil(request.response) + XCTAssertNotNil(request.response) } } @@ -68,15 +78,15 @@ class DownloadResponseTestCase: BaseTestCase { func testDownloadRequest() { // Given let fileURL = randomCachesFileURL - let numberOfLines = 100 + let numberOfLines = 10 let urlString = "https://httpbin.org/stream/\(numberOfLines)" - let destination: DownloadRequest.DownloadFileDestination = { _, _ in (fileURL, []) } + let destination: DownloadRequest.Destination = { _, _ in (fileURL, []) } let expectation = self.expectation(description: "Download request should download data to file: \(urlString)") - var response: DefaultDownloadResponse? + var response: DownloadResponse? // When - Alamofire.download(urlString, to: destination) + AF.download(urlString, to: destination) .response { resp in response = resp expectation.fulfill() @@ -87,11 +97,11 @@ class DownloadResponseTestCase: BaseTestCase { // Then XCTAssertNotNil(response?.request) XCTAssertNotNil(response?.response) - XCTAssertNotNil(response?.destinationURL) + XCTAssertNotNil(response?.fileURL) XCTAssertNil(response?.resumeData) XCTAssertNil(response?.error) - if let destinationURL = response?.destinationURL { + if let destinationURL = response?.fileURL { XCTAssertTrue(FileManager.default.fileExists(atPath: destinationURL.path)) if let data = try? Data(contentsOf: destinationURL) { @@ -105,15 +115,15 @@ class DownloadResponseTestCase: BaseTestCase { func testCancelledDownloadRequest() { // Given let fileURL = randomCachesFileURL - let numberOfLines = 100 + let numberOfLines = 10 let urlString = "https://httpbin.org/stream/\(numberOfLines)" - let destination: DownloadRequest.DownloadFileDestination = { _, _ in (fileURL, []) } + let destination: DownloadRequest.Destination = { _, _ in (fileURL, []) } let expectation = self.expectation(description: "Cancelled download request should not download data to file") - var response: DefaultDownloadResponse? + var response: DownloadResponse? // When - Alamofire.download(urlString, to: destination) + AF.download(urlString, to: destination) .response { resp in response = resp expectation.fulfill() @@ -125,22 +135,22 @@ class DownloadResponseTestCase: BaseTestCase { // Then XCTAssertNotNil(response?.request) XCTAssertNil(response?.response) - XCTAssertNil(response?.destinationURL) + XCTAssertNil(response?.fileURL) XCTAssertNotNil(response?.error) } func testDownloadRequestWithProgress() { // Given - let randomBytes = 4 * 1024 * 1024 + let randomBytes = 1 * 1024 * 1024 let urlString = "https://httpbin.org/bytes/\(randomBytes)" let expectation = self.expectation(description: "Bytes download progress should be reported: \(urlString)") var progressValues: [Double] = [] - var response: DefaultDownloadResponse? + var response: DownloadResponse? // When - Alamofire.download(urlString) + AF.download(urlString) .downloadProgress { progress in progressValues.append(progress.fractionCompleted) } @@ -154,8 +164,7 @@ class DownloadResponseTestCase: BaseTestCase { // Then XCTAssertNotNil(response?.request) XCTAssertNotNil(response?.response) - XCTAssertNotNil(response?.temporaryURL) - XCTAssertNil(response?.destinationURL) + XCTAssertNotNil(response?.fileURL) XCTAssertNil(response?.resumeData) XCTAssertNil(response?.error) @@ -175,14 +184,16 @@ class DownloadResponseTestCase: BaseTestCase { func testDownloadRequestWithParameters() { // Given + let fileURL = randomCachesFileURL let urlString = "https://httpbin.org/get" let parameters = ["foo": "bar"] + let destination: DownloadRequest.Destination = { _, _ in (fileURL, []) } let expectation = self.expectation(description: "Download request should download data to file") - var response: DefaultDownloadResponse? + var response: DownloadResponse? // When - Alamofire.download(urlString, parameters: parameters) + AF.download(urlString, parameters: parameters, to: destination) .response { resp in response = resp expectation.fulfill() @@ -193,15 +204,13 @@ class DownloadResponseTestCase: BaseTestCase { // Then XCTAssertNotNil(response?.request) XCTAssertNotNil(response?.response) - XCTAssertNotNil(response?.temporaryURL) - XCTAssertNil(response?.destinationURL) + XCTAssertNotNil(response?.fileURL) XCTAssertNil(response?.resumeData) XCTAssertNil(response?.error) - + // TODO: Fails since the file is deleted by the time we get here? if - let temporaryURL = response?.temporaryURL, - let data = try? Data(contentsOf: temporaryURL), - let jsonObject = try? JSONSerialization.jsonObject(with: data, options: JSONSerialization.ReadingOptions(rawValue: 0)), + let data = try? Data(contentsOf: fileURL), + let jsonObject = try? JSONSerialization.jsonObject(with: data, options: []), let json = jsonObject as? [String: Any], let args = json["args"] as? [String: String] { @@ -215,14 +224,14 @@ class DownloadResponseTestCase: BaseTestCase { // Given let fileURL = randomCachesFileURL let urlString = "https://httpbin.org/get" - let headers = ["Authorization": "123456"] - let destination: DownloadRequest.DownloadFileDestination = { _, _ in (fileURL, []) } + let headers: HTTPHeaders = ["Authorization": "123456"] + let destination: DownloadRequest.Destination = { _, _ in (fileURL, []) } let expectation = self.expectation(description: "Download request should download data to file: \(fileURL)") - var response: DefaultDownloadResponse? + var response: DownloadResponse? // When - Alamofire.download(urlString, headers: headers, to: destination) + AF.download(urlString, headers: headers, to: destination) .response { resp in response = resp expectation.fulfill() @@ -233,7 +242,7 @@ class DownloadResponseTestCase: BaseTestCase { // Then XCTAssertNotNil(response?.request) XCTAssertNotNil(response?.response) - XCTAssertNotNil(response?.destinationURL) + XCTAssertNotNil(response?.fileURL) XCTAssertNil(response?.resumeData) XCTAssertNil(response?.error) @@ -254,10 +263,10 @@ class DownloadResponseTestCase: BaseTestCase { let fileURL = testDirectoryURL.appendingPathComponent("some/random/folder/test_output.json") let expectation = self.expectation(description: "Download request should download data but fail to move file") - var response: DefaultDownloadResponse? + var response: DownloadResponse? // When - Alamofire.download("https://httpbin.org/get", to: { _, _ in (fileURL, [])}) + AF.download("https://httpbin.org/get", to: { _, _ in (fileURL, [])}) .response { resp in response = resp expectation.fulfill() @@ -268,8 +277,7 @@ class DownloadResponseTestCase: BaseTestCase { // Then XCTAssertNotNil(response?.request) XCTAssertNotNil(response?.response) - XCTAssertNotNil(response?.temporaryURL) - XCTAssertNotNil(response?.destinationURL) + XCTAssertNil(response?.fileURL) XCTAssertNil(response?.resumeData) XCTAssertNotNil(response?.error) @@ -285,10 +293,10 @@ class DownloadResponseTestCase: BaseTestCase { let fileURL = testDirectoryURL.appendingPathComponent("some/random/folder/test_output.json") let expectation = self.expectation(description: "Download request should download data to file: \(fileURL)") - var response: DefaultDownloadResponse? + var response: DownloadResponse? // When - Alamofire.download("https://httpbin.org/get", to: { _, _ in (fileURL, [.createIntermediateDirectories])}) + AF.download("https://httpbin.org/get", to: { _, _ in (fileURL, [.createIntermediateDirectories])}) .response { resp in response = resp expectation.fulfill() @@ -299,8 +307,7 @@ class DownloadResponseTestCase: BaseTestCase { // Then XCTAssertNotNil(response?.request) XCTAssertNotNil(response?.response) - XCTAssertNotNil(response?.temporaryURL) - XCTAssertNotNil(response?.destinationURL) + XCTAssertNotNil(response?.fileURL) XCTAssertNil(response?.resumeData) XCTAssertNil(response?.error) } @@ -315,10 +322,10 @@ class DownloadResponseTestCase: BaseTestCase { try "random_data".write(to: fileURL, atomically: true, encoding: .utf8) let expectation = self.expectation(description: "Download should complete but fail to move file") - var response: DefaultDownloadResponse? + var response: DownloadResponse? // When - Alamofire.download("https://httpbin.org/get", to: { _, _ in (fileURL, [])}) + AF.download("https://httpbin.org/get", to: { _, _ in (fileURL, [])}) .response { resp in response = resp expectation.fulfill() @@ -331,8 +338,7 @@ class DownloadResponseTestCase: BaseTestCase { XCTAssertNotNil(response?.request) XCTAssertNotNil(response?.response) - XCTAssertNotNil(response?.temporaryURL) - XCTAssertNotNil(response?.destinationURL) + XCTAssertNil(response?.fileURL) XCTAssertNil(response?.resumeData) XCTAssertNotNil(response?.error) @@ -354,10 +360,10 @@ class DownloadResponseTestCase: BaseTestCase { let fileURL = directoryURL.appendingPathComponent("test_output.json") let expectation = self.expectation(description: "Download should complete and move file to URL: \(fileURL)") - var response: DefaultDownloadResponse? + var response: DownloadResponse? // When - Alamofire.download("https://httpbin.org/get", to: { _, _ in (fileURL, [.removePreviousFile])}) + AF.download("https://httpbin.org/get", to: { _, _ in (fileURL, [.removePreviousFile, .createIntermediateDirectories])}) .response { resp in response = resp expectation.fulfill() @@ -370,8 +376,7 @@ class DownloadResponseTestCase: BaseTestCase { XCTAssertNotNil(response?.request) XCTAssertNotNil(response?.response) - XCTAssertNotNil(response?.temporaryURL) - XCTAssertNotNil(response?.destinationURL) + XCTAssertNotNil(response?.fileURL) XCTAssertNil(response?.resumeData) XCTAssertNil(response?.error) } @@ -387,10 +392,10 @@ class DownloadResumeDataTestCase: BaseTestCase { let expectation = self.expectation(description: "Download should be cancelled") var cancelled = false - var response: DefaultDownloadResponse? + var response: DownloadResponse? // When - let download = Alamofire.download(urlString) + let download = AF.download(urlString) download.downloadProgress { progress in guard !cancelled else { return } @@ -409,7 +414,7 @@ class DownloadResumeDataTestCase: BaseTestCase { // Then XCTAssertNotNil(response?.request) XCTAssertNotNil(response?.response) - XCTAssertNil(response?.destinationURL) + XCTAssertNil(response?.fileURL) XCTAssertNotNil(response?.error) XCTAssertNotNil(response?.resumeData) @@ -426,7 +431,7 @@ class DownloadResumeDataTestCase: BaseTestCase { var response: DownloadResponse? // When - let download = Alamofire.download(urlString) + let download = AF.download(urlString) download.downloadProgress { progress in guard !cancelled else { return } @@ -445,7 +450,7 @@ class DownloadResumeDataTestCase: BaseTestCase { // Then XCTAssertNotNil(response?.request) XCTAssertNotNil(response?.response) - XCTAssertNil(response?.destinationURL) + XCTAssertNil(response?.fileURL) XCTAssertEqual(response?.result.isFailure, true) XCTAssertNotNil(response?.result.error) @@ -463,7 +468,7 @@ class DownloadResumeDataTestCase: BaseTestCase { var response1: DownloadResponse? // When - let download = Alamofire.download(urlString) + let download = AF.download(urlString) download.downloadProgress { progress in guard !cancelled else { return } @@ -488,8 +493,10 @@ class DownloadResumeDataTestCase: BaseTestCase { var progressValues: [Double] = [] var response2: DownloadResponse? - - Alamofire.download(resumingWith: resumeData) + let destination = DownloadRequest.suggestedDownloadDestination(options: [.removePreviousFile, .createIntermediateDirectories]) + // TODO: Added destination because temp file was being deleted very quickly. + AF.download(resumingWith: resumeData, + to: destination) .downloadProgress { progress in progressValues.append(progress.fractionCompleted) } @@ -503,13 +510,12 @@ class DownloadResumeDataTestCase: BaseTestCase { // Then XCTAssertNotNil(response1?.request) XCTAssertNotNil(response1?.response) - XCTAssertNil(response1?.destinationURL) + XCTAssertNil(response1?.fileURL) XCTAssertEqual(response1?.result.isFailure, true) XCTAssertNotNil(response1?.result.error) XCTAssertNotNil(response2?.response) - XCTAssertNotNil(response2?.temporaryURL) - XCTAssertNil(response2?.destinationURL) + XCTAssertNotNil(response2?.fileURL) XCTAssertEqual(response2?.result.isSuccess, true) XCTAssertNil(response2?.result.error) @@ -528,7 +534,7 @@ class DownloadResponseMapTestCase: BaseTestCase { var response: DownloadResponse? // When - Alamofire.download(urlString, parameters: ["foo": "bar"]).responseJSON { resp in + AF.download(urlString, parameters: ["foo": "bar"]).responseJSON { resp in response = resp.map { json in // json["args"]["foo"] is "bar": use this invariant to test the map function return ((json as? [String: Any])?["args"] as? [String: Any])?["foo"] as? String ?? "invalid" @@ -542,15 +548,11 @@ class DownloadResponseMapTestCase: BaseTestCase { // Then XCTAssertNotNil(response?.request) XCTAssertNotNil(response?.response) - XCTAssertNotNil(response?.temporaryURL) - XCTAssertNil(response?.destinationURL) + XCTAssertNotNil(response?.fileURL) XCTAssertNil(response?.resumeData) XCTAssertNil(response?.error) XCTAssertEqual(response?.result.value, "bar") - - if #available(iOS 10.0, macOS 10.12, tvOS 10.0, *) { - XCTAssertNotNil(response?.metrics) - } + XCTAssertNotNil(response?.metrics) } func testThatMapPreservesFailureError() { @@ -561,7 +563,7 @@ class DownloadResponseMapTestCase: BaseTestCase { var response: DownloadResponse? // When - Alamofire.download(urlString, parameters: ["foo": "bar"]).responseJSON { resp in + AF.download(urlString, parameters: ["foo": "bar"]).responseJSON { resp in response = resp.map { _ in "ignored" } expectation.fulfill() } @@ -571,15 +573,11 @@ class DownloadResponseMapTestCase: BaseTestCase { // Then XCTAssertNotNil(response?.request) XCTAssertNil(response?.response) - XCTAssertNil(response?.temporaryURL) - XCTAssertNil(response?.destinationURL) + XCTAssertNil(response?.fileURL) XCTAssertNil(response?.resumeData) XCTAssertNotNil(response?.error) XCTAssertEqual(response?.result.isFailure, true) - - if #available(iOS 10.0, macOS 10.12, tvOS 10.0, *) { - XCTAssertNotNil(response?.metrics) - } + XCTAssertNotNil(response?.metrics) } } @@ -594,7 +592,7 @@ class DownloadResponseFlatMapTestCase: BaseTestCase { var response: DownloadResponse? // When - Alamofire.download(urlString, parameters: ["foo": "bar"]).responseJSON { resp in + AF.download(urlString, parameters: ["foo": "bar"]).responseJSON { resp in response = resp.flatMap { json in // json["args"]["foo"] is "bar": use this invariant to test the map function return ((json as? [String: Any])?["args"] as? [String: Any])?["foo"] as? String ?? "invalid" @@ -608,15 +606,11 @@ class DownloadResponseFlatMapTestCase: BaseTestCase { // Then XCTAssertNotNil(response?.request) XCTAssertNotNil(response?.response) - XCTAssertNotNil(response?.temporaryURL) - XCTAssertNil(response?.destinationURL) + XCTAssertNotNil(response?.fileURL) XCTAssertNil(response?.resumeData) XCTAssertNil(response?.error) XCTAssertEqual(response?.result.value, "bar") - - if #available(iOS 10.0, macOS 10.12, tvOS 10.0, *) { - XCTAssertNotNil(response?.metrics) - } + XCTAssertNotNil(response?.metrics) } func testThatFlatMapCatchesTransformationError() { @@ -629,7 +623,7 @@ class DownloadResponseFlatMapTestCase: BaseTestCase { var response: DownloadResponse? // When - Alamofire.download(urlString, parameters: ["foo": "bar"]).responseJSON { resp in + AF.download(urlString, parameters: ["foo": "bar"]).responseJSON { resp in response = resp.flatMap { json in throw TransformError() } @@ -642,8 +636,7 @@ class DownloadResponseFlatMapTestCase: BaseTestCase { // Then XCTAssertNotNil(response?.request) XCTAssertNotNil(response?.response) - XCTAssertNotNil(response?.temporaryURL) - XCTAssertNil(response?.destinationURL) + XCTAssertNotNil(response?.fileURL) XCTAssertNil(response?.resumeData) if let error = response?.result.error { XCTAssertTrue(error is TransformError) @@ -651,9 +644,7 @@ class DownloadResponseFlatMapTestCase: BaseTestCase { XCTFail("flatMap should catch the transformation error") } - if #available(iOS 10.0, macOS 10.12, tvOS 10.0, *) { - XCTAssertNotNil(response?.metrics) - } + XCTAssertNotNil(response?.metrics) } func testThatFlatMapPreservesFailureError() { @@ -664,7 +655,7 @@ class DownloadResponseFlatMapTestCase: BaseTestCase { var response: DownloadResponse? // When - Alamofire.download(urlString, parameters: ["foo": "bar"]).responseJSON { resp in + AF.download(urlString, parameters: ["foo": "bar"]).responseJSON { resp in response = resp.flatMap { _ in "ignored" } expectation.fulfill() } @@ -674,15 +665,11 @@ class DownloadResponseFlatMapTestCase: BaseTestCase { // Then XCTAssertNotNil(response?.request) XCTAssertNil(response?.response) - XCTAssertNil(response?.temporaryURL) - XCTAssertNil(response?.destinationURL) + XCTAssertNil(response?.fileURL) XCTAssertNil(response?.resumeData) XCTAssertNotNil(response?.error) XCTAssertEqual(response?.result.isFailure, true) - - if #available(iOS 10.0, macOS 10.12, tvOS 10.0, *) { - XCTAssertNotNil(response?.metrics) - } + XCTAssertNotNil(response?.metrics) } } @@ -695,7 +682,7 @@ class DownloadResponseMapErrorTestCase: BaseTestCase { var response: DownloadResponse? // When - Alamofire.download(urlString).responseJSON { resp in + AF.download(urlString).responseJSON { resp in response = resp.mapError { error in return TestError.error(error: error) } @@ -708,16 +695,14 @@ class DownloadResponseMapErrorTestCase: BaseTestCase { // Then XCTAssertNotNil(response?.request) XCTAssertNil(response?.response) - XCTAssertNil(response?.temporaryURL) - XCTAssertNil(response?.destinationURL) + XCTAssertNil(response?.fileURL) XCTAssertNil(response?.resumeData) XCTAssertNotNil(response?.error) XCTAssertEqual(response?.result.isFailure, true) + guard let error = response?.error as? TestError, case .error = error else { XCTFail(); return } - if #available(iOS 10.0, macOS 10.12, tvOS 10.0, *) { - XCTAssertNotNil(response?.metrics) - } + XCTAssertNotNil(response?.metrics) } func testThatMapErrorPreservesSuccessValue() { @@ -728,7 +713,7 @@ class DownloadResponseMapErrorTestCase: BaseTestCase { var response: DownloadResponse? // When - Alamofire.download(urlString).responseData { resp in + AF.download(urlString).responseData { resp in response = resp.mapError { TestError.error(error: $0) } expectation.fulfill() } @@ -738,14 +723,10 @@ class DownloadResponseMapErrorTestCase: BaseTestCase { // Then XCTAssertNotNil(response?.request) XCTAssertNotNil(response?.response) - XCTAssertNotNil(response?.temporaryURL) - XCTAssertNil(response?.destinationURL) + XCTAssertNotNil(response?.fileURL) XCTAssertNil(response?.resumeData) XCTAssertEqual(response?.result.isSuccess, true) - - if #available(iOS 10.0, macOS 10.12, tvOS 10.0, *) { - XCTAssertNotNil(response?.metrics) - } + XCTAssertNotNil(response?.metrics) } } @@ -760,7 +741,7 @@ class DownloadResponseFlatMapErrorTestCase: BaseTestCase { var response: DownloadResponse? // When - Alamofire.download(urlString).responseData { resp in + AF.download(urlString).responseData { resp in response = resp.flatMapError { TestError.error(error: $0) } expectation.fulfill() } @@ -770,15 +751,11 @@ class DownloadResponseFlatMapErrorTestCase: BaseTestCase { // Then XCTAssertNotNil(response?.request) XCTAssertNotNil(response?.response) - XCTAssertNotNil(response?.temporaryURL) - XCTAssertNil(response?.destinationURL) + XCTAssertNotNil(response?.fileURL) XCTAssertNil(response?.resumeData) XCTAssertNil(response?.error) XCTAssertEqual(response?.result.isSuccess, true) - - if #available(iOS 10.0, macOS 10.12, tvOS 10.0, *) { - XCTAssertNotNil(response?.metrics) - } + XCTAssertNotNil(response?.metrics) } func testThatFlatMapErrorCatchesTransformationError() { @@ -789,7 +766,7 @@ class DownloadResponseFlatMapErrorTestCase: BaseTestCase { var response: DownloadResponse? // When - Alamofire.download(urlString).responseData { resp in + AF.download(urlString).responseData { resp in response = resp.flatMapError { _ in try TransformationError.error.alwaysFails() } expectation.fulfill() } @@ -799,8 +776,7 @@ class DownloadResponseFlatMapErrorTestCase: BaseTestCase { // Then XCTAssertNotNil(response?.request) XCTAssertNil(response?.response) - XCTAssertNil(response?.temporaryURL) - XCTAssertNil(response?.destinationURL) + XCTAssertNil(response?.fileURL) XCTAssertNil(response?.resumeData) XCTAssertNotNil(response?.error) XCTAssertEqual(response?.result.isFailure, true) @@ -811,9 +787,7 @@ class DownloadResponseFlatMapErrorTestCase: BaseTestCase { XCTFail("flatMapError should catch the transformation error") } - if #available(iOS 10.0, macOS 10.12, tvOS 10.0, *) { - XCTAssertNotNil(response?.metrics) - } + XCTAssertNotNil(response?.metrics) } func testThatFlatMapErrorTransformsError() { @@ -824,7 +798,7 @@ class DownloadResponseFlatMapErrorTestCase: BaseTestCase { var response: DownloadResponse? // When - Alamofire.download(urlString).responseData { resp in + AF.download(urlString).responseData { resp in response = resp.flatMapError { TestError.error(error: $0) } expectation.fulfill() } @@ -834,15 +808,12 @@ class DownloadResponseFlatMapErrorTestCase: BaseTestCase { // Then XCTAssertNotNil(response?.request) XCTAssertNil(response?.response) - XCTAssertNil(response?.temporaryURL) - XCTAssertNil(response?.destinationURL) + XCTAssertNil(response?.fileURL) XCTAssertNil(response?.resumeData) XCTAssertNotNil(response?.error) XCTAssertEqual(response?.result.isFailure, true) guard let error = response?.error as? TestError, case .error = error else { XCTFail(); return } - if #available(iOS 10.0, macOS 10.12, tvOS 10.0, *) { - XCTAssertNotNil(response?.metrics) - } + XCTAssertNotNil(response?.metrics) } } diff --git a/Tests/HTTPBin.swift b/Tests/HTTPBin.swift new file mode 100644 index 000000000..4f162b9e1 --- /dev/null +++ b/Tests/HTTPBin.swift @@ -0,0 +1,70 @@ +// +// HTTPBin.swift +// +// Copyright (c) 2014-2018 Alamofire Software Foundation (http://alamofire.org/) +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +import Alamofire +import Foundation + +extension String { + static let httpBinURLString = "https://httpbin.org" +} + +extension URL { + static func makeHTTPBinURL(path: String = "get") -> URL { + let url = URL(string: .httpBinURLString)! + return url.appendingPathComponent(path) + } +} + +extension URLRequest { + static func makeHTTPBinRequest(path: String = "get", + method: HTTPMethod = .get, + headers: HTTPHeaders = .init()) -> URLRequest { + var request = URLRequest(url: .makeHTTPBinURL(path: path)) + request.httpMethod = method.rawValue + request.httpHeaders = headers + + return request + } +} + +extension Data { + var asString: String { + return String(data: self, encoding: .utf8)! + } +} + +struct HTTPBinResponse: Decodable { + let headers: [String: String] + let origin: String + let url: String + let data: String? + let form: [String: String]? + let args: [String: String] +} + +struct HTTPBinParameters: Encodable { + static let `default` = HTTPBinParameters(property: "property") + + let property: String +} diff --git a/Tests/HTTPHeadersTests.swift b/Tests/HTTPHeadersTests.swift new file mode 100644 index 000000000..038153ffe --- /dev/null +++ b/Tests/HTTPHeadersTests.swift @@ -0,0 +1,133 @@ +// +// HTTPHeadersTests.swift +// +// Copyright (c) 2014-2018 Alamofire Software Foundation (http://alamofire.org/) +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +import Alamofire +import XCTest + +class HTTPHeadersTests: BaseTestCase { + func testHeadersAreStoreUniquelyByCaseInsensitiveName() { + // Given + let headersFromDictionaryLiteral: HTTPHeaders = ["key": "", "Key": "", "KEY": ""] + let headersFromDictionary = HTTPHeaders(["key": "", "Key": "", "KEY": ""]) + let headersFromArrayLiteral: HTTPHeaders = [HTTPHeader(name: "key", value: ""), + HTTPHeader(name: "Key", value: ""), + HTTPHeader(name: "KEY", value: "")] + let headersFromArray = HTTPHeaders([HTTPHeader(name: "key", value: ""), + HTTPHeader(name: "Key", value: ""), + HTTPHeader(name: "KEY", value: "")]) + var headersCreatedManually = HTTPHeaders() + headersCreatedManually.update(HTTPHeader(name: "key", value: "")) + headersCreatedManually.update(name: "Key", value: "") + headersCreatedManually.update(name: "KEY", value: "") + + // When, Then + XCTAssertEqual(headersFromDictionaryLiteral.count, 1) + XCTAssertEqual(headersFromDictionary.count, 1) + XCTAssertEqual(headersFromArrayLiteral.count, 1) + XCTAssertEqual(headersFromArray.count, 1) + XCTAssertEqual(headersCreatedManually.count, 1) + } + + func testHeadersPreserveOrderOfInsertion() { + // Given + let headersFromDictionaryLiteral: HTTPHeaders = ["c": "", "a": "", "b": ""] + // Dictionary initializer can't preserve order. + let headersFromArrayLiteral: HTTPHeaders = [HTTPHeader(name: "b", value: ""), + HTTPHeader(name: "a", value: ""), + HTTPHeader(name: "c", value: "")] + let headersFromArray = HTTPHeaders([HTTPHeader(name: "b", value: ""), + HTTPHeader(name: "a", value: ""), + HTTPHeader(name: "c", value: "")]) + var headersCreatedManually = HTTPHeaders() + headersCreatedManually.update(HTTPHeader(name: "c", value: "")) + headersCreatedManually.update(name: "b", value: "") + headersCreatedManually.update(name: "a", value: "") + + // When + let dictionaryLiteralNames = headersFromDictionaryLiteral.map { $0.name } + let arrayLiteralNames = headersFromArrayLiteral.map { $0.name } + let arrayNames = headersFromArray.map { $0.name } + let manualNames = headersCreatedManually.map { $0.name } + + // Then + XCTAssertEqual(dictionaryLiteralNames, ["c", "a", "b"]) + XCTAssertEqual(arrayLiteralNames, ["b", "a", "c"]) + XCTAssertEqual(arrayNames, ["b", "a", "c"]) + XCTAssertEqual(manualNames, ["c", "b", "a"]) + } + + func testHeadersCanBeProperlySortedByName() { + // Given + let headers: HTTPHeaders = ["c": "", "a": "", "b": ""] + + // When + let sortedHeaders = headers.sorted() + + // Then + XCTAssertEqual(headers.map { $0.name }, ["c", "a", "b"]) + XCTAssertEqual(sortedHeaders.map { $0.name }, ["a", "b", "c"]) + } + + func testHeadersCanInsensitivelyGetAndSetThroughSubscript() { + // Given + var headers: HTTPHeaders = ["c": "", "a": "", "b": ""] + + // When + headers["C"] = "c" + headers["a"] = "a" + headers["b"] = "b" + + // Then + XCTAssertEqual(headers["c"], "c") + XCTAssertEqual(headers.map { $0.value }, ["c", "a", "b"]) + XCTAssertEqual(headers.count, 3) + } + + func testHeadersPreserveLastFormAndValueOfAName() { + // Given + var headers: HTTPHeaders = ["c": "a"] + + // When + headers["C"] = "c" + + // Then + XCTAssertEqual(headers.description, "C: c") + } + + func testHeadersHaveUnsortedDescription() { + // Given + let headers: HTTPHeaders = ["c": "c", "a": "a", "b": "b"] + + // When + let description = headers.description + let expectedDescription = """ + c: c + a: a + b: b + """ + + // Then + XCTAssertEqual(description, expectedDescription) + } +} diff --git a/Tests/MultipartFormDataTests.swift b/Tests/MultipartFormDataTests.swift index 3ac6ee283..91251a2da 100644 --- a/Tests/MultipartFormDataTests.swift +++ b/Tests/MultipartFormDataTests.swift @@ -51,10 +51,8 @@ struct BoundaryGenerator { } static func boundaryData(boundaryType: BoundaryType, boundaryKey: String) -> Data { - return BoundaryGenerator.boundary( - forBoundaryType: boundaryType, - boundaryKey: boundaryKey - ).data(using: .utf8, allowLossyConversion: false)! + return Data(BoundaryGenerator.boundary(forBoundaryType: boundaryType, + boundaryKey: boundaryKey).utf8) } } @@ -78,8 +76,8 @@ class MultipartFormDataPropertiesTestCase: BaseTestCase { func testThatContentLengthMatchesTotalBodyPartSize() { // Given let multipartFormData = MultipartFormData() - let data1 = "Lorem ipsum dolor sit amet.".data(using: .utf8, allowLossyConversion: false)! - let data2 = "Vim at integre alterum.".data(using: .utf8, allowLossyConversion: false)! + let data1 = Data("Lorem ipsum dolor sit amet.".utf8) + let data2 = Data("Vim at integre alterum.".utf8) // When multipartFormData.append(data1, withName: "data1") @@ -100,7 +98,7 @@ class MultipartFormDataEncodingTestCase: BaseTestCase { // Given let multipartFormData = MultipartFormData() - let data = "Lorem ipsum dolor sit amet.".data(using: .utf8, allowLossyConversion: false)! + let data = Data("Lorem ipsum dolor sit amet.".utf8) multipartFormData.append(data, withName: "data") var encodedData: Data? @@ -118,12 +116,13 @@ class MultipartFormDataEncodingTestCase: BaseTestCase { if let encodedData = encodedData { let boundary = multipartFormData.boundary - let expectedData = ( + let expectedString = ( BoundaryGenerator.boundary(forBoundaryType: .initial, boundaryKey: boundary) + "Content-Disposition: form-data; name=\"data\"\(crlf)\(crlf)" + "Lorem ipsum dolor sit amet." + BoundaryGenerator.boundary(forBoundaryType: .final, boundaryKey: boundary) - ).data(using: .utf8, allowLossyConversion: false)! + ) + let expectedData = Data(expectedString.utf8) XCTAssertEqual(encodedData, expectedData, "encoded data should match expected data") } @@ -158,17 +157,17 @@ class MultipartFormDataEncodingTestCase: BaseTestCase { let expectedString = ( BoundaryGenerator.boundary(forBoundaryType: .initial, boundaryKey: boundary) + - "Content-Disposition: form-data; name=\"french\"\(crlf)\(crlf)" + - "français" + - BoundaryGenerator.boundary(forBoundaryType: .encapsulated, boundaryKey: boundary) + - "Content-Type: text/plain\(crlf)" + - "Content-Disposition: form-data; name=\"japanese\"\(crlf)\(crlf)" + - "日本語" + - BoundaryGenerator.boundary(forBoundaryType: .encapsulated, boundaryKey: boundary) + - "Content-Type: text/plain\(crlf)" + - "Content-Disposition: form-data; name=\"emoji\"\(crlf)\(crlf)" + - "😃👍🏻🍻🎉" + - BoundaryGenerator.boundary(forBoundaryType: .final, boundaryKey: boundary) + "Content-Disposition: form-data; name=\"french\"\(crlf)\(crlf)" + + "français" + + BoundaryGenerator.boundary(forBoundaryType: .encapsulated, boundaryKey: boundary) + + "Content-Disposition: form-data; name=\"japanese\"\(crlf)" + + "Content-Type: text/plain\(crlf)\(crlf)" + + "日本語" + + BoundaryGenerator.boundary(forBoundaryType: .encapsulated, boundaryKey: boundary) + + "Content-Disposition: form-data; name=\"emoji\"\(crlf)" + + "Content-Type: text/plain\(crlf)\(crlf)" + + "😃👍🏻🍻🎉" + + BoundaryGenerator.boundary(forBoundaryType: .final, boundaryKey: boundary) ) let expectedData = Data(expectedString.utf8) @@ -201,8 +200,8 @@ class MultipartFormDataEncodingTestCase: BaseTestCase { var expectedData = Data() expectedData.append(BoundaryGenerator.boundaryData(boundaryType: .initial, boundaryKey: boundary)) expectedData.append(Data(( - "Content-Type: image/png\(crlf)" + - "Content-Disposition: form-data; name=\"unicorn\"; filename=\"unicorn.png\"\(crlf)\(crlf)").utf8 + "Content-Disposition: form-data; name=\"unicorn\"; filename=\"unicorn.png\"\(crlf)" + + "Content-Type: image/png\(crlf)\(crlf)").utf8 ) ) expectedData.append(try! Data(contentsOf: unicornImageURL)) @@ -240,15 +239,15 @@ class MultipartFormDataEncodingTestCase: BaseTestCase { var expectedData = Data() expectedData.append(BoundaryGenerator.boundaryData(boundaryType: .initial, boundaryKey: boundary)) expectedData.append(Data(( - "Content-Type: image/png\(crlf)" + - "Content-Disposition: form-data; name=\"unicorn\"; filename=\"unicorn.png\"\(crlf)\(crlf)").utf8 + "Content-Disposition: form-data; name=\"unicorn\"; filename=\"unicorn.png\"\(crlf)" + + "Content-Type: image/png\(crlf)\(crlf)").utf8 ) ) expectedData.append(try! Data(contentsOf: unicornImageURL)) expectedData.append(BoundaryGenerator.boundaryData(boundaryType: .encapsulated, boundaryKey: boundary)) expectedData.append(Data(( - "Content-Type: image/jpeg\(crlf)" + - "Content-Disposition: form-data; name=\"rainbow\"; filename=\"rainbow.jpg\"\(crlf)\(crlf)").utf8 + "Content-Disposition: form-data; name=\"rainbow\"; filename=\"rainbow.jpg\"\(crlf)" + + "Content-Type: image/jpeg\(crlf)\(crlf)").utf8 ) ) expectedData.append(try! Data(contentsOf: rainbowImageURL)) @@ -292,8 +291,8 @@ class MultipartFormDataEncodingTestCase: BaseTestCase { var expectedData = Data() expectedData.append(BoundaryGenerator.boundaryData(boundaryType: .initial, boundaryKey: boundary)) expectedData.append(Data(( - "Content-Type: image/png\(crlf)" + - "Content-Disposition: form-data; name=\"unicorn\"; filename=\"unicorn.png\"\(crlf)\(crlf)").utf8 + "Content-Disposition: form-data; name=\"unicorn\"; filename=\"unicorn.png\"\(crlf)" + + "Content-Type: image/png\(crlf)\(crlf)").utf8 ) ) expectedData.append(try! Data(contentsOf: unicornImageURL)) @@ -348,15 +347,15 @@ class MultipartFormDataEncodingTestCase: BaseTestCase { var expectedData = Data() expectedData.append(BoundaryGenerator.boundaryData(boundaryType: .initial, boundaryKey: boundary)) expectedData.append(Data(( - "Content-Type: image/png\(crlf)" + - "Content-Disposition: form-data; name=\"unicorn\"; filename=\"unicorn.png\"\(crlf)\(crlf)").utf8 + "Content-Disposition: form-data; name=\"unicorn\"; filename=\"unicorn.png\"\(crlf)" + + "Content-Type: image/png\(crlf)\(crlf)").utf8 ) ) expectedData.append(try! Data(contentsOf: unicornImageURL)) expectedData.append(BoundaryGenerator.boundaryData(boundaryType: .encapsulated, boundaryKey: boundary)) expectedData.append(Data(( - "Content-Type: image/jpeg\(crlf)" + - "Content-Disposition: form-data; name=\"rainbow\"; filename=\"rainbow.jpg\"\(crlf)\(crlf)").utf8 + "Content-Disposition: form-data; name=\"rainbow\"; filename=\"rainbow.jpg\"\(crlf)" + + "Content-Type: image/jpeg\(crlf)\(crlf)").utf8 ) ) expectedData.append(try! Data(contentsOf: rainbowImageURL)) @@ -412,15 +411,15 @@ class MultipartFormDataEncodingTestCase: BaseTestCase { expectedData.append(loremData) expectedData.append(BoundaryGenerator.boundaryData(boundaryType: .encapsulated, boundaryKey: boundary)) expectedData.append(Data(( - "Content-Type: image/png\(crlf)" + - "Content-Disposition: form-data; name=\"unicorn\"; filename=\"unicorn.png\"\(crlf)\(crlf)").utf8 + "Content-Disposition: form-data; name=\"unicorn\"; filename=\"unicorn.png\"\(crlf)" + + "Content-Type: image/png\(crlf)\(crlf)").utf8 ) ) expectedData.append(try! Data(contentsOf: unicornImageURL)) expectedData.append(BoundaryGenerator.boundaryData(boundaryType: .encapsulated, boundaryKey: boundary)) expectedData.append(Data(( - "Content-Type: image/jpeg\(crlf)" + - "Content-Disposition: form-data; name=\"rainbow\"; filename=\"rainbow.jpg\"\(crlf)\(crlf)").utf8 + "Content-Disposition: form-data; name=\"rainbow\"; filename=\"rainbow.jpg\"\(crlf)" + + "Content-Type: image/jpeg\(crlf)\(crlf)").utf8 ) ) expectedData.append(try! Data(contentsOf: rainbowImageURL)) @@ -441,7 +440,7 @@ class MultipartFormDataWriteEncodedDataToDiskTestCase: BaseTestCase { let fileURL = temporaryFileURL() let multipartFormData = MultipartFormData() - let data = "Lorem ipsum dolor sit amet.".data(using: .utf8, allowLossyConversion: false)! + let data = Data("Lorem ipsum dolor sit amet.".utf8) multipartFormData.append(data, withName: "data") var encodingError: Error? @@ -459,12 +458,11 @@ class MultipartFormDataWriteEncodedDataToDiskTestCase: BaseTestCase { if let fileData = try? Data(contentsOf: fileURL) { let boundary = multipartFormData.boundary - let expectedFileData = ( - BoundaryGenerator.boundary(forBoundaryType: .initial, boundaryKey: boundary) + - "Content-Disposition: form-data; name=\"data\"\(crlf)\(crlf)" + - "Lorem ipsum dolor sit amet." + - BoundaryGenerator.boundary(forBoundaryType: .final, boundaryKey: boundary) - ).data(using: .utf8, allowLossyConversion: false)! + let expectedString = BoundaryGenerator.boundary(forBoundaryType: .initial, boundaryKey: boundary) + + "Content-Disposition: form-data; name=\"data\"\(crlf)\(crlf)" + + "Lorem ipsum dolor sit amet." + + BoundaryGenerator.boundary(forBoundaryType: .final, boundaryKey: boundary) + let expectedFileData = Data(expectedString.utf8) XCTAssertEqual(fileData, expectedFileData, "file data should match expected file data") } else { @@ -477,9 +475,9 @@ class MultipartFormDataWriteEncodedDataToDiskTestCase: BaseTestCase { let fileURL = temporaryFileURL() let multipartFormData = MultipartFormData() - let frenchData = "français".data(using: .utf8, allowLossyConversion: false)! - let japaneseData = "日本語".data(using: .utf8, allowLossyConversion: false)! - let emojiData = "😃👍🏻🍻🎉".data(using: .utf8, allowLossyConversion: false)! + let frenchData = Data("français".utf8) + let japaneseData = Data("日本語".utf8) + let emojiData = Data("😃👍🏻🍻🎉".utf8) multipartFormData.append(frenchData, withName: "french") multipartFormData.append(japaneseData, withName: "japanese") @@ -500,7 +498,7 @@ class MultipartFormDataWriteEncodedDataToDiskTestCase: BaseTestCase { if let fileData = try? Data(contentsOf: fileURL) { let boundary = multipartFormData.boundary - let expectedFileData = ( + let expectedString = ( BoundaryGenerator.boundary(forBoundaryType: .initial, boundaryKey: boundary) + "Content-Disposition: form-data; name=\"french\"\(crlf)\(crlf)" + "français" + @@ -511,7 +509,8 @@ class MultipartFormDataWriteEncodedDataToDiskTestCase: BaseTestCase { "Content-Disposition: form-data; name=\"emoji\"\(crlf)\(crlf)" + "😃👍🏻🍻🎉" + BoundaryGenerator.boundary(forBoundaryType: .final, boundaryKey: boundary) - ).data(using: .utf8, allowLossyConversion: false)! + ) + let expectedFileData = Data(expectedString.utf8) XCTAssertEqual(fileData, expectedFileData, "file data should match expected file data") } else { @@ -545,8 +544,8 @@ class MultipartFormDataWriteEncodedDataToDiskTestCase: BaseTestCase { var expectedFileData = Data() expectedFileData.append(BoundaryGenerator.boundaryData(boundaryType: .initial, boundaryKey: boundary)) expectedFileData.append(Data(( - "Content-Type: image/png\(crlf)" + - "Content-Disposition: form-data; name=\"unicorn\"; filename=\"unicorn.png\"\(crlf)\(crlf)").utf8 + "Content-Disposition: form-data; name=\"unicorn\"; filename=\"unicorn.png\"\(crlf)" + + "Content-Type: image/png\(crlf)\(crlf)").utf8 ) ) expectedFileData.append(try! Data(contentsOf: unicornImageURL)) @@ -587,15 +586,15 @@ class MultipartFormDataWriteEncodedDataToDiskTestCase: BaseTestCase { var expectedFileData = Data() expectedFileData.append(BoundaryGenerator.boundaryData(boundaryType: .initial, boundaryKey: boundary)) expectedFileData.append(Data(( - "Content-Type: image/png\(crlf)" + - "Content-Disposition: form-data; name=\"unicorn\"; filename=\"unicorn.png\"\(crlf)\(crlf)").utf8 + "Content-Disposition: form-data; name=\"unicorn\"; filename=\"unicorn.png\"\(crlf)" + + "Content-Type: image/png\(crlf)\(crlf)").utf8 ) ) expectedFileData.append(try! Data(contentsOf: unicornImageURL)) expectedFileData.append(BoundaryGenerator.boundaryData(boundaryType: .encapsulated, boundaryKey: boundary)) expectedFileData.append(Data(( - "Content-Type: image/jpeg\(crlf)" + - "Content-Disposition: form-data; name=\"rainbow\"; filename=\"rainbow.jpg\"\(crlf)\(crlf)").utf8 + "Content-Disposition: form-data; name=\"rainbow\"; filename=\"rainbow.jpg\"\(crlf)" + + "Content-Type: image/jpeg\(crlf)\(crlf)").utf8 ) ) expectedFileData.append(try! Data(contentsOf: rainbowImageURL)) @@ -642,8 +641,8 @@ class MultipartFormDataWriteEncodedDataToDiskTestCase: BaseTestCase { var expectedFileData = Data() expectedFileData.append(BoundaryGenerator.boundaryData(boundaryType: .initial, boundaryKey: boundary)) expectedFileData.append(Data(( - "Content-Type: image/png\(crlf)" + - "Content-Disposition: form-data; name=\"unicorn\"; filename=\"unicorn.png\"\(crlf)\(crlf)").utf8 + "Content-Disposition: form-data; name=\"unicorn\"; filename=\"unicorn.png\"\(crlf)" + + "Content-Type: image/png\(crlf)\(crlf)").utf8 ) ) expectedFileData.append(try! Data(contentsOf: unicornImageURL)) @@ -702,15 +701,15 @@ class MultipartFormDataWriteEncodedDataToDiskTestCase: BaseTestCase { var expectedFileData = Data() expectedFileData.append(BoundaryGenerator.boundaryData(boundaryType: .initial, boundaryKey: boundary)) expectedFileData.append(Data(( - "Content-Type: image/png\(crlf)" + - "Content-Disposition: form-data; name=\"unicorn\"; filename=\"unicorn.png\"\(crlf)\(crlf)").utf8 + "Content-Disposition: form-data; name=\"unicorn\"; filename=\"unicorn.png\"\(crlf)" + + "Content-Type: image/png\(crlf)\(crlf)").utf8 ) ) expectedFileData.append(try! Data(contentsOf: unicornImageURL)) expectedFileData.append(BoundaryGenerator.boundaryData(boundaryType: .encapsulated, boundaryKey: boundary)) expectedFileData.append(Data(( - "Content-Type: image/jpeg\(crlf)" + - "Content-Disposition: form-data; name=\"rainbow\"; filename=\"rainbow.jpg\"\(crlf)\(crlf)").utf8 + "Content-Disposition: form-data; name=\"rainbow\"; filename=\"rainbow.jpg\"\(crlf)" + + "Content-Type: image/jpeg\(crlf)\(crlf)").utf8 ) ) expectedFileData.append(try! Data(contentsOf: rainbowImageURL)) @@ -769,15 +768,15 @@ class MultipartFormDataWriteEncodedDataToDiskTestCase: BaseTestCase { expectedFileData.append(loremData) expectedFileData.append(BoundaryGenerator.boundaryData(boundaryType: .encapsulated, boundaryKey: boundary)) expectedFileData.append(Data(( - "Content-Type: image/png\(crlf)" + - "Content-Disposition: form-data; name=\"unicorn\"; filename=\"unicorn.png\"\(crlf)\(crlf)").utf8 + "Content-Disposition: form-data; name=\"unicorn\"; filename=\"unicorn.png\"\(crlf)" + + "Content-Type: image/png\(crlf)\(crlf)").utf8 ) ) expectedFileData.append(try! Data(contentsOf: unicornImageURL)) expectedFileData.append(BoundaryGenerator.boundaryData(boundaryType: .encapsulated, boundaryKey: boundary)) expectedFileData.append(Data(( - "Content-Type: image/jpeg\(crlf)" + - "Content-Disposition: form-data; name=\"rainbow\"; filename=\"rainbow.jpg\"\(crlf)\(crlf)").utf8 + "Content-Disposition: form-data; name=\"rainbow\"; filename=\"rainbow.jpg\"\(crlf)" + + "Content-Type: image/jpeg\(crlf)\(crlf)").utf8 ) ) expectedFileData.append(try! Data(contentsOf: rainbowImageURL)) @@ -811,7 +810,7 @@ class MultipartFormDataFailureTestCase: BaseTestCase { // Then XCTAssertNotNil(encodingError, "encoding error should not be nil") - if let error = encodingError as? AFError { + if let error = encodingError?.asAFError { XCTAssertTrue(error.isBodyPartFilenameInvalid) let expectedFailureReason = "The URL provided does not have a valid filename: \(fileURL)" @@ -839,7 +838,7 @@ class MultipartFormDataFailureTestCase: BaseTestCase { // Then XCTAssertNotNil(encodingError, "encoding error should not be nil") - if let error = encodingError as? AFError { + if let error = encodingError?.asAFError { XCTAssertTrue(error.isBodyPartURLInvalid) let expectedFailureReason = "The URL provided is not a file URL: \(fileURL)" @@ -868,7 +867,7 @@ class MultipartFormDataFailureTestCase: BaseTestCase { // Then XCTAssertNotNil(encodingError, "encoding error should not be nil") - if let error = encodingError as? AFError { + if let error = encodingError?.asAFError { XCTAssertTrue(error.isBodyPartFileNotReachableWithError) } else { XCTFail("Error should be AFError.") @@ -893,7 +892,7 @@ class MultipartFormDataFailureTestCase: BaseTestCase { // Then XCTAssertNotNil(encodingError, "encoding error should not be nil") - if let error = encodingError as? AFError { + if let error = encodingError?.asAFError { XCTAssertTrue(error.isBodyPartFileIsDirectory) let expectedFailureReason = "The URL provided is a directory: \(directoryURL)" @@ -916,7 +915,7 @@ class MultipartFormDataFailureTestCase: BaseTestCase { } let multipartFormData = MultipartFormData() - let data = "Lorem ipsum dolor sit amet.".data(using: .utf8, allowLossyConversion: false)! + let data = Data("Lorem ipsum dolor sit amet.".utf8) multipartFormData.append(data, withName: "data") var encodingError: Error? @@ -932,7 +931,7 @@ class MultipartFormDataFailureTestCase: BaseTestCase { XCTAssertNil(writerError, "writer error should be nil") XCTAssertNotNil(encodingError, "encoding error should not be nil") - if let encodingError = encodingError as? AFError { + if let encodingError = encodingError?.asAFError { XCTAssertTrue(encodingError.isOutputStreamFileAlreadyExists) } } @@ -942,7 +941,7 @@ class MultipartFormDataFailureTestCase: BaseTestCase { let fileURL = URL(string: "/this/is/not/a/valid/url")! let multipartFormData = MultipartFormData() - let data = "Lorem ipsum dolor sit amet.".data(using: .utf8, allowLossyConversion: false)! + let data = Data("Lorem ipsum dolor sit amet.".utf8) multipartFormData.append(data, withName: "data") var encodingError: Error? @@ -957,7 +956,7 @@ class MultipartFormDataFailureTestCase: BaseTestCase { // Then XCTAssertNotNil(encodingError, "encoding error should not be nil") - if let encodingError = encodingError as? AFError { + if let encodingError = encodingError?.asAFError { XCTAssertTrue(encodingError.isOutputStreamURLInvalid) } } diff --git a/Tests/NSLoggingEventMonitor.swift b/Tests/NSLoggingEventMonitor.swift new file mode 100644 index 000000000..83a852ddf --- /dev/null +++ b/Tests/NSLoggingEventMonitor.swift @@ -0,0 +1,198 @@ +// +// NSLoggingEventMonitor.swift +// +// Copyright (c) 2014-2018 Alamofire Software Foundation (http://alamofire.org/) +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +import Alamofire +import Foundation + +public final class NSLoggingEventMonitor: EventMonitor { + public let queue = DispatchQueue(label: "org.alamofire.nsLoggingEventMonitorQueue", qos: .background) + + public init() { } + + public func urlSession(_ session: URLSession, didBecomeInvalidWithError error: Error?) { + NSLog("URLSession: \(session), didBecomeInvalidWithError: \(error?.localizedDescription ?? "None")") + } + + public func urlSession(_ session: URLSession, task: URLSessionTask, didReceive challenge: URLAuthenticationChallenge) { + NSLog("URLSession: \(session), task: \(task), didReceiveChallenge: \(challenge)") + } + + public func urlSession(_ session: URLSession, + task: URLSessionTask, + didSendBodyData bytesSent: Int64, + totalBytesSent: Int64, + totalBytesExpectedToSend: Int64) { + NSLog("URLSession: \(session), task: \(task), didSendBodyData: \(bytesSent), totalBytesSent: \(totalBytesSent), totalBytesExpectedToSent: \(totalBytesExpectedToSend)") + } + + public func urlSession(_ session: URLSession, taskNeedsNewBodyStream task: URLSessionTask) { + NSLog("URLSession: \(session), taskNeedsNewBodyStream: \(task)") + } + + public func urlSession(_ session: URLSession, + task: URLSessionTask, + willPerformHTTPRedirection response: HTTPURLResponse, + newRequest request: URLRequest) { + NSLog("URLSession: \(session), task: \(task), willPerformHTTPRedirection: \(response), newRequest: \(request)") + } + + public func urlSession(_ session: URLSession, task: URLSessionTask, didFinishCollecting metrics: URLSessionTaskMetrics) { + NSLog("URLSession: \(session), task: \(task), didFinishCollecting: \(metrics)") + } + + public func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) { + NSLog("URLSession: \(session), task: \(task), didCompleteWithError: \(error?.localizedDescription ?? "None")") + } + + public func urlSession(_ session: URLSession, taskIsWaitingForConnectivity task: URLSessionTask) { + NSLog("URLSession: \(session), taskIsWaitingForConnectivity: \(task)") + } + + public func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) { + NSLog("URLSession: \(session), dataTask: \(dataTask), didReceiveDataOfLength: \(data.count)") + } + + public func urlSession(_ session: URLSession, + dataTask: URLSessionDataTask, + willCacheResponse proposedResponse: CachedURLResponse) { + NSLog("URLSession: \(session), dataTask: \(dataTask), willCacheResponse: \(proposedResponse)") + } + + public func urlSession(_ session: URLSession, + downloadTask: URLSessionDownloadTask, + didResumeAtOffset fileOffset: Int64, + expectedTotalBytes: Int64) { + NSLog("URLSession: \(session), downloadTask: \(downloadTask), didResumeAtOffset: \(fileOffset), expectedTotalBytes: \(expectedTotalBytes)") + } + + public func urlSession(_ session: URLSession, + downloadTask: URLSessionDownloadTask, + didWriteData bytesWritten: Int64, + totalBytesWritten: Int64, + totalBytesExpectedToWrite: Int64) { + NSLog("URLSession: \(session), downloadTask: \(downloadTask), didWriteData bytesWritten: \(bytesWritten), totalBytesWritten: \(totalBytesWritten), totalBytesExpectedToWrite: \(totalBytesExpectedToWrite)") + } + + public func urlSession(_ session: URLSession, + downloadTask: URLSessionDownloadTask, + didFinishDownloadingTo location: URL) { + NSLog("URLSession: \(session), downloadTask: \(downloadTask), didFinishDownloadingTo: \(location)") + } + + public func request(_ request: Request, didCreateURLRequest urlRequest: URLRequest) { + NSLog("Request: \(request) didCreateURLRequest: \(urlRequest)") + } + + public func request(_ request: Request, didFailToCreateURLRequestWithError error: Error) { + NSLog("Request: \(request) didFailToCreateURLRequestWithError: \(error)") + } + + public func request(_ request: Request, didAdaptInitialRequest initialRequest: URLRequest, to adaptedRequest: URLRequest) { + NSLog("Request: \(request) didAdaptInitialRequest \(initialRequest) to \(adaptedRequest)") + } + + public func request(_ request: Request, didFailToAdaptURLRequest initialRequest: URLRequest, withError error: Error) { + NSLog("Request: \(request) didFailToAdaptURLRequest \(initialRequest) withError \(error)") + } + + public func request(_ request: Request, didCreateTask task: URLSessionTask) { + NSLog("Request: \(request) didCreateTask \(task)") + } + + public func request(_ request: Request, didGatherMetrics metrics: URLSessionTaskMetrics) { + NSLog("Request: \(request) didGatherMetrics \(metrics)") + } + + public func request(_ request: Request, didFailTask task: URLSessionTask, earlyWithError error: Error) { + NSLog("Request: \(request) didFailTask \(task) earlyWithError \(error)") + } + + public func request(_ request: Request, didCompleteTask task: URLSessionTask, with error: Error?) { + NSLog("Request: \(request) didCompleteTask \(task) withError: \(error?.localizedDescription ?? "None")") + } + + public func requestDidFinish(_ request: Request) { + NSLog("Request: \(request) didFinish") + } + + public func requestDidResume(_ request: Request) { + NSLog("Request: \(request) didResume") + } + + public func requestDidSuspend(_ request: Request) { + NSLog("Request: \(request) didSuspend") + } + + public func requestDidCancel(_ request: Request) { + NSLog("Request: \(request) didCancel") + } + + public func request(_ request: DataRequest, didParseResponse response: DataResponse) { + NSLog("Request: \(request), didParseResponse: \(response)") + } + + public func request(_ request: DataRequest, didParseResponse response: DataResponse) { + NSLog("Request: \(request), didParseResponse: \(response)") + } + + public func request(_ request: DownloadRequest, didParseResponse response: DownloadResponse) { + NSLog("Request: \(request), didParseResponse: \(response)") + } + + public func request(_ request: DownloadRequest, didParseResponse response: DownloadResponse) { + NSLog("Request: \(request), didParseResponse: \(response)") + } + + public func requestIsRetrying(_ request: Request) { + NSLog("Request: \(request), isRetrying") + } + + public func request(_ request: DataRequest, didValidateRequest urlRequest: URLRequest?, response: HTTPURLResponse, data: Data?, withResult result: Request.ValidationResult) { + NSLog("Request: \(request), didValidateRequestWithResult: \(result)") + } + + public func request(_ request: UploadRequest, didCreateUploadable uploadable: UploadRequest.Uploadable) { + NSLog("Request: \(request), didCreateUploadable: \(uploadable)") + } + + public func request(_ request: UploadRequest, didFailToCreateUploadableWithError error: Error) { + NSLog("Request: \(request), didFailToCreateUploadableWithError: \(error)") + } + + public func request(_ request: UploadRequest, didProvideInputStream stream: InputStream) { + NSLog("Request: \(request), didProvideInputStream: \(stream)") + } + + public func request(_ request: DownloadRequest, didFinishDownloadingUsing task: URLSessionTask, with result: Result) { + NSLog("Request: \(request), didFinishDownloadingUsing: \(task), withResult: \(result)") + } + + public func request(_ request: DownloadRequest, didCreateDestinationURL url: URL) { + NSLog("Request: \(request), didCreateDestinationURL: \(url)") + } + + public func request(_ request: DownloadRequest, didValidateRequest urlRequest: URLRequest?, response: HTTPURLResponse, temporaryURL: URL?, destinationURL: URL?, withResult result: Request.ValidationResult) { + NSLog("Request: \(request), didValidateRequestWithResult: \(result)") + } +} diff --git a/Tests/ParameterEncoderTests.swift b/Tests/ParameterEncoderTests.swift new file mode 100644 index 000000000..5fd2cc4e5 --- /dev/null +++ b/Tests/ParameterEncoderTests.swift @@ -0,0 +1,782 @@ +// +// ParameterEncoderTests.swift +// +// Copyright (c) 2014-2018 Alamofire Software Foundation (http://alamofire.org/) +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +import Alamofire +import XCTest + +final class JSONParameterEncoderTests: BaseTestCase { + func testThatDataIsProperlyEncodedAndProperContentTypeIsSet() throws { + // Given + let encoder = JSONParameterEncoder() + let request = URLRequest.makeHTTPBinRequest() + + // When + let newRequest = try encoder.encode(HTTPBinParameters.default, into: request) + + // Then + XCTAssertEqual(newRequest.httpHeaders["Content-Type"], "application/json") + XCTAssertEqual(newRequest.httpBody?.asString, "{\"property\":\"property\"}") + } + + func testThatDataIsProperlyEncodedButContentTypeIsNotSetIfRequestAlreadyHasAContentType() throws { + // Given + let encoder = JSONParameterEncoder() + var request = URLRequest.makeHTTPBinRequest() + request.httpHeaders.update(.contentType("type")) + + // When + let newRequest = try encoder.encode(HTTPBinParameters.default, into: request) + + // Then + XCTAssertEqual(newRequest.httpHeaders["Content-Type"], "type") + XCTAssertEqual(newRequest.httpBody?.asString, "{\"property\":\"property\"}") + } + + func testThatJSONEncoderCanBeCustomized() throws { + // Given + let jsonEncoder = JSONEncoder() + jsonEncoder.outputFormatting = .prettyPrinted + let encoder = JSONParameterEncoder(encoder: jsonEncoder) + let request = URLRequest.makeHTTPBinRequest() + + // When + let newRequest = try encoder.encode(HTTPBinParameters.default, into: request) + + // Then + let expected = """ + { + "property" : "property" + } + """ + XCTAssertEqual(newRequest.httpBody?.asString, expected) + } + + func testThatJSONEncoderDefaultWorks() throws { + // Given + let encoder = JSONParameterEncoder.default + let request = URLRequest.makeHTTPBinRequest() + + // When + let encoded = try encoder.encode(HTTPBinParameters.default, into: request) + + // Then + let expected = """ + {"property":"property"} + """ + XCTAssertEqual(encoded.httpBody?.asString, expected) + } + + func testThatJSONEncoderPrettyPrintedPrintsPretty() throws { + // Given + let encoder = JSONParameterEncoder.prettyPrinted + let request = URLRequest.makeHTTPBinRequest() + + // When + let encoded = try encoder.encode(HTTPBinParameters.default, into: request) + + // Then + let expected = """ + { + "property" : "property" + } + """ + XCTAssertEqual(encoded.httpBody?.asString, expected) + } +} + +final class SortedKeysJSONParameterEncoderTests: BaseTestCase { + func testTestJSONEncoderSortedKeysHasSortedKeys() throws { + guard #available(macOS 10.13, iOS 11.0, tvOS 11.0, watchOS 4.0, *) else { return } + // Given + let encoder = JSONParameterEncoder.sortedKeys + let request = URLRequest.makeHTTPBinRequest() + + // When + let encoded = try encoder.encode(["z": "z", "a": "a", "p": "p"], into: request) + + // Then + let expected = """ + {"a":"a","p":"p","z":"z"} + """ + XCTAssertEqual(encoded.httpBody?.asString, expected) + } +} + +final class URLEncodedFormParameterEncoderTests: BaseTestCase { + func testThatQueryIsBodyEncodedAndProperContentTypeIsSetForPOSTRequest() throws { + // Given + let encoder = URLEncodedFormParameterEncoder() + let request = URLRequest.makeHTTPBinRequest(method: .post) + + // When + let newRequest = try encoder.encode(HTTPBinParameters.default, into: request) + + // Then + XCTAssertEqual(newRequest.httpHeaders["Content-Type"], "application/x-www-form-urlencoded; charset=utf-8") + XCTAssertEqual(newRequest.httpBody?.asString, "property=property") + } + + func testThatQueryIsBodyEncodedButContentTypeIsNotSetWhenRequestAlreadyHasContentType() throws { + // Given + let encoder = URLEncodedFormParameterEncoder() + var request = URLRequest.makeHTTPBinRequest(method: .post) + request.httpHeaders.update(.contentType("type")) + + // When + let newRequest = try encoder.encode(HTTPBinParameters.default, into: request) + + // Then + XCTAssertEqual(newRequest.httpHeaders["Content-Type"], "type") + XCTAssertEqual(newRequest.httpBody?.asString, "property=property") + } + + func testThatEncoderCanBeCustomized() throws { + // Given + let urlEncoder = URLEncodedFormEncoder(boolEncoding: .literal) + let encoder = URLEncodedFormParameterEncoder(encoder: urlEncoder) + let request = URLRequest.makeHTTPBinRequest() + + // When + let newRequest = try encoder.encode(["bool": true], into: request) + + // Then + let components = URLComponents(url: newRequest.url!, resolvingAgainstBaseURL: false) + XCTAssertEqual(components?.percentEncodedQuery, "bool=true") + } + + func testThatQueryIsInURLWhenDestinationIsURLAndMethodIsPOST() throws { + // Given + let encoder = URLEncodedFormParameterEncoder(destination: .queryString) + let request = URLRequest.makeHTTPBinRequest(method: .post) + + // When + let newRequest = try encoder.encode(HTTPBinParameters.default, into: request) + + // Then + let components = URLComponents(url: newRequest.url!, resolvingAgainstBaseURL: false) + XCTAssertEqual(components?.percentEncodedQuery, "property=property") + } +} + +final class URLEncodedFormEncoderTests: BaseTestCase { + func testEncoderThrowsErrorWhenAttemptingToEncodeNilInKeyedContainer() { + // Given + let encoder = URLEncodedFormEncoder() + let parameters = FailingOptionalStruct(testedContainer: .keyed) + + // When + let result = Result { try encoder.encode(parameters) } + + // Then + XCTAssertTrue(result.isFailure) + } + + func testEncoderThrowsErrorWhenAttemptingToEncodeNilInUnkeyedContainer() { + // Given + let encoder = URLEncodedFormEncoder() + let parameters = FailingOptionalStruct(testedContainer: .unkeyed) + + // When + let result = Result { try encoder.encode(parameters) } + + // Then + XCTAssertTrue(result.isFailure) + } + + func testEncoderCanEncodeDictionary() { + // Given + let encoder = URLEncodedFormEncoder() + let parameters = ["a": "a"] + + // When + let result = Result { try encoder.encode(parameters) } + + // Then + XCTAssertEqual(result.value, "a=a") + } + + func testEncoderCanEncodeDouble() { + // Given + let encoder = URLEncodedFormEncoder() + let parameters = ["a": 1.0] + + // When + let result = Result { try encoder.encode(parameters) } + + // Then + XCTAssertEqual(result.value, "a=1.0") + } + + func testEncoderCanEncodeFloat() { + // Given + let encoder = URLEncodedFormEncoder() + let parameters: [String: Float] = ["a": 1.0] + + // When + let result = Result { try encoder.encode(parameters) } + + // Then + XCTAssertEqual(result.value, "a=1.0") + } + + func testEncoderCanEncodeInt8() { + // Given + let encoder = URLEncodedFormEncoder() + let parameters: [String: Int8] = ["a": 1] + + // When + let result = Result { try encoder.encode(parameters) } + + // Then + XCTAssertEqual(result.value, "a=1") + } + + func testEncoderCanEncodeInt16() { + // Given + let encoder = URLEncodedFormEncoder() + let parameters: [String: Int16] = ["a": 1] + + // When + let result = Result { try encoder.encode(parameters) } + + // Then + XCTAssertEqual(result.value, "a=1") + } + + func testEncoderCanEncodeInt32() { + // Given + let encoder = URLEncodedFormEncoder() + let parameters: [String: Int32] = ["a": 1] + + // When + let result = Result { try encoder.encode(parameters) } + + // Then + XCTAssertEqual(result.value, "a=1") + } + + func testEncoderCanEncodeInt64() { + // Given + let encoder = URLEncodedFormEncoder() + let parameters: [String: Int64] = ["a": 1] + + // When + let result = Result { try encoder.encode(parameters) } + + // Then + XCTAssertEqual(result.value, "a=1") + } + + func testEncoderCanEncodeUInt() { + // Given + let encoder = URLEncodedFormEncoder() + let parameters: [String: UInt] = ["a": 1] + + // When + let result = Result { try encoder.encode(parameters) } + + // Then + XCTAssertEqual(result.value, "a=1") + } + + func testEncoderCanEncodeUInt8() { + // Given + let encoder = URLEncodedFormEncoder() + let parameters: [String: UInt8] = ["a": 1] + + // When + let result = Result { try encoder.encode(parameters) } + + // Then + XCTAssertEqual(result.value, "a=1") + } + + func testEncoderCanEncodeUInt16() { + // Given + let encoder = URLEncodedFormEncoder() + let parameters: [String: UInt16] = ["a": 1] + + // When + let result = Result { try encoder.encode(parameters) } + + // Then + XCTAssertEqual(result.value, "a=1") + } + + func testEncoderCanEncodeUInt32() { + // Given + let encoder = URLEncodedFormEncoder() + let parameters: [String: UInt32] = ["a": 1] + + // When + let result = Result { try encoder.encode(parameters) } + + // Then + XCTAssertEqual(result.value, "a=1") + } + + func testEncoderCanEncodeUInt64() { + // Given + let encoder = URLEncodedFormEncoder() + let parameters: [String: UInt64] = ["a": 1] + + // When + let result = Result { try encoder.encode(parameters) } + + // Then + XCTAssertEqual(result.value, "a=1") + } + + func testThatNestedDictionariesHaveBracketedKeys() { + // Given + let encoder = URLEncodedFormEncoder() + let parameters = ["a": ["b": "b"]] + + // When + let result = Result { try encoder.encode(parameters) } + + // Then + XCTAssertEqual(result.value, "a%5Bb%5D=b") + } + + func testThatEncodableStructCanBeEncoded() { + // Given + let encoder = URLEncodedFormEncoder() + let parameters = EncodableStruct() + + // When + let result = Result { try encoder.encode(parameters) } + + // Then + let expected = "four%5B%5D=1&four%5B%5D=2&four%5B%5D=3&three=1&one=one&two=2&five%5Ba%5D=a&six%5Ba%5D%5Bb%5D=b&seven%5Ba%5D=a" + XCTAssertEqual(result.value, expected) + } + + func testThatManuallyEncodableStructCanBeEncoded() { + // Given + let encoder = URLEncodedFormEncoder() + let parameters = ManuallyEncodableStruct() + + // When + let result = Result { try encoder.encode(parameters) } + + // Then + let expected = "root%5B%5D%5B%5D=1&root%5B%5D%5B%5D=2&root%5B%5D%5B%5D=3&root%5B%5D%5Ba%5D%5Bstring%5D=string&root%5B%5D%5B%5D%5B%5D=1&root%5B%5D%5B%5D%5B%5D=2&root%5B%5D%5B%5D%5B%5D=3" + XCTAssertEqual(result.value, expected) + } + + func testThatEncodableClassWithNoInheritanceCanBeEncoded() { + // Given + let encoder = URLEncodedFormEncoder() + let parameters = EncodableSuperclass() + + // When + let result = Result { try encoder.encode(parameters) } + + // Then + XCTAssertEqual(result.value, "two=2&one=one&three=1") + } + + func testThatEncodableSubclassCanBeEncoded() { + // Given + let encoder = URLEncodedFormEncoder() + let parameters = EncodableSubclass() + + // When + let result = Result { try encoder.encode(parameters) } + + // Then + let expected = "four%5B%5D=1&four%5B%5D=2&four%5B%5D=3&two=2&five%5Ba%5D=a&five%5Bb%5D=b&three=1&one=one" + XCTAssertEqual(result.value, expected) + } + + func testThatManuallyEncodableSubclassCanBeEncoded() { + // Given + let encoder = URLEncodedFormEncoder() + let parameters = ManuallyEncodableSubclass() + + // When + let result = Result { try encoder.encode(parameters) } + + // Then + let expected = "five%5Ba%5D=a&five%5Bb%5D=b&four%5Bfour%5D=one&four%5Bfive%5D=2" + XCTAssertEqual(result.value, expected) + } + + func testThatARootArrayCannotBeEncoded() { + // Given + let encoder = URLEncodedFormEncoder() + let parameters = [1] + + // When + let result = Result { try encoder.encode(parameters) } + + // Then + XCTAssertFalse(result.isSuccess) + } + + func testThatARootValueCannotBeEncoded() { + // Given + let encoder = URLEncodedFormEncoder() + let parameters = "string" + + // When + let result = Result { try encoder.encode(parameters) } + + // Then + XCTAssertFalse(result.isSuccess) + } + + func testThatOptionalValuesCannotBeEncoded() { + // Givenp + let encoder = URLEncodedFormEncoder() + let parameters: [String: String?] = ["string": nil] + + // When + let result = Result { try encoder.encode(parameters) } + + // Then + XCTAssertFalse(result.isSuccess) + } + + func testThatBoolsCanBeLiteralEncoded() { + // Given + let encoder = URLEncodedFormEncoder(boolEncoding: .literal) + let parameters = ["bool": true] + + // When + let result = Result { try encoder.encode(parameters) } + + // Then + XCTAssertEqual(result.value, "bool=true") + } + + func testThatArraysCanBeEncodedWithoutBrackets() { + // Given + let encoder = URLEncodedFormEncoder(arrayEncoding: .noBrackets) + let parameters = ["array": [1, 2]] + + // When + let result = Result { try encoder.encode(parameters) } + + // Then + XCTAssertEqual(result.value, "array=1&array=2") + } + + func testThatSpacesCanBeEncodedAsPluses() { + // Given + let encoder = URLEncodedFormEncoder(spaceEncoding: .plusReplaced) + let parameters = ["spaces": "replace with spaces"] + + // When + let result = Result { try encoder.encode(parameters) } + + // Then + XCTAssertEqual(result.value, "spaces=replace+with+spaces") + } + + func testThatEscapedCharactersCanBeCustomized() { + // Given + var allowed = CharacterSet.afURLQueryAllowed + allowed.remove(charactersIn: "?/") + let encoder = URLEncodedFormEncoder(allowedCharacters: allowed) + let parameters = ["allowed": "?/"] + + // When + let result = Result { try encoder.encode(parameters) } + + // Then + XCTAssertEqual(result.value, "allowed=%3F%2F") + } + + func testThatUnreservedCharactersAreNotPercentEscaped() { + // Given + let encoder = URLEncodedFormEncoder() + let parameters = ["lowercase": "abcdefghijklmnopqrstuvwxyz", + "uppercase": "ABCDEFGHIJKLMNOPQRSTUVWXYZ", + "numbers": "0123456789"] + + // When + let result = Result { try encoder.encode(parameters) } + + // Then + XCTAssertEqual(result.value, + "uppercase=ABCDEFGHIJKLMNOPQRSTUVWXYZ&numbers=0123456789&lowercase=abcdefghijklmnopqrstuvwxyz") + } + + func testThatReseredCharactersArePercentEscaped() { + // Given + let encoder = URLEncodedFormEncoder() + let generalDelimiters = ":#[]@" + let subDelimiters = "!$&'()*+,;=" + let parameters = ["reserved": "\(generalDelimiters)\(subDelimiters)"] + + // When + let result = Result { try encoder.encode(parameters) } + + // Then + XCTAssertEqual(result.value, "reserved=%3A%23%5B%5D%40%21%24%26%27%28%29%2A%2B%2C%3B%3D") + } + + func testThatIllegalASCIICharactersArePercentEscaped() { + // Given + let encoder = URLEncodedFormEncoder() + let parameters = ["illegal": " \"#%<>[]\\^`{}|"] + + // When + let result = Result { try encoder.encode(parameters) } + + // Then + XCTAssertEqual(result.value, "illegal=%20%22%23%25%3C%3E%5B%5D%5C%5E%60%7B%7D%7C") + } + + func testThatAmpersandsInKeysAndValuesArePercentEscaped() { + // Given + let encoder = URLEncodedFormEncoder() + let parameters = ["foo&bar": "baz&qux", "foobar": "bazqux"] + + // When + let result = Result { try encoder.encode(parameters) } + + // Then + XCTAssertEqual(result.value, "foobar=bazqux&foo%26bar=baz%26qux") + } + + func testThatQuestionMarksInKeysAndValuesAreNotPercentEscaped() { + // Given + let encoder = URLEncodedFormEncoder() + let parameters = ["?foo?": "?bar?"] + + // When + let result = Result { try encoder.encode(parameters) } + + // Then + XCTAssertEqual(result.value, "?foo?=?bar?") + } + + func testThatSlashesInKeysAndValuesAreNotPercentEscaped() { + // Given + let encoder = URLEncodedFormEncoder() + let parameters = ["foo": "/bar/baz/qux"] + + // When + let result = Result { try encoder.encode(parameters) } + + // Then + XCTAssertEqual(result.value, "foo=/bar/baz/qux") + } + + func testThatSpacesInKeysAndValuesArePercentEscaped() { + // Given + let encoder = URLEncodedFormEncoder() + let parameters = [" foo ": " bar "] + + // When + let result = Result { try encoder.encode(parameters) } + + // Then + XCTAssertEqual(result.value, "%20foo%20=%20bar%20") + } + + func testThatPlusesInKeysAndValuesArePercentEscaped() { + // Given + let encoder = URLEncodedFormEncoder() + let parameters = ["+foo+": "+bar+"] + + // When + let result = Result { try encoder.encode(parameters) } + + // Then + XCTAssertEqual(result.value, "%2Bfoo%2B=%2Bbar%2B") + } + + func testThatPercentsInKeysAndValuesArePercentEscaped() { + // Given + let encoder = URLEncodedFormEncoder() + let parameters = ["percent%": "%25"] + + // When + let result = Result { try encoder.encode(parameters) } + + // Then + XCTAssertEqual(result.value, "percent%25=%2525") + } + + func testThatNonLatinCharactersArePercentEscaped() { + // Given + let encoder = URLEncodedFormEncoder() + let parameters = [ + "french": "français", + "japanese": "日本語", + "arabic": "العربية", + "emoji": "😃" + ] + + // When + let result = Result { try encoder.encode(parameters) } + + // Then + let expectedParameterValues = [ + "arabic=%D8%A7%D9%84%D8%B9%D8%B1%D8%A8%D9%8A%D8%A9", + "japanese=%E6%97%A5%E6%9C%AC%E8%AA%9E", + "french=fran%C3%A7ais", + "emoji=%F0%9F%98%83" + ].joined(separator: "&") + XCTAssertEqual(result.value, expectedParameterValues) + } + + func testStringWithThousandsOfChineseCharactersIsPercentEscaped() { + // Given + let encoder = URLEncodedFormEncoder() + let repeatedCount = 2_000 + let parameters = ["chinese": String(repeating: "一二三四五六七八九十", count: repeatedCount)] + + // When + let result = Result { try encoder.encode(parameters) } + + // Then + let escaped = String(repeating: "%E4%B8%80%E4%BA%8C%E4%B8%89%E5%9B%9B%E4%BA%94%E5%85%AD%E4%B8%83%E5%85%AB%E4%B9%9D%E5%8D%81", + count: repeatedCount) + let expected = "chinese=\(escaped)" + XCTAssertEqual(result.value, expected) + } +} + +private struct EncodableStruct: Encodable { + let one = "one" + let two = 2 + let three = true + let four = [1, 2, 3] + let five = ["a": "a"] + let six = ["a": ["b": "b"]] + let seven = NestedEncodableStruct() +} + +private struct NestedEncodableStruct: Encodable { + let a = "a" +} + +private class EncodableSuperclass: Encodable { + let one = "one" + let two = 2 + let three = true +} + +private final class EncodableSubclass: EncodableSuperclass { + let four = [1, 2, 3] + let five = ["a": "a", "b": "b"] + + private enum CodingKeys: String, CodingKey { + case four, five + } + + override func encode(to encoder: Encoder) throws { + try super.encode(to: encoder) + + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(four, forKey: .four) + try container.encode(five, forKey: .five) + } +} + +private final class ManuallyEncodableSubclass: EncodableSuperclass { + let four = [1, 2, 3] + let five = ["a": "a", "b": "b"] + + private enum CodingKeys: String, CodingKey { + case four, five + } + + override func encode(to encoder: Encoder) throws { + var keyedContainer = encoder.container(keyedBy: CodingKeys.self) + + try keyedContainer.encode(four, forKey: .four) + try keyedContainer.encode(five, forKey: .five) + + let superEncoder = keyedContainer.superEncoder() + var superContainer = superEncoder.container(keyedBy: CodingKeys.self) + try superContainer.encode(one, forKey: .four) + + let keyedSuperEncoder = keyedContainer.superEncoder(forKey: .four) + var superKeyedContainer = keyedSuperEncoder.container(keyedBy: CodingKeys.self) + try superKeyedContainer.encode(two, forKey: .five) + + var unkeyedContainer = keyedContainer.nestedUnkeyedContainer(forKey: .four) + let unkeyedSuperEncoder = unkeyedContainer.superEncoder() + var keyedUnkeyedSuperContainer = unkeyedSuperEncoder.container(keyedBy: CodingKeys.self) + try keyedUnkeyedSuperContainer.encode(one, forKey: .four) + } +} + +private struct ManuallyEncodableStruct: Encodable { + let a = ["string": "string"] + let b = [1, 2, 3] + + private enum RootKey: String, CodingKey { + case root + } + + private enum TypeKeys: String, CodingKey { + case a, b + } + + func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: RootKey.self) + + var nestedKeyedContainer = container.nestedContainer(keyedBy: TypeKeys.self, forKey: .root) + try nestedKeyedContainer.encode(a, forKey: .a) + + var nestedUnkeyedContainer = container.nestedUnkeyedContainer(forKey: .root) + try nestedUnkeyedContainer.encode(b) + + var nestedUnkeyedKeyedContainer = nestedUnkeyedContainer.nestedContainer(keyedBy: TypeKeys.self) + try nestedUnkeyedKeyedContainer.encode(a, forKey: .a) + + var nestedUnkeyedUnkeyedContainer = nestedUnkeyedContainer.nestedUnkeyedContainer() + try nestedUnkeyedUnkeyedContainer.encode(b) + } +} + +private struct FailingOptionalStruct: Encodable { + enum TestedContainer { + case keyed, unkeyed + } + + enum CodingKeys: String, CodingKey { case a } + + let testedContainer: TestedContainer + + func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + + switch testedContainer { + case .keyed: + var nested = container.nestedContainer(keyedBy: CodingKeys.self, forKey: .a) + try nested.encodeNil(forKey: .a) + case .unkeyed: + var nested = container.nestedUnkeyedContainer(forKey: .a) + try nested.encodeNil() + } + } +} diff --git a/Tests/ParameterEncodingTests.swift b/Tests/ParameterEncodingTests.swift index 8f6598767..86e2797a7 100644 --- a/Tests/ParameterEncodingTests.swift +++ b/Tests/ParameterEncodingTests.swift @@ -768,124 +768,3 @@ class JSONParameterEncodingTestCase: ParameterEncodingTestCase { } } } - -// MARK: - - -class PropertyListParameterEncodingTestCase: ParameterEncodingTestCase { - - // MARK: Properties - - let encoding = PropertyListEncoding.default - - // MARK: Tests - - func testPropertyListParameterEncodeNilParameters() { - do { - // Given, When - let urlRequest = try encoding.encode(self.urlRequest, with: nil) - - // Then - XCTAssertNil(urlRequest.url?.query) - XCTAssertNil(urlRequest.value(forHTTPHeaderField: "Content-Type")) - XCTAssertNil(urlRequest.httpBody) - } catch { - XCTFail("Test encountered unexpected error: \(error)") - } - } - - func testPropertyListParameterEncodeComplexParameters() { - do { - // Given - let parameters: [String: Any] = [ - "foo": "bar", - "baz": ["a", 1, true], - "qux": [ - "a": 1, - "b": [2, 2], - "c": [3, 3, 3] - ] - ] - - // When - let urlRequest = try encoding.encode(self.urlRequest, with: parameters) - - // Then - XCTAssertNil(urlRequest.url?.query) - XCTAssertNotNil(urlRequest.value(forHTTPHeaderField: "Content-Type")) - XCTAssertEqual(urlRequest.value(forHTTPHeaderField: "Content-Type"), "application/x-plist") - XCTAssertNotNil(urlRequest.httpBody) - - if let httpBody = urlRequest.httpBody { - do { - let plist = try PropertyListSerialization.propertyList(from: httpBody, options: [], format: nil) - - if let plist = plist as? NSObject { - XCTAssertEqual(plist, parameters as NSObject) - } else { - XCTFail("plist should be an NSObject") - } - } catch { - XCTFail("plist should not be nil") - } - } - } catch { - XCTFail("Test encountered unexpected error: \(error)") - } - } - - func testPropertyListParameterEncodeDateAndDataParameters() { - do { - // Given - let date: Date = Date() - let data: Data = "data".data(using: .utf8, allowLossyConversion: false)! - - let parameters: [String: Any] = [ - "date": date, - "data": data - ] - - // When - let urlRequest = try encoding.encode(self.urlRequest, with: parameters) - - // Then - XCTAssertNil(urlRequest.url?.query) - XCTAssertNotNil(urlRequest.value(forHTTPHeaderField: "Content-Type")) - XCTAssertEqual(urlRequest.value(forHTTPHeaderField: "Content-Type"), "application/x-plist") - XCTAssertNotNil(urlRequest.httpBody) - - if let httpBody = urlRequest.httpBody { - do { - let plist = try PropertyListSerialization.propertyList(from: httpBody, options: [], format: nil) as AnyObject - - XCTAssertTrue(plist.value(forKey: "date") is Date) - XCTAssertTrue(plist.value(forKey: "data") is Data) - } catch { - XCTFail("plist should not be nil") - } - } else { - XCTFail("HTTPBody should not be nil") - } - } catch { - XCTFail("Test encountered unexpected error: \(error)") - } - } - - func testPropertyListParameterEncodeParametersRetainsCustomContentType() { - do { - // Given - var mutableURLRequest = URLRequest(url: URL(string: "https://example.com/")!) - mutableURLRequest.setValue("application/custom-plist-type+plist", forHTTPHeaderField: "Content-Type") - - let parameters = ["foo": "bar"] - - // When - let urlRequest = try encoding.encode(mutableURLRequest, with: parameters) - - // Then - XCTAssertNil(urlRequest.url?.query) - XCTAssertEqual(urlRequest.value(forHTTPHeaderField: "Content-Type"), "application/custom-plist-type+plist") - } catch { - XCTFail("Test encountered unexpected error: \(error)") - } - } -} diff --git a/Tests/RequestTests.swift b/Tests/RequestTests.swift index 53a081431..afc466b7d 100644 --- a/Tests/RequestTests.swift +++ b/Tests/RequestTests.swift @@ -26,165 +26,17 @@ import Alamofire import Foundation import XCTest -class RequestInitializationTestCase: BaseTestCase { - func testRequestClassMethodWithMethodAndURL() { - // Given - let urlString = "https://httpbin.org/" - - // When - let request = Alamofire.request(urlString) - - // Then - XCTAssertNotNil(request.request) - XCTAssertEqual(request.request?.httpMethod, "GET") - XCTAssertEqual(request.request?.url?.absoluteString, urlString) - XCTAssertNil(request.response) - } - - func testRequestClassMethodWithMethodAndURLAndParameters() { - // Given - let urlString = "https://httpbin.org/get" - - // When - let request = Alamofire.request(urlString, parameters: ["foo": "bar"]) - - // Then - XCTAssertNotNil(request.request) - XCTAssertEqual(request.request?.httpMethod, "GET") - XCTAssertNotEqual(request.request?.url?.absoluteString, urlString) - XCTAssertEqual(request.request?.url?.query, "foo=bar") - XCTAssertNil(request.response) - } - - func testRequestClassMethodWithMethodURLParametersAndHeaders() { - // Given - let urlString = "https://httpbin.org/get" - let headers = ["Authorization": "123456"] - - // When - let request = Alamofire.request(urlString, parameters: ["foo": "bar"], headers: headers) - - // Then - XCTAssertNotNil(request.request) - XCTAssertEqual(request.request?.httpMethod, "GET") - XCTAssertNotEqual(request.request?.url?.absoluteString, urlString) - XCTAssertEqual(request.request?.url?.query, "foo=bar") - XCTAssertEqual(request.request?.value(forHTTPHeaderField: "Authorization"), "123456") - XCTAssertNil(request.response) - } -} - -// MARK: - - -class RequestSubclassRequestPropertyTestCase: BaseTestCase { - private enum AuthenticationError: Error { - case expiredAccessToken - } - - private class AuthenticationAdapter: RequestAdapter { - func adapt(_ urlRequest: URLRequest) throws -> URLRequest { - throw AuthenticationError.expiredAccessToken - } - } - - private var sessionManager: SessionManager! - - override func setUp() { - super.setUp() - - sessionManager = SessionManager() - sessionManager.startRequestsImmediately = false - - sessionManager.adapter = AuthenticationAdapter() - } - - func testDataRequestHasURLRequest() { - // Given - let urlString = "https://httpbin.org/" - - // When - let request = sessionManager.request(urlString) - - // Then - XCTAssertNotNil(request.request) - XCTAssertEqual(request.request?.httpMethod, "GET") - XCTAssertEqual(request.request?.url?.absoluteString, urlString) - XCTAssertNil(request.response) - } - - func testDownloadRequestHasURLRequest() { - // Given - let urlString = "https://httpbin.org/" - - // When - let request = sessionManager.download(urlString) - - // Then - XCTAssertNotNil(request.request) - XCTAssertEqual(request.request?.httpMethod, "GET") - XCTAssertEqual(request.request?.url?.absoluteString, urlString) - XCTAssertNil(request.response) - } - - func testUploadDataRequestHasURLRequest() { - // Given - let urlString = "https://httpbin.org/" - - // When - let request = sessionManager.upload(Data(), to: urlString) - - // Then - XCTAssertNotNil(request.request) - XCTAssertEqual(request.request?.httpMethod, "POST") - XCTAssertEqual(request.request?.url?.absoluteString, urlString) - XCTAssertNil(request.response) - } - - func testUploadFileRequestHasURLRequest() { - // Given - let urlString = "https://httpbin.org/" - let imageURL = url(forResource: "rainbow", withExtension: "jpg") - - // When - let request = sessionManager.upload(imageURL, to: urlString) - - // Then - XCTAssertNotNil(request.request) - XCTAssertEqual(request.request?.httpMethod, "POST") - XCTAssertEqual(request.request?.url?.absoluteString, urlString) - XCTAssertNil(request.response) - } - - func testUploadStreamRequestHasURLRequest() { - // Given - let urlString = "https://httpbin.org/" - let imageURL = url(forResource: "rainbow", withExtension: "jpg") - let imageStream = InputStream(url: imageURL)! - - // When - let request = sessionManager.upload(imageStream, to: urlString) - - // Then - XCTAssertNotNil(request.request) - XCTAssertEqual(request.request?.httpMethod, "POST") - XCTAssertEqual(request.request?.url?.absoluteString, urlString) - XCTAssertNil(request.response) - } -} - // MARK: - class RequestResponseTestCase: BaseTestCase { func testRequestResponse() { // Given let urlString = "https://httpbin.org/get" - let expectation = self.expectation(description: "GET request should succeed: \(urlString)") - - var response: DefaultDataResponse? + var response: DataResponse? // When - Alamofire.request(urlString, parameters: ["foo": "bar"]) + AF.request(urlString, parameters: ["foo": "bar"]) .response { resp in response = resp expectation.fulfill() @@ -201,16 +53,16 @@ class RequestResponseTestCase: BaseTestCase { func testRequestResponseWithProgress() { // Given - let randomBytes = 4 * 1024 * 1024 + let randomBytes = 1 * 1024 * 1024 let urlString = "https://httpbin.org/bytes/\(randomBytes)" let expectation = self.expectation(description: "Bytes download progress should be reported: \(urlString)") var progressValues: [Double] = [] - var response: DefaultDataResponse? + var response: DataResponse? // When - Alamofire.request(urlString) + AF.request(urlString) .downloadProgress { progress in progressValues.append(progress.fractionCompleted) } @@ -241,53 +93,6 @@ class RequestResponseTestCase: BaseTestCase { } } - func testRequestResponseWithStream() { - // Given - let randomBytes = 4 * 1024 * 1024 - let urlString = "https://httpbin.org/bytes/\(randomBytes)" - - let expectation = self.expectation(description: "Bytes download progress should be reported: \(urlString)") - - var progressValues: [Double] = [] - var accumulatedData = [Data]() - var response: DefaultDataResponse? - - // When - Alamofire.request(urlString) - .downloadProgress { progress in - progressValues.append(progress.fractionCompleted) - } - .stream { data in - accumulatedData.append(data) - } - .response { resp in - response = resp - expectation.fulfill() - } - - waitForExpectations(timeout: timeout, handler: nil) - - // Then - XCTAssertNotNil(response?.request) - XCTAssertNotNil(response?.response) - XCTAssertNil(response?.data) - XCTAssertNil(response?.error) - XCTAssertGreaterThanOrEqual(accumulatedData.count, 1) - - var previousProgress: Double = progressValues.first ?? 0.0 - - for progress in progressValues { - XCTAssertGreaterThanOrEqual(progress, previousProgress) - previousProgress = progress - } - - if let lastProgress = progressValues.last { - XCTAssertEqual(lastProgress, 1.0) - } else { - XCTFail("last item in progressValues should not be nil") - } - } - func testPOSTRequestWithUnicodeParameters() { // Given let urlString = "https://httpbin.org/post" @@ -303,7 +108,7 @@ class RequestResponseTestCase: BaseTestCase { var response: DataResponse? // When - Alamofire.request(urlString, method: .post, parameters: parameters) + AF.request(urlString, method: .post, parameters: parameters) .responseJSON { closureResponse in response = closureResponse expectation.fulfill() @@ -355,7 +160,7 @@ class RequestResponseTestCase: BaseTestCase { var response: DataResponse? // When - Alamofire.request(urlString, method: .post, parameters: parameters) + AF.request(urlString, method: .post, parameters: parameters) .responseJSON { closureResponse in response = closureResponse expectation.fulfill() @@ -377,62 +182,85 @@ class RequestResponseTestCase: BaseTestCase { XCTFail("form parameter in JSON should not be nil") } } -} - -// MARK: - -extension Request { - fileprivate func preValidate(operation: @escaping () -> Void) -> Self { - delegate.queue.addOperation { - operation() - } + // MARK: Serialization Queue - return self - } + func testThatResponseSerializationWorksWithSerializationQueue() { + // Given + let queue = DispatchQueue(label: "org.alamofire.serializationQueue") + let manager = Session(serializationQueue: queue) + let expectation = self.expectation(description: "request should complete") + var response: DataResponse? - fileprivate func postValidate(operation: @escaping () -> Void) -> Self { - delegate.queue.addOperation { - operation() + // When + manager.request("https://httpbin.org/get").responseJSON { (resp) in + response = resp + expectation.fulfill() } - return self + waitForExpectations(timeout: timeout, handler: nil) + + // Then + XCTAssertEqual(response?.result.isSuccess, true) } -} -// MARK: - + // MARK: Encodable Parameters -class RequestExtensionTestCase: BaseTestCase { - func testThatRequestExtensionHasAccessToTaskDelegateQueue() { + func testThatRequestsCanPassEncodableParametersAsJSONBodyData() { // Given - let urlString = "https://httpbin.org/get" - let expectation = self.expectation(description: "GET request should succeed: \(urlString)") + let parameters = HTTPBinParameters(property: "one") + let expect = expectation(description: "request should complete") + var receivedResponse: DataResponse? - var responses: [String] = [] + // When + AF.request("https://httpbin.org/post", method: .post, parameters: parameters, encoder: JSONParameterEncoder.default) + .responseDecodable { (response: DataResponse) in + receivedResponse = response + expect.fulfill() + } + + waitForExpectations(timeout: timeout, handler: nil) + + // Then + XCTAssertEqual(receivedResponse?.result.value?.data, "{\"property\":\"one\"}") + } + + func testThatRequestsCanPassEncodableParametersAsAURLQuery() { + // Given + let parameters = HTTPBinParameters(property: "one") + let expect = expectation(description: "request should complete") + var receivedResponse: DataResponse? // When - Alamofire.request(urlString) - .preValidate { - responses.append("preValidate") - } - .validate() - .postValidate { - responses.append("postValidate") - } - .response { _ in - responses.append("response") - expectation.fulfill() - } + AF.request("https://httpbin.org/get", method: .get, parameters: parameters) + .responseDecodable { (response: DataResponse) in + receivedResponse = response + expect.fulfill() + } waitForExpectations(timeout: timeout, handler: nil) // Then - if responses.count == 3 { - XCTAssertEqual(responses[0], "preValidate") - XCTAssertEqual(responses[1], "postValidate") - XCTAssertEqual(responses[2], "response") - } else { - XCTFail("responses count should be equal to 3") + XCTAssertEqual(receivedResponse?.result.value?.args, ["property": "one"]) + } + + func testThatRequestsCanPassEncodableParametersAsURLEncodedBodyData() { + // Given + let parameters = HTTPBinParameters(property: "one") + let expect = expectation(description: "request should complete") + var receivedResponse: DataResponse? + + // When + AF.request("https://httpbin.org/post", method: .post, parameters: parameters) + .responseDecodable { (response: DataResponse) in + receivedResponse = response + expect.fulfill() } + + waitForExpectations(timeout: timeout, handler: nil) + + // Then + XCTAssertEqual(receivedResponse?.result.value?.form, ["property": "one"]) } } @@ -442,27 +270,24 @@ class RequestDescriptionTestCase: BaseTestCase { func testRequestDescription() { // Given let urlString = "https://httpbin.org/get" - let request = Alamofire.request(urlString) - let initialRequestDescription = request.description + let manager = Session(startRequestsImmediately: false) + let request = manager.request(urlString) let expectation = self.expectation(description: "Request description should update: \(urlString)") - var finalRequestDescription: String? var response: HTTPURLResponse? // When request.response { resp in - finalRequestDescription = request.description response = resp.response expectation.fulfill() - } + }.resume() waitForExpectations(timeout: timeout, handler: nil) // Then - XCTAssertEqual(initialRequestDescription, "GET https://httpbin.org/get") - XCTAssertEqual(finalRequestDescription, "GET https://httpbin.org/get (\(response?.statusCode ?? -1))") + XCTAssertEqual(request.description, "GET https://httpbin.org/get (\(response?.statusCode ?? -1))") } } @@ -471,44 +296,48 @@ class RequestDescriptionTestCase: BaseTestCase { class RequestDebugDescriptionTestCase: BaseTestCase { // MARK: Properties - let manager: SessionManager = { - let manager = SessionManager(configuration: .default) - manager.startRequestsImmediately = false + let manager: Session = { + let manager = Session() + return manager }() - let managerWithAcceptLanguageHeader: SessionManager = { - var headers = SessionManager.default.session.configuration.httpAdditionalHeaders ?? [:] + let managerWithAcceptLanguageHeader: Session = { + var headers = HTTPHeaders.default headers["Accept-Language"] = "en-US" - let configuration = URLSessionConfiguration.default - configuration.httpAdditionalHeaders = headers + let configuration = URLSessionConfiguration.alamofireDefault + configuration.httpHeaders = headers - let manager = SessionManager(configuration: configuration) - manager.startRequestsImmediately = false + let manager = Session(configuration: configuration) return manager }() - let managerWithContentTypeHeader: SessionManager = { - var headers = SessionManager.default.session.configuration.httpAdditionalHeaders ?? [:] + let managerWithContentTypeHeader: Session = { + var headers = HTTPHeaders.default headers["Content-Type"] = "application/json" - let configuration = URLSessionConfiguration.default - configuration.httpAdditionalHeaders = headers + let configuration = URLSessionConfiguration.alamofireDefault + configuration.httpHeaders = headers - let manager = SessionManager(configuration: configuration) - manager.startRequestsImmediately = false + let manager = Session(configuration: configuration) return manager }() - let managerDisallowingCookies: SessionManager = { - let configuration = URLSessionConfiguration.default + func managerWithCookie(_ cookie: HTTPCookie) -> Session { + let configuration = URLSessionConfiguration.alamofireDefault + configuration.httpCookieStorage?.setCookie(cookie) + + return Session(configuration: configuration) + } + + let managerDisallowingCookies: Session = { + let configuration = URLSessionConfiguration.alamofireDefault configuration.httpShouldSetCookies = false - let manager = SessionManager(configuration: configuration) - manager.startRequestsImmediately = false + let manager = Session(configuration: configuration) return manager }() @@ -518,24 +347,31 @@ class RequestDebugDescriptionTestCase: BaseTestCase { func testGETRequestDebugDescription() { // Given let urlString = "https://httpbin.org/get" + let expectation = self.expectation(description: "request should complete") // When - let request = manager.request(urlString) + let request = manager.request(urlString).response { _ in expectation.fulfill() } + + waitForExpectations(timeout: timeout, handler: nil) + let components = cURLCommandComponents(for: request) // Then XCTAssertEqual(components[0..<3], ["$", "curl", "-v"]) - XCTAssertFalse(components.contains("-X")) + XCTAssertTrue(components.contains("-X")) XCTAssertEqual(components.last, "\"\(urlString)\"") } func testGETRequestWithJSONHeaderDebugDescription() { // Given let urlString = "https://httpbin.org/get" + let expectation = self.expectation(description: "request should complete") // When - let headers: [String: String] = [ "X-Custom-Header": "{\"key\": \"value\"}" ] - let request = manager.request(urlString, headers: headers) + let headers: HTTPHeaders = [ "X-Custom-Header": "{\"key\": \"value\"}" ] + let request = manager.request(urlString, headers: headers).response { _ in expectation.fulfill() } + + waitForExpectations(timeout: timeout, handler: nil) // Then XCTAssertNotNil(request.debugDescription.range(of: "-H \"X-Custom-Header: {\\\"key\\\": \\\"value\\\"}\"")) @@ -544,15 +380,19 @@ class RequestDebugDescriptionTestCase: BaseTestCase { func testGETRequestWithDuplicateHeadersDebugDescription() { // Given let urlString = "https://httpbin.org/get" + let expectation = self.expectation(description: "request should complete") // When - let headers = [ "Accept-Language": "en-GB" ] - let request = managerWithAcceptLanguageHeader.request(urlString, headers: headers) + let headers: HTTPHeaders = [ "Accept-Language": "en-GB" ] + let request = managerWithAcceptLanguageHeader.request(urlString, headers: headers).response { _ in expectation.fulfill() } + + waitForExpectations(timeout: timeout, handler: nil) + let components = cURLCommandComponents(for: request) // Then XCTAssertEqual(components[0..<3], ["$", "curl", "-v"]) - XCTAssertFalse(components.contains("-X")) + XCTAssertTrue(components.contains("-X")) XCTAssertEqual(components.last, "\"\(urlString)\"") let tokens = request.debugDescription.components(separatedBy: "Accept-Language:") @@ -564,9 +404,14 @@ class RequestDebugDescriptionTestCase: BaseTestCase { func testPOSTRequestDebugDescription() { // Given let urlString = "https://httpbin.org/post" + let expectation = self.expectation(description: "request should complete") + // When - let request = manager.request(urlString, method: .post) + let request = manager.request(urlString, method: .post).response { _ in expectation.fulfill() } + + waitForExpectations(timeout: timeout, handler: nil) + let components = cURLCommandComponents(for: request) // Then @@ -578,6 +423,7 @@ class RequestDebugDescriptionTestCase: BaseTestCase { func testPOSTRequestWithJSONParametersDebugDescription() { // Given let urlString = "https://httpbin.org/post" + let expectation = self.expectation(description: "request should complete") let parameters = [ "foo": "bar", @@ -586,7 +432,12 @@ class RequestDebugDescriptionTestCase: BaseTestCase { ] // When - let request = manager.request(urlString, method: .post, parameters: parameters, encoding: JSONEncoding.default) + let request = manager.request(urlString, method: .post, parameters: parameters, encoding: JSONEncoding.default).response { + _ in expectation.fulfill() + } + + waitForExpectations(timeout: timeout, handler: nil) + let components = cURLCommandComponents(for: request) // Then @@ -614,10 +465,15 @@ class RequestDebugDescriptionTestCase: BaseTestCase { ] let cookie = HTTPCookie(properties: properties)! - manager.session.configuration.httpCookieStorage?.setCookie(cookie) + let cookieManager = managerWithCookie(cookie) + let expectation = self.expectation(description: "request should complete") + // When - let request = manager.request(urlString, method: .post) + let request = cookieManager.request(urlString, method: .post).response { _ in expectation.fulfill() } + + waitForExpectations(timeout: timeout, handler: nil) + let components = cURLCommandComponents(for: request) // Then @@ -653,34 +509,20 @@ class RequestDebugDescriptionTestCase: BaseTestCase { func testMultipartFormDataRequestWithDuplicateHeadersDebugDescription() { // Given let urlString = "https://httpbin.org/post" - let japaneseData = "日本語".data(using: .utf8, allowLossyConversion: false)! + let japaneseData = Data("日本語".utf8) let expectation = self.expectation(description: "multipart form data encoding should succeed") - var request: Request? - var components: [String] = [] - // When - managerWithContentTypeHeader.upload( - multipartFormData: { multipartFormData in - multipartFormData.append(japaneseData, withName: "japanese") - }, - to: urlString, - encodingCompletion: { result in - switch result { - case .success(let upload, _, _): - request = upload - components = self.cURLCommandComponents(for: upload) - - expectation.fulfill() - case .failure: - expectation.fulfill() - } + let request = managerWithContentTypeHeader.upload(multipartFormData: { (data) in + data.append(japaneseData, withName: "japanese") + }, to: urlString) + .response { _ in + expectation.fulfill() } - ) waitForExpectations(timeout: timeout, handler: nil) - debugPrint(request!) + let components = cURLCommandComponents(for: request) // Then XCTAssertEqual(components[0..<3], ["$", "curl", "-v"]) @@ -696,9 +538,13 @@ class RequestDebugDescriptionTestCase: BaseTestCase { func testThatRequestWithInvalidURLDebugDescription() { // Given let urlString = "invalid_url" + let expectation = self.expectation(description: "request should complete") // When - let request = manager.request(urlString) + let request = manager.request(urlString).response { _ in expectation.fulfill() } + + waitForExpectations(timeout: timeout, handler: nil) + let debugDescription = request.debugDescription // Then diff --git a/Tests/ResponseSerializationTests.swift b/Tests/ResponseSerializationTests.swift index 9bf05628d..4a088b8d4 100644 --- a/Tests/ResponseSerializationTests.swift +++ b/Tests/ResponseSerializationTests.swift @@ -26,28 +26,21 @@ import Alamofire import Foundation import XCTest -private func httpURLResponse(forStatusCode statusCode: Int, headers: HTTPHeaders = [:]) -> HTTPURLResponse { - let url = URL(string: "https://httpbin.org/get")! - return HTTPURLResponse(url: url, statusCode: statusCode, httpVersion: "HTTP/1.1", headerFields: headers)! -} - -// MARK: - - class DataResponseSerializationTestCase: BaseTestCase { // MARK: Properties - private let error = AFError.responseSerializationFailed(reason: .inputDataNil) + private let error = AFError.responseSerializationFailed(reason: .inputDataNilOrZeroLength) - // MARK: Tests - Data Response Serializer + // MARK: DataResponseSerializer func testThatDataResponseSerializerSucceedsWhenDataIsNotNil() { // Given - let serializer = DataRequest.dataResponseSerializer() - let data = "data".data(using: .utf8)! + let serializer = DataResponseSerializer() + let data = Data("data".utf8) // When - let result = serializer.serializeResponse(nil, nil, data, nil) + let result = Result { try serializer.serialize(request: nil, response: nil, data: data, error: nil) } // Then XCTAssertTrue(result.isSuccess) @@ -57,18 +50,18 @@ class DataResponseSerializationTestCase: BaseTestCase { func testThatDataResponseSerializerFailsWhenDataIsNil() { // Given - let serializer = DataRequest.dataResponseSerializer() + let serializer = DataResponseSerializer() // When - let result = serializer.serializeResponse(nil, nil, nil, nil) + let result = Result { try serializer.serialize(request: nil, response: nil, data: nil, error: nil) } // Then XCTAssertTrue(result.isFailure) XCTAssertNil(result.value) XCTAssertNotNil(result.error) - if let error = result.error as? AFError { - XCTAssertTrue(error.isInputDataNil) + if let error = result.error?.asAFError { + XCTAssertTrue(error.isInputDataNilOrZeroLength) } else { XCTFail("error should not be nil") } @@ -76,18 +69,18 @@ class DataResponseSerializationTestCase: BaseTestCase { func testThatDataResponseSerializerFailsWhenErrorIsNotNil() { // Given - let serializer = DataRequest.dataResponseSerializer() + let serializer = DataResponseSerializer() // When - let result = serializer.serializeResponse(nil, nil, nil, error) + let result = Result { try serializer.serialize(request: nil, response: nil, data: nil, error: error) } // Then XCTAssertTrue(result.isFailure) XCTAssertNil(result.value) XCTAssertNotNil(result.error) - if let error = result.error as? AFError { - XCTAssertTrue(error.isInputDataNil) + if let error = result.error?.asAFError { + XCTAssertTrue(error.isInputDataNilOrZeroLength) } else { XCTFail("error should not be nil") } @@ -95,19 +88,19 @@ class DataResponseSerializationTestCase: BaseTestCase { func testThatDataResponseSerializerFailsWhenDataIsNilWithNonEmptyResponseStatusCode() { // Given - let serializer = DataRequest.dataResponseSerializer() - let response = httpURLResponse(forStatusCode: 200) + let serializer = DataResponseSerializer() + let response = HTTPURLResponse(statusCode: 200) // When - let result = serializer.serializeResponse(nil, response, nil, nil) + let result = Result { try serializer.serialize(request: nil, response: response, data: nil, error: nil) } // Then XCTAssertTrue(result.isFailure, "result is failure should be true") XCTAssertNil(result.value, "result value should be nil") XCTAssertNotNil(result.error, "result error should not be nil") - if let error = result.error as? AFError { - XCTAssertTrue(error.isInputDataNil) + if let error = result.error?.asAFError { + XCTAssertTrue(error.isInputDataNilOrZeroLength) } else { XCTFail("error should not be nil") } @@ -115,11 +108,11 @@ class DataResponseSerializationTestCase: BaseTestCase { func testThatDataResponseSerializerSucceedsWhenDataIsNilWithEmptyResponseStatusCode() { // Given - let serializer = DataRequest.dataResponseSerializer() - let response = httpURLResponse(forStatusCode: 204) + let serializer = DataResponseSerializer() + let response = HTTPURLResponse(statusCode: 204) // When - let result = serializer.serializeResponse(nil, response, nil, nil) + let result = Result { try serializer.serialize(request: nil, response: response, data: nil, error: nil) } // Then XCTAssertTrue(result.isSuccess) @@ -131,46 +124,52 @@ class DataResponseSerializationTestCase: BaseTestCase { } } - // MARK: Tests - String Response Serializer + // MARK: StringResponseSerializer func testThatStringResponseSerializerFailsWhenDataIsNil() { // Given - let serializer = DataRequest.stringResponseSerializer() + let serializer = DataResponseSerializer() // When - let result = serializer.serializeResponse(nil, nil, nil, nil) + let result = Result { try serializer.serialize(request: nil, response: nil, data: nil, error: nil) } // Then XCTAssertTrue(result.isFailure) XCTAssertNil(result.value) XCTAssertNotNil(result.error) - if let error = result.error as? AFError { - XCTAssertTrue(error.isInputDataNil) + if let error = result.error?.asAFError { + XCTAssertTrue(error.isInputDataNilOrZeroLength) } else { XCTFail("error should not be nil") } } - func testThatStringResponseSerializerSucceedsWhenDataIsEmpty() { + func testThatStringResponseSerializerFailsWhenDataIsEmpty() { // Given - let serializer = DataRequest.stringResponseSerializer() + let serializer = StringResponseSerializer() // When - let result = serializer.serializeResponse(nil, nil, Data(), nil) + let result = Result { try serializer.serialize(request: nil, response: nil, data: Data(), error: nil) } // Then - XCTAssertTrue(result.isSuccess) - XCTAssertNotNil(result.value) - XCTAssertNil(result.error) + XCTAssertTrue(result.isFailure) + XCTAssertNil(result.value) + XCTAssertNotNil(result.error) + + if let error = result.error?.asAFError { + XCTAssertTrue(error.isInputDataNilOrZeroLength) + } else { + XCTFail("error should not be nil") + } } func testThatStringResponseSerializerSucceedsWithUTF8DataAndNoProvidedEncoding() { - let serializer = DataRequest.stringResponseSerializer() - let data = "data".data(using: .utf8)! + let serializer = StringResponseSerializer() + let data = Data("data".utf8) // When - let result = serializer.serializeResponse(nil, nil, data, nil) + let result = Result { try serializer.serialize(request: nil, response: nil, data: data, error: nil) } // Then XCTAssertTrue(result.isSuccess) @@ -179,11 +178,11 @@ class DataResponseSerializationTestCase: BaseTestCase { } func testThatStringResponseSerializerSucceedsWithUTF8DataAndUTF8ProvidedEncoding() { - let serializer = DataRequest.stringResponseSerializer(encoding: .utf8) - let data = "data".data(using: .utf8)! + let serializer = StringResponseSerializer(encoding: .utf8) + let data = Data("data".utf8) // When - let result = serializer.serializeResponse(nil, nil, data, nil) + let result = Result { try serializer.serialize(request: nil, response: nil, data: data, error: nil) } // Then XCTAssertTrue(result.isSuccess) @@ -192,12 +191,12 @@ class DataResponseSerializationTestCase: BaseTestCase { } func testThatStringResponseSerializerSucceedsWithUTF8DataUsingResponseTextEncodingName() { - let serializer = DataRequest.stringResponseSerializer() - let data = "data".data(using: .utf8)! - let response = httpURLResponse(forStatusCode: 200, headers: ["Content-Type": "image/jpeg; charset=utf-8"]) + let serializer = StringResponseSerializer() + let data = Data("data".utf8) + let response = HTTPURLResponse(statusCode: 200, headers: ["Content-Type": "image/jpeg; charset=utf-8"]) // When - let result = serializer.serializeResponse(nil, response, data, nil) + let result = Result { try serializer.serialize(request: nil, response: response, data: data, error: nil) } // Then XCTAssertTrue(result.isSuccess) @@ -207,18 +206,18 @@ class DataResponseSerializationTestCase: BaseTestCase { func testThatStringResponseSerializerFailsWithUTF32DataAndUTF8ProvidedEncoding() { // Given - let serializer = DataRequest.stringResponseSerializer(encoding: .utf8) + let serializer = StringResponseSerializer(encoding: .utf8) let data = "random data".data(using: .utf32)! // When - let result = serializer.serializeResponse(nil, nil, data, nil) + let result = Result { try serializer.serialize(request: nil, response: nil, data: data, error: nil) } // Then XCTAssertTrue(result.isFailure) XCTAssertNil(result.value) XCTAssertNotNil(result.error) - if let error = result.error as? AFError, let failedEncoding = error.failedStringEncoding { + if let error = result.error?.asAFError, let failedEncoding = error.failedStringEncoding { XCTAssertTrue(error.isStringSerializationFailed) XCTAssertEqual(failedEncoding, .utf8) } else { @@ -228,19 +227,19 @@ class DataResponseSerializationTestCase: BaseTestCase { func testThatStringResponseSerializerFailsWithUTF32DataAndUTF8ResponseEncoding() { // Given - let serializer = DataRequest.stringResponseSerializer() + let serializer = StringResponseSerializer() let data = "random data".data(using: .utf32)! - let response = httpURLResponse(forStatusCode: 200, headers: ["Content-Type": "image/jpeg; charset=utf-8"]) + let response = HTTPURLResponse(statusCode: 200, headers: ["Content-Type": "image/jpeg; charset=utf-8"]) // When - let result = serializer.serializeResponse(nil, response, data, nil) + let result = Result { try serializer.serialize(request: nil, response: response, data: data, error: nil) } // Then XCTAssertTrue(result.isFailure) XCTAssertNil(result.value) XCTAssertNotNil(result.error) - if let error = result.error as? AFError, let failedEncoding = error.failedStringEncoding { + if let error = result.error?.asAFError, let failedEncoding = error.failedStringEncoding { XCTAssertTrue(error.isStringSerializationFailed) XCTAssertEqual(failedEncoding, .utf8) } else { @@ -250,18 +249,18 @@ class DataResponseSerializationTestCase: BaseTestCase { func testThatStringResponseSerializerFailsWhenErrorIsNotNil() { // Given - let serializer = DataRequest.stringResponseSerializer() + let serializer = StringResponseSerializer() // When - let result = serializer.serializeResponse(nil, nil, nil, error) + let result = Result { try serializer.serialize(request: nil, response: nil, data: nil, error: error) } // Then XCTAssertTrue(result.isFailure) XCTAssertNil(result.value) XCTAssertNotNil(result.error) - if let error = result.error as? AFError { - XCTAssertTrue(error.isInputDataNil) + if let error = result.error?.asAFError { + XCTAssertTrue(error.isInputDataNilOrZeroLength) } else { XCTFail("error should not be nil") } @@ -269,19 +268,19 @@ class DataResponseSerializationTestCase: BaseTestCase { func testThatStringResponseSerializerFailsWhenDataIsNilWithNonEmptyResponseStatusCode() { // Given - let serializer = DataRequest.stringResponseSerializer() - let response = httpURLResponse(forStatusCode: 200) + let serializer = StringResponseSerializer() + let response = HTTPURLResponse(statusCode: 200) // When - let result = serializer.serializeResponse(nil, response, nil, nil) + let result = Result { try serializer.serialize(request: nil, response: response, data: nil, error: nil) } // Then XCTAssertTrue(result.isFailure) XCTAssertNil(result.value) XCTAssertNotNil(result.error) - if let error = result.error as? AFError { - XCTAssertTrue(error.isInputDataNil) + if let error = result.error?.asAFError { + XCTAssertTrue(error.isInputDataNilOrZeroLength) } else { XCTFail("error should not be nil") } @@ -289,11 +288,11 @@ class DataResponseSerializationTestCase: BaseTestCase { func testThatStringResponseSerializerSucceedsWhenDataIsNilWithEmptyResponseStatusCode() { // Given - let serializer = DataRequest.stringResponseSerializer() - let response = httpURLResponse(forStatusCode: 205) + let serializer = StringResponseSerializer() + let response = HTTPURLResponse(statusCode: 205) // When - let result = serializer.serializeResponse(nil, response, nil, nil) + let result = Result { try serializer.serialize(request: nil, response: response, data: nil, error: nil) } // Then XCTAssertTrue(result.isSuccess) @@ -305,21 +304,21 @@ class DataResponseSerializationTestCase: BaseTestCase { } } - // MARK: Tests - JSON Response Serializer + // MARK: JSONResponseSerializer func testThatJSONResponseSerializerFailsWhenDataIsNil() { // Given - let serializer = DataRequest.jsonResponseSerializer() + let serializer = JSONResponseSerializer() // When - let result = serializer.serializeResponse(nil, nil, nil, nil) + let result = Result { try serializer.serialize(request: nil, response: nil, data: nil, error: nil) } // Then XCTAssertTrue(result.isFailure) XCTAssertNil(result.value) XCTAssertNotNil(result.error) - if let error = result.error as? AFError { + if let error = result.error?.asAFError { XCTAssertTrue(error.isInputDataNilOrZeroLength) } else { XCTFail("error should not be nil") @@ -328,17 +327,17 @@ class DataResponseSerializationTestCase: BaseTestCase { func testThatJSONResponseSerializerFailsWhenDataIsEmpty() { // Given - let serializer = DataRequest.jsonResponseSerializer() + let serializer = JSONResponseSerializer() // When - let result = serializer.serializeResponse(nil, nil, Data(), nil) + let result = Result { try serializer.serialize(request: nil, response: nil, data: Data(), error: nil) } // Then XCTAssertTrue(result.isFailure) XCTAssertNil(result.value) XCTAssertNotNil(result.error) - if let error = result.error as? AFError { + if let error = result.error?.asAFError { XCTAssertTrue(error.isInputDataNilOrZeroLength) } else { XCTFail("error should not be nil") @@ -347,11 +346,11 @@ class DataResponseSerializationTestCase: BaseTestCase { func testThatJSONResponseSerializerSucceedsWhenDataIsValidJSON() { // Given - let serializer = DataRequest.jsonResponseSerializer() - let data = "{\"json\": true}".data(using: .utf8)! + let serializer = JSONResponseSerializer() + let data = Data("{\"json\": true}".utf8) // When - let result = serializer.serializeResponse(nil, nil, data, nil) + let result = Result { try serializer.serialize(request: nil, response: nil, data: data, error: nil) } // Then XCTAssertTrue(result.isSuccess) @@ -361,18 +360,18 @@ class DataResponseSerializationTestCase: BaseTestCase { func testThatJSONResponseSerializerFailsWhenDataIsInvalidJSON() { // Given - let serializer = DataRequest.jsonResponseSerializer() - let data = "definitely not valid json".data(using: .utf8)! + let serializer = JSONResponseSerializer() + let data = Data("definitely not valid json".utf8) // When - let result = serializer.serializeResponse(nil, nil, data, nil) + let result = Result { try serializer.serialize(request: nil, response: nil, data: data, error: nil) } // Then XCTAssertTrue(result.isFailure) XCTAssertNil(result.value) XCTAssertNotNil(result.error) - if let error = result.error as? AFError, let underlyingError = error.underlyingError as? CocoaError { + if let error = result.error?.asAFError, let underlyingError = error.underlyingError as? CocoaError { XCTAssertTrue(error.isJSONSerializationFailed) XCTAssertEqual(underlyingError.errorCode, 3840) } else { @@ -382,18 +381,18 @@ class DataResponseSerializationTestCase: BaseTestCase { func testThatJSONResponseSerializerFailsWhenErrorIsNotNil() { // Given - let serializer = DataRequest.jsonResponseSerializer() + let serializer = JSONResponseSerializer() // When - let result = serializer.serializeResponse(nil, nil, nil, error) + let result = Result { try serializer.serialize(request: nil, response: nil, data: nil, error: error) } // Then XCTAssertTrue(result.isFailure) XCTAssertNil(result.value) XCTAssertNotNil(result.error) - if let error = result.error as? AFError { - XCTAssertTrue(error.isInputDataNil) + if let error = result.error?.asAFError { + XCTAssertTrue(error.isInputDataNilOrZeroLength) } else { XCTFail("error should not be nil") } @@ -401,18 +400,18 @@ class DataResponseSerializationTestCase: BaseTestCase { func testThatJSONResponseSerializerFailsWhenDataIsNilWithNonEmptyResponseStatusCode() { // Given - let serializer = DataRequest.jsonResponseSerializer() - let response = httpURLResponse(forStatusCode: 200) + let serializer = JSONResponseSerializer() + let response = HTTPURLResponse(statusCode: 200) // When - let result = serializer.serializeResponse(nil, response, nil, nil) + let result = Result { try serializer.serialize(request: nil, response: response, data: nil, error: nil) } // Then XCTAssertTrue(result.isFailure) XCTAssertNil(result.value) XCTAssertNotNil(result.error) - if let error = result.error as? AFError { + if let error = result.error?.asAFError { XCTAssertTrue(error.isInputDataNilOrZeroLength) } else { XCTFail("error should not be nil") @@ -421,11 +420,11 @@ class DataResponseSerializationTestCase: BaseTestCase { func testThatJSONResponseSerializerSucceedsWhenDataIsNilWithEmptyResponseStatusCode() { // Given - let serializer = DataRequest.jsonResponseSerializer() - let response = httpURLResponse(forStatusCode: 204) + let serializer = JSONResponseSerializer() + let response = HTTPURLResponse(statusCode: 204) // When - let result = serializer.serializeResponse(nil, response, nil, nil) + let result = Result { try serializer.serialize(request: nil, response: response, data: nil, error: nil) } // Then XCTAssertTrue(result.isSuccess) @@ -434,139 +433,135 @@ class DataResponseSerializationTestCase: BaseTestCase { if let json = result.value as? NSNull { XCTAssertEqual(json, NSNull()) + } else { + XCTFail("json should not be nil") } } - // MARK: Tests - Property List Response Serializer + // MARK: DecodableResponseSerializer - func testThatPropertyListResponseSerializerFailsWhenDataIsNil() { + struct DecodableValue: Codable { + let string: String + } + + func testThatJSONDecodableResponseSerializerFailsWhenDataIsNil() { // Given - let serializer = DataRequest.propertyListResponseSerializer() + let serializer = DecodableResponseSerializer() // When - let result = serializer.serializeResponse(nil, nil, nil, nil) + let result = Result { try serializer.serialize(request: nil, response: nil, data: nil, error: nil) } // Then XCTAssertTrue(result.isFailure) XCTAssertNil(result.value) XCTAssertNotNil(result.error) - if let error = result.error as? AFError { + if let error = result.error?.asAFError { XCTAssertTrue(error.isInputDataNilOrZeroLength) } else { XCTFail("error should not be nil") } } - func testThatPropertyListResponseSerializerFailsWhenDataIsEmpty() { + func testThatJSONDecodableResponseSerializerFailsWhenDataIsEmpty() { // Given - let serializer = DataRequest.propertyListResponseSerializer() + let serializer = DecodableResponseSerializer() // When - let result = serializer.serializeResponse(nil, nil, Data(), nil) + let result = Result { try serializer.serialize(request: nil, response: nil, data: Data(), error: nil) } // Then XCTAssertTrue(result.isFailure) XCTAssertNil(result.value) XCTAssertNotNil(result.error) - if let error = result.error as? AFError { + if let error = result.error?.asAFError { XCTAssertTrue(error.isInputDataNilOrZeroLength) } else { XCTFail("error should not be nil") } } - func testThatPropertyListResponseSerializerSucceedsWhenDataIsValidPropertyListData() { + func testThatJSONDecodableResponseSerializerSucceedsWhenDataIsValidJSON() { // Given - let serializer = DataRequest.propertyListResponseSerializer() - let data = NSKeyedArchiver.archivedData(withRootObject: ["foo": "bar"]) + let data = Data("{\"string\":\"string\"}".utf8) + let serializer = DecodableResponseSerializer() // When - let result = serializer.serializeResponse(nil, nil, data, nil) + let result = Result { try serializer.serialize(request: nil, response: nil, data: data, error: nil) } // Then XCTAssertTrue(result.isSuccess) XCTAssertNotNil(result.value) + XCTAssertEqual(result.value?.string, "string") XCTAssertNil(result.error) } - func testThatPropertyListResponseSerializerFailsWhenDataIsInvalidPropertyListData() { + func testThatJSONDecodableResponseSerializerFailsWhenDataIsInvalidJSON() { // Given - let serializer = DataRequest.propertyListResponseSerializer() - let data = "definitely not valid plist data".data(using: .utf8)! + let serializer = DecodableResponseSerializer() + let data = Data("definitely not valid json".utf8) // When - let result = serializer.serializeResponse(nil, nil, data, nil) + let result = Result { try serializer.serialize(request: nil, response: nil, data: data, error: nil) } // Then XCTAssertTrue(result.isFailure) XCTAssertNil(result.value) XCTAssertNotNil(result.error) - - if let error = result.error as? AFError, let underlyingError = error.underlyingError as? CocoaError { - XCTAssertTrue(error.isPropertyListSerializationFailed) - XCTAssertEqual(underlyingError.errorCode, 3840) - } else { - XCTFail("error should not be nil") - } } - func testThatPropertyListResponseSerializerFailsWhenErrorIsNotNil() { + func testThatJSONDecodableResponseSerializerFailsWhenErrorIsNotNil() { // Given - let serializer = DataRequest.propertyListResponseSerializer() + let serializer = DecodableResponseSerializer() // When - let result = serializer.serializeResponse(nil, nil, nil, error) + let result = Result { try serializer.serialize(request: nil, response: nil, data: nil, error: error) } // Then XCTAssertTrue(result.isFailure) XCTAssertNil(result.value) XCTAssertNotNil(result.error) - if let error = result.error as? AFError { - XCTAssertTrue(error.isInputDataNil) + if let error = result.error?.asAFError { + XCTAssertTrue(error.isInputDataNilOrZeroLength) } else { XCTFail("error should not be nil") } } - func testThatPropertyListResponseSerializerFailsWhenDataIsNilWithNonEmptyResponseStatusCode() { + func testThatJSONDecodableResponseSerializerFailsWhenDataIsNilWithNonEmptyResponseStatusCode() { // Given - let serializer = DataRequest.propertyListResponseSerializer() - let response = httpURLResponse(forStatusCode: 200) + let serializer = DecodableResponseSerializer() + let response = HTTPURLResponse(statusCode: 200) // When - let result = serializer.serializeResponse(nil, response, nil, nil) + let result = Result { try serializer.serialize(request: nil, response: response, data: nil, error: nil) } // Then XCTAssertTrue(result.isFailure) XCTAssertNil(result.value) XCTAssertNotNil(result.error) - if let error = result.error as? AFError { + if let error = result.error?.asAFError { XCTAssertTrue(error.isInputDataNilOrZeroLength) } else { XCTFail("error should not be nil") } } - func testThatPropertyListResponseSerializerSucceedsWhenDataIsNilWithEmptyResponseStatusCode() { + func testThatJSONDecodableResponseSerializerSucceedsWhenDataIsNilWithEmptyResponseStatusCode() { // Given - let serializer = DataRequest.propertyListResponseSerializer() - let response = httpURLResponse(forStatusCode: 205) + let serializer = DecodableResponseSerializer() + let response = HTTPURLResponse(statusCode: 204) // When - let result = serializer.serializeResponse(nil, response, nil, nil) + let result = Result { try serializer.serialize(request: nil, response: response, data: nil, error: nil) } // Then XCTAssertTrue(result.isSuccess) XCTAssertNotNil(result.value) XCTAssertNil(result.error) - - if let plist = result.value as? NSNull { - XCTAssertEqual(plist, NSNull()) - } } } @@ -596,10 +591,10 @@ class DownloadResponseSerializationTestCase: BaseTestCase { func testThatDataResponseSerializerSucceedsWhenFileDataIsNotNil() { // Given - let serializer = DownloadRequest.dataResponseSerializer() + let serializer = DataResponseSerializer() // When - let result = serializer.serializeResponse(nil, nil, jsonValidDataFileURL, nil) + let result = Result { try serializer.serializeDownload(request: nil, response: nil, fileURL: jsonValidDataFileURL, error: nil) } // Then XCTAssertTrue(result.isSuccess) @@ -607,32 +602,38 @@ class DownloadResponseSerializationTestCase: BaseTestCase { XCTAssertNil(result.error) } - func testThatDataResponseSerializerSucceedsWhenFileDataIsNil() { + func testThatDataResponseSerializerFailsWhenFileDataIsEmpty() { // Given - let serializer = DownloadRequest.dataResponseSerializer() + let serializer = DataResponseSerializer() // When - let result = serializer.serializeResponse(nil, nil, jsonEmptyDataFileURL, nil) + let result = Result { try serializer.serializeDownload(request: nil, response: nil, fileURL: jsonEmptyDataFileURL, error: nil) } // Then - XCTAssertTrue(result.isSuccess) - XCTAssertNotNil(result.value) - XCTAssertNil(result.error) + XCTAssertTrue(result.isFailure) + XCTAssertNil(result.value) + XCTAssertNotNil(result.error) + + if let error = result.error?.asAFError { + XCTAssertTrue(error.isInputDataNilOrZeroLength) + } else { + XCTFail("error should not be nil") + } } func testThatDataResponseSerializerFailsWhenFileURLIsNil() { // Given - let serializer = DownloadRequest.dataResponseSerializer() + let serializer = DataResponseSerializer() // When - let result = serializer.serializeResponse(nil, nil, nil, nil) + let result = Result { try serializer.serializeDownload(request: nil, response: nil, fileURL: nil, error: nil) } // Then XCTAssertTrue(result.isFailure) XCTAssertNil(result.value) XCTAssertNotNil(result.error) - if let error = result.error as? AFError { + if let error = result.error?.asAFError { XCTAssertTrue(error.isInputFileNil) } else { XCTFail("error should not be nil") @@ -641,17 +642,17 @@ class DownloadResponseSerializationTestCase: BaseTestCase { func testThatDataResponseSerializerFailsWhenFileURLIsInvalid() { // Given - let serializer = DownloadRequest.dataResponseSerializer() + let serializer = DataResponseSerializer() // When - let result = serializer.serializeResponse(nil, nil, invalidFileURL, nil) + let result = Result { try serializer.serializeDownload(request: nil, response: nil, fileURL: invalidFileURL, error: nil) } // Then XCTAssertTrue(result.isFailure) XCTAssertNil(result.value) XCTAssertNotNil(result.error) - if let error = result.error as? AFError { + if let error = result.error?.asAFError { XCTAssertTrue(error.isInputFileReadFailed) } else { XCTFail("error should not be nil") @@ -660,17 +661,17 @@ class DownloadResponseSerializationTestCase: BaseTestCase { func testThatDataResponseSerializerFailsWhenErrorIsNotNil() { // Given - let serializer = DownloadRequest.dataResponseSerializer() + let serializer = DataResponseSerializer() // When - let result = serializer.serializeResponse(nil, nil, nil, error) + let result = Result { try serializer.serializeDownload(request: nil, response: nil, fileURL: nil, error: error) } // Then XCTAssertTrue(result.isFailure) XCTAssertNil(result.value) XCTAssertNotNil(result.error) - if let error = result.error as? AFError { + if let error = result.error?.asAFError { XCTAssertTrue(error.isInputFileNil) } else { XCTFail("error should not be nil") @@ -679,18 +680,18 @@ class DownloadResponseSerializationTestCase: BaseTestCase { func testThatDataResponseSerializerFailsWhenFileURLIsNilWithNonEmptyResponseStatusCode() { // Given - let serializer = DownloadRequest.dataResponseSerializer() - let response = httpURLResponse(forStatusCode: 200) + let serializer = DataResponseSerializer() + let response = HTTPURLResponse(statusCode: 200) // When - let result = serializer.serializeResponse(nil, response, nil, nil) + let result = Result { try serializer.serializeDownload(request: nil, response: response, fileURL: nil, error: nil) } // Then XCTAssertTrue(result.isFailure) XCTAssertNil(result.value) XCTAssertNotNil(result.error) - if let error = result.error as? AFError { + if let error = result.error?.asAFError { XCTAssertTrue(error.isInputFileNil) } else { XCTFail("error should not be nil") @@ -699,11 +700,11 @@ class DownloadResponseSerializationTestCase: BaseTestCase { func testThatDataResponseSerializerSucceedsWhenDataIsNilWithEmptyResponseStatusCode() { // Given - let serializer = DataRequest.dataResponseSerializer() - let response = httpURLResponse(forStatusCode: 205) + let serializer = DataResponseSerializer() + let response = HTTPURLResponse(statusCode: 205) // When - let result = serializer.serializeResponse(nil, response, nil, nil) + let result = Result { try serializer.serializeDownload(request: nil, response: response, fileURL: jsonEmptyDataFileURL, error: nil) } // Then XCTAssertTrue(result.isSuccess) @@ -712,6 +713,8 @@ class DownloadResponseSerializationTestCase: BaseTestCase { if let data = result.value { XCTAssertEqual(data.count, 0) + } else { + XCTFail("data should not be nil") } } @@ -719,17 +722,17 @@ class DownloadResponseSerializationTestCase: BaseTestCase { func testThatStringResponseSerializerFailsWhenFileURLIsNil() { // Given - let serializer = DownloadRequest.stringResponseSerializer() + let serializer = StringResponseSerializer() // When - let result = serializer.serializeResponse(nil, nil, nil, nil) + let result = Result { try serializer.serializeDownload(request: nil, response: nil, fileURL: nil, error: nil) } // Then XCTAssertTrue(result.isFailure) XCTAssertNil(result.value) XCTAssertNotNil(result.error) - if let error = result.error as? AFError { + if let error = result.error?.asAFError { XCTAssertTrue(error.isInputFileNil) } else { XCTFail("error should not be nil") @@ -739,41 +742,48 @@ class DownloadResponseSerializationTestCase: BaseTestCase { func testThatStringResponseSerializerFailsWhenFileURLIsInvalid() { // Given - let serializer = DownloadRequest.stringResponseSerializer() + let serializer = StringResponseSerializer() // When - let result = serializer.serializeResponse(nil, nil, invalidFileURL, nil) + let result = Result { try serializer.serializeDownload(request: nil, response: nil, fileURL: invalidFileURL, error: nil) } // Then XCTAssertEqual(result.isSuccess, false) XCTAssertNil(result.value) XCTAssertNotNil(result.error) - if let error = result.error as? AFError { + if let error = result.error?.asAFError { XCTAssertTrue(error.isInputFileReadFailed) } else { XCTFail("error should not be nil") } } - func testThatStringResponseSerializerSucceedsWhenFileDataIsEmpty() { + func testThatStringResponseSerializerFailsWhenFileDataIsEmpty() { // Given - let serializer = DownloadRequest.stringResponseSerializer() + let serializer = StringResponseSerializer() // When - let result = serializer.serializeResponse(nil, nil, stringEmptyDataFileURL, nil) + let result = Result { try serializer.serializeDownload(request: nil, response: nil, fileURL: stringEmptyDataFileURL, error: nil) } // Then - XCTAssertTrue(result.isSuccess) - XCTAssertNotNil(result.value) - XCTAssertNil(result.error) + XCTAssertTrue(result.isFailure) + XCTAssertNil(result.value) + XCTAssertNotNil(result.error) + + if let error = result.error?.asAFError { + XCTAssertTrue(error.isInputDataNilOrZeroLength) + } else { + XCTFail("error should not be nil") + } } func testThatStringResponseSerializerSucceedsWithUTF8DataAndNoProvidedEncoding() { - let serializer = DownloadRequest.stringResponseSerializer() + // Given + let serializer = StringResponseSerializer() // When - let result = serializer.serializeResponse(nil, nil, stringUTF8DataFileURL, nil) + let result = Result { try serializer.serializeDownload(request: nil, response: nil, fileURL: stringUTF8DataFileURL, error: nil) } // Then XCTAssertTrue(result.isSuccess) @@ -782,10 +792,11 @@ class DownloadResponseSerializationTestCase: BaseTestCase { } func testThatStringResponseSerializerSucceedsWithUTF8DataAndUTF8ProvidedEncoding() { - let serializer = DownloadRequest.stringResponseSerializer(encoding: .utf8) + // Given + let serializer = StringResponseSerializer(encoding: .utf8) // When - let result = serializer.serializeResponse(nil, nil, stringUTF8DataFileURL, nil) + let result = Result { try serializer.serializeDownload(request: nil, response: nil, fileURL: stringUTF8DataFileURL, error: nil) } // Then XCTAssertTrue(result.isSuccess) @@ -794,11 +805,12 @@ class DownloadResponseSerializationTestCase: BaseTestCase { } func testThatStringResponseSerializerSucceedsWithUTF8DataUsingResponseTextEncodingName() { - let serializer = DownloadRequest.stringResponseSerializer() - let response = httpURLResponse(forStatusCode: 200, headers: ["Content-Type": "image/jpeg; charset=utf-8"]) + // Given + let serializer = StringResponseSerializer() + let response = HTTPURLResponse(statusCode: 200, headers: ["Content-Type": "image/jpeg; charset=utf-8"]) // When - let result = serializer.serializeResponse(nil, response, stringUTF8DataFileURL, nil) + let result = Result { try serializer.serializeDownload(request: nil, response: response, fileURL: stringUTF8DataFileURL, error: nil) } // Then XCTAssertTrue(result.isSuccess) @@ -808,17 +820,17 @@ class DownloadResponseSerializationTestCase: BaseTestCase { func testThatStringResponseSerializerFailsWithUTF32DataAndUTF8ProvidedEncoding() { // Given - let serializer = DownloadRequest.stringResponseSerializer(encoding: .utf8) + let serializer = StringResponseSerializer(encoding: .utf8) // When - let result = serializer.serializeResponse(nil, nil, stringUTF32DataFileURL, nil) + let result = Result { try serializer.serializeDownload(request: nil, response: nil, fileURL: stringUTF32DataFileURL, error: nil) } // Then XCTAssertTrue(result.isFailure) XCTAssertNil(result.value) XCTAssertNotNil(result.error) - if let error = result.error as? AFError, let failedEncoding = error.failedStringEncoding { + if let error = result.error?.asAFError, let failedEncoding = error.failedStringEncoding { XCTAssertTrue(error.isStringSerializationFailed) XCTAssertEqual(failedEncoding, .utf8) } else { @@ -828,18 +840,18 @@ class DownloadResponseSerializationTestCase: BaseTestCase { func testThatStringResponseSerializerFailsWithUTF32DataAndUTF8ResponseEncoding() { // Given - let serializer = DownloadRequest.stringResponseSerializer() - let response = httpURLResponse(forStatusCode: 200, headers: ["Content-Type": "image/jpeg; charset=utf-8"]) + let serializer = StringResponseSerializer() + let response = HTTPURLResponse(statusCode: 200, headers: ["Content-Type": "image/jpeg; charset=utf-8"]) // When - let result = serializer.serializeResponse(nil, response, stringUTF32DataFileURL, nil) + let result = Result { try serializer.serializeDownload(request: nil, response: response, fileURL: stringUTF32DataFileURL, error: nil) } // Then XCTAssertTrue(result.isFailure) XCTAssertNil(result.value) XCTAssertNotNil(result.error) - if let error = result.error as? AFError, let failedEncoding = error.failedStringEncoding { + if let error = result.error?.asAFError, let failedEncoding = error.failedStringEncoding { XCTAssertTrue(error.isStringSerializationFailed) XCTAssertEqual(failedEncoding, .utf8) } else { @@ -849,17 +861,17 @@ class DownloadResponseSerializationTestCase: BaseTestCase { func testThatStringResponseSerializerFailsWhenErrorIsNotNil() { // Given - let serializer = DownloadRequest.stringResponseSerializer() + let serializer = StringResponseSerializer() // When - let result = serializer.serializeResponse(nil, nil, nil, error) + let result = Result { try serializer.serializeDownload(request: nil, response: nil, fileURL: nil, error: error) } // Then XCTAssertTrue(result.isFailure) XCTAssertNil(result.value) XCTAssertNotNil(result.error) - if let error = result.error as? AFError { + if let error = result.error?.asAFError { XCTAssertTrue(error.isInputFileNil) } else { XCTFail("error should not be nil") @@ -868,19 +880,19 @@ class DownloadResponseSerializationTestCase: BaseTestCase { func testThatStringResponseSerializerFailsWhenDataIsNilWithNonEmptyResponseStatusCode() { // Given - let serializer = DataRequest.stringResponseSerializer() - let response = httpURLResponse(forStatusCode: 200) + let serializer = StringResponseSerializer() + let response = HTTPURLResponse(statusCode: 200) // When - let result = serializer.serializeResponse(nil, response, nil, nil) + let result = Result { try serializer.serializeDownload(request: nil, response: response, fileURL: nil, error: nil) } // Then XCTAssertTrue(result.isFailure) XCTAssertNil(result.value) XCTAssertNotNil(result.error) - if let error = result.error as? AFError { - XCTAssertTrue(error.isInputDataNil) + if let error = result.error?.asAFError { + XCTAssertTrue(error.isInputFileNil) } else { XCTFail("error should not be nil") } @@ -888,11 +900,11 @@ class DownloadResponseSerializationTestCase: BaseTestCase { func testThatStringResponseSerializerSucceedsWhenDataIsNilWithEmptyResponseStatusCode() { // Given - let serializer = DataRequest.stringResponseSerializer() - let response = httpURLResponse(forStatusCode: 204) + let serializer = StringResponseSerializer() + let response = HTTPURLResponse(statusCode: 204) // When - let result = serializer.serializeResponse(nil, response, nil, nil) + let result = Result { try serializer.serializeDownload(request: nil, response: response, fileURL: stringEmptyDataFileURL, error: nil) } // Then XCTAssertTrue(result.isSuccess) @@ -908,17 +920,17 @@ class DownloadResponseSerializationTestCase: BaseTestCase { func testThatJSONResponseSerializerFailsWhenFileURLIsNil() { // Given - let serializer = DownloadRequest.jsonResponseSerializer() + let serializer = JSONResponseSerializer() // When - let result = serializer.serializeResponse(nil, nil, nil, nil) + let result = Result { try serializer.serializeDownload(request: nil, response: nil, fileURL: nil, error: nil) } // Then XCTAssertTrue(result.isFailure) XCTAssertNil(result.value) XCTAssertNotNil(result.error) - if let error = result.error as? AFError { + if let error = result.error?.asAFError { XCTAssertTrue(error.isInputFileNil) } else { XCTFail("error should not be nil") @@ -927,17 +939,17 @@ class DownloadResponseSerializationTestCase: BaseTestCase { func testThatJSONResponseSerializerFailsWhenFileURLIsInvalid() { // Given - let serializer = DownloadRequest.jsonResponseSerializer() + let serializer = JSONResponseSerializer() // When - let result = serializer.serializeResponse(nil, nil, invalidFileURL, nil) + let result = Result { try serializer.serializeDownload(request: nil, response: nil, fileURL: invalidFileURL, error: nil) } // Then XCTAssertTrue(result.isFailure) XCTAssertNil(result.value) XCTAssertNotNil(result.error) - if let error = result.error as? AFError { + if let error = result.error?.asAFError { XCTAssertTrue(error.isInputFileReadFailed) } else { XCTFail("error should not be nil") @@ -946,17 +958,17 @@ class DownloadResponseSerializationTestCase: BaseTestCase { func testThatJSONResponseSerializerFailsWhenFileDataIsEmpty() { // Given - let serializer = DownloadRequest.jsonResponseSerializer() + let serializer = JSONResponseSerializer() // When - let result = serializer.serializeResponse(nil, nil, jsonEmptyDataFileURL, nil) + let result = Result { try serializer.serializeDownload(request: nil, response: nil, fileURL: jsonEmptyDataFileURL, error: nil) } // Then XCTAssertTrue(result.isFailure) XCTAssertNil(result.value) XCTAssertNotNil(result.error) - if let error = result.error as? AFError { + if let error = result.error?.asAFError { XCTAssertTrue(error.isInputDataNilOrZeroLength) } else { XCTFail("error should not be nil") @@ -965,10 +977,10 @@ class DownloadResponseSerializationTestCase: BaseTestCase { func testThatJSONResponseSerializerSucceedsWhenDataIsValidJSON() { // Given - let serializer = DownloadRequest.jsonResponseSerializer() + let serializer = JSONResponseSerializer() // When - let result = serializer.serializeResponse(nil, nil, jsonValidDataFileURL, nil) + let result = Result { try serializer.serializeDownload(request: nil, response: nil, fileURL: jsonValidDataFileURL, error: nil) } // Then XCTAssertTrue(result.isSuccess) @@ -978,17 +990,17 @@ class DownloadResponseSerializationTestCase: BaseTestCase { func testThatJSONResponseSerializerFailsWhenDataIsInvalidJSON() { // Given - let serializer = DownloadRequest.jsonResponseSerializer() + let serializer = JSONResponseSerializer() // When - let result = serializer.serializeResponse(nil, nil, jsonInvalidDataFileURL, nil) + let result = Result { try serializer.serializeDownload(request: nil, response: nil, fileURL: jsonInvalidDataFileURL, error: nil) } // Then XCTAssertTrue(result.isFailure) XCTAssertNil(result.value) XCTAssertNotNil(result.error) - if let error = result.error as? AFError, let underlyingError = error.underlyingError as? CocoaError { + if let error = result.error?.asAFError, let underlyingError = error.underlyingError as? CocoaError { XCTAssertTrue(error.isJSONSerializationFailed) XCTAssertEqual(underlyingError.errorCode, 3840) } else { @@ -998,17 +1010,17 @@ class DownloadResponseSerializationTestCase: BaseTestCase { func testThatJSONResponseSerializerFailsWhenErrorIsNotNil() { // Given - let serializer = DownloadRequest.jsonResponseSerializer() + let serializer = JSONResponseSerializer() // When - let result = serializer.serializeResponse(nil, nil, nil, error) + let result = Result { try serializer.serializeDownload(request: nil, response: nil, fileURL: nil, error: error) } // Then XCTAssertTrue(result.isFailure) XCTAssertNil(result.value) XCTAssertNotNil(result.error) - if let error = result.error as? AFError { + if let error = result.error?.asAFError { XCTAssertTrue(error.isInputFileNil) } else { XCTFail("error should not be nil") @@ -1017,19 +1029,19 @@ class DownloadResponseSerializationTestCase: BaseTestCase { func testThatJSONResponseSerializerFailsWhenDataIsNilWithNonEmptyResponseStatusCode() { // Given - let serializer = DataRequest.jsonResponseSerializer() - let response = httpURLResponse(forStatusCode: 200) + let serializer = JSONResponseSerializer() + let response = HTTPURLResponse(statusCode: 200) // When - let result = serializer.serializeResponse(nil, response, nil, nil) + let result = Result { try serializer.serializeDownload(request: nil, response: response, fileURL: nil, error: nil) } // Then XCTAssertTrue(result.isFailure) XCTAssertNil(result.value) XCTAssertNotNil(result.error) - if let error = result.error as? AFError { - XCTAssertTrue(error.isInputDataNilOrZeroLength) + if let error = result.error?.asAFError { + XCTAssertTrue(error.isInputFileNil) } else { XCTFail("error should not be nil") } @@ -1037,11 +1049,11 @@ class DownloadResponseSerializationTestCase: BaseTestCase { func testThatJSONResponseSerializerSucceedsWhenDataIsNilWithEmptyResponseStatusCode() { // Given - let serializer = DataRequest.jsonResponseSerializer() - let response = httpURLResponse(forStatusCode: 205) + let serializer = JSONResponseSerializer() + let response = HTTPURLResponse(statusCode: 205) // When - let result = serializer.serializeResponse(nil, response, nil, nil) + let result = Result { try serializer.serializeDownload(request: nil, response: response, fileURL: jsonEmptyDataFileURL, error: nil) } // Then XCTAssertTrue(result.isSuccess) @@ -1052,153 +1064,36 @@ class DownloadResponseSerializationTestCase: BaseTestCase { XCTAssertEqual(json, NSNull()) } } +} - // MARK: Tests - Property List Response Serializer - - func testThatPropertyListResponseSerializerFailsWhenFileURLIsNil() { - // Given - let serializer = DownloadRequest.propertyListResponseSerializer() - - // When - let result = serializer.serializeResponse(nil, nil, nil, nil) - - // Then - XCTAssertTrue(result.isFailure) - XCTAssertNil(result.value) - XCTAssertNotNil(result.error) - - if let error = result.error as? AFError { - XCTAssertTrue(error.isInputFileNil) - } else { - XCTFail("error should not be nil") - } - } - - func testThatPropertyListResponseSerializerFailsWhenFileURLIsInvalid() { - // Given - let serializer = DownloadRequest.propertyListResponseSerializer() - - // When - let result = serializer.serializeResponse(nil, nil, invalidFileURL, nil) - - // Then - XCTAssertTrue(result.isFailure) - XCTAssertNil(result.value) - XCTAssertNotNil(result.error) - - if let error = result.error as? AFError { - XCTAssertTrue(error.isInputFileReadFailed) - } else { - XCTFail("error should not be nil") - } - } - - func testThatPropertyListResponseSerializerFailsWhenFileDataIsEmpty() { - // Given - let serializer = DownloadRequest.propertyListResponseSerializer() - - // When - let result = serializer.serializeResponse(nil, nil, plistEmptyDataFileURL, nil) - - // Then - XCTAssertTrue(result.isFailure) - XCTAssertNil(result.value) - XCTAssertNotNil(result.error) - - if let error = result.error as? AFError { - XCTAssertTrue(error.isInputDataNilOrZeroLength) - } else { - XCTFail("error should not be nil") - } - } - - func testThatPropertyListResponseSerializerSucceedsWhenFileDataIsValidPropertyListData() { - // Given - let serializer = DownloadRequest.propertyListResponseSerializer() - - // When - let result = serializer.serializeResponse(nil, nil, plistValidDataFileURL, nil) - - // Then - XCTAssertTrue(result.isSuccess) - XCTAssertNotNil(result.value) - XCTAssertNil(result.error) - } - - func testThatPropertyListResponseSerializerFailsWhenDataIsInvalidPropertyListData() { +final class CustomResponseSerializerTestCases: BaseTestCase { + func testThatCustomResponseSerializersCanBeWrittenWithoutCompilerIssues() { // Given - let serializer = DownloadRequest.propertyListResponseSerializer() - - // When - let result = serializer.serializeResponse(nil, nil, plistInvalidDataFileURL, nil) - - // Then - XCTAssertTrue(result.isFailure) - XCTAssertNil(result.value) - XCTAssertNotNil(result.error) - - if let error = result.error as? AFError, let underlyingError = error.underlyingError as? CocoaError { - XCTAssertTrue(error.isPropertyListSerializationFailed) - XCTAssertEqual(underlyingError.errorCode, 3840) - } else { - XCTFail("error should not be nil") + final class UselessResponseSerializer: ResponseSerializer { + func serialize(request: URLRequest?, response: HTTPURLResponse?, data: Data?, error: Error?) throws -> Data? { + return data + } } - } - - func testThatPropertyListResponseSerializerFailsWhenErrorIsNotNil() { - // Given - let serializer = DownloadRequest.propertyListResponseSerializer() + let serializer = UselessResponseSerializer() + let expectation = self.expectation(description: "request should finish") + var data: Data? // When - let result = serializer.serializeResponse(nil, nil, nil, error) - - // Then - XCTAssertTrue(result.isFailure) - XCTAssertNil(result.value) - XCTAssertNotNil(result.error) - - if let error = result.error as? AFError { - XCTAssertTrue(error.isInputFileNil) - } else { - XCTFail("error should not be nil") + AF.request(URLRequest.makeHTTPBinRequest()).response(responseSerializer: serializer) { (response) in + data = response.data + expectation.fulfill() } - } - - func testThatPropertyListResponseSerializerFailsWhenDataIsNilWithNonEmptyResponseStatusCode() { - // Given - let serializer = DataRequest.propertyListResponseSerializer() - let response = httpURLResponse(forStatusCode: 200) - // When - let result = serializer.serializeResponse(nil, response, nil, nil) + waitForExpectations(timeout: timeout, handler: nil) // Then - XCTAssertTrue(result.isFailure) - XCTAssertNil(result.value) - XCTAssertNotNil(result.error) - - if let error = result.error as? AFError { - XCTAssertTrue(error.isInputDataNilOrZeroLength) - } else { - XCTFail("error should not be nil") - } + XCTAssertNotNil(data) } +} - func testThatPropertyListResponseSerializerSucceedsWhenDataIsNilWith204ResponseStatusCode() { - // Given - let serializer = DataRequest.propertyListResponseSerializer() - let response = httpURLResponse(forStatusCode: 204) - - // When - let result = serializer.serializeResponse(nil, response, nil, nil) - - // Then - XCTAssertTrue(result.isSuccess) - XCTAssertNotNil(result.value) - XCTAssertNil(result.error) - - if let plist = result.value as? NSNull { - XCTAssertEqual(plist, NSNull(), "plist should be equal to NSNull") - } +extension HTTPURLResponse { + convenience init(statusCode: Int, headers: HTTPHeaders? = nil) { + let url = URL(string: "https://httpbin.org/get")! + self.init(url: url, statusCode: statusCode, httpVersion: "HTTP/1.1", headerFields: headers?.dictionary)! } } diff --git a/Tests/ResponseTests.swift b/Tests/ResponseTests.swift index 054bbba3d..f628383ce 100644 --- a/Tests/ResponseTests.swift +++ b/Tests/ResponseTests.swift @@ -32,10 +32,10 @@ class ResponseTestCase: BaseTestCase { let urlString = "https://httpbin.org/get" let expectation = self.expectation(description: "request should succeed") - var response: DefaultDataResponse? + var response: DataResponse? // When - Alamofire.request(urlString, parameters: ["foo": "bar"]).response { resp in + AF.request(urlString, parameters: ["foo": "bar"]).response { resp in response = resp expectation.fulfill() } @@ -47,10 +47,7 @@ class ResponseTestCase: BaseTestCase { XCTAssertNotNil(response?.response) XCTAssertNotNil(response?.data) XCTAssertNil(response?.error) - - if #available(iOS 10.0, macOS 10.12, tvOS 10.0, *) { - XCTAssertNotNil(response?.metrics) - } + XCTAssertNotNil(response?.metrics) } func testThatResponseReturnsFailureResultWithOptionalDataAndError() { @@ -58,10 +55,10 @@ class ResponseTestCase: BaseTestCase { let urlString = "https://invalid-url-here.org/this/does/not/exist" let expectation = self.expectation(description: "request should fail with 404") - var response: DefaultDataResponse? + var response: DataResponse? // When - Alamofire.request(urlString, parameters: ["foo": "bar"]).response { resp in + AF.request(urlString, parameters: ["foo": "bar"]).response { resp in response = resp expectation.fulfill() } @@ -71,12 +68,9 @@ class ResponseTestCase: BaseTestCase { // Then XCTAssertNotNil(response?.request) XCTAssertNil(response?.response) - XCTAssertNotNil(response?.data) + XCTAssertNil(response?.data) XCTAssertNotNil(response?.error) - - if #available(iOS 10.0, macOS 10.12, tvOS 10.0, *) { - XCTAssertNotNil(response?.metrics) - } + XCTAssertNotNil(response?.metrics) } } @@ -91,7 +85,7 @@ class ResponseDataTestCase: BaseTestCase { var response: DataResponse? // When - Alamofire.request(urlString, parameters: ["foo": "bar"]).responseData { resp in + AF.request(urlString, parameters: ["foo": "bar"]).responseData { resp in response = resp expectation.fulfill() } @@ -104,10 +98,7 @@ class ResponseDataTestCase: BaseTestCase { XCTAssertNotNil(response?.data) XCTAssertNotNil(response?.data) XCTAssertEqual(response?.result.isSuccess, true) - - if #available(iOS 10.0, macOS 10.12, tvOS 10.0, *) { - XCTAssertNotNil(response?.metrics) - } + XCTAssertNotNil(response?.metrics) } func testThatResponseDataReturnsFailureResultWithOptionalDataAndError() { @@ -118,7 +109,7 @@ class ResponseDataTestCase: BaseTestCase { var response: DataResponse? // When - Alamofire.request(urlString, parameters: ["foo": "bar"]).responseData { resp in + AF.request(urlString, parameters: ["foo": "bar"]).responseData { resp in response = resp expectation.fulfill() } @@ -128,12 +119,9 @@ class ResponseDataTestCase: BaseTestCase { // Then XCTAssertNotNil(response?.request) XCTAssertNil(response?.response) - XCTAssertNotNil(response?.data) + XCTAssertNil(response?.data) XCTAssertEqual(response?.result.isFailure, true) - - if #available(iOS 10.0, macOS 10.12, tvOS 10.0, *) { - XCTAssertNotNil(response?.metrics) - } + XCTAssertNotNil(response?.metrics) } } @@ -148,7 +136,7 @@ class ResponseStringTestCase: BaseTestCase { var response: DataResponse? // When - Alamofire.request(urlString, parameters: ["foo": "bar"]).responseString { resp in + AF.request(urlString, parameters: ["foo": "bar"]).responseString { resp in response = resp expectation.fulfill() } @@ -161,10 +149,7 @@ class ResponseStringTestCase: BaseTestCase { XCTAssertNotNil(response?.data) XCTAssertNotNil(response?.data) XCTAssertEqual(response?.result.isSuccess, true) - - if #available(iOS 10.0, macOS 10.12, tvOS 10.0, *) { - XCTAssertNotNil(response?.metrics) - } + XCTAssertNotNil(response?.metrics) } func testThatResponseStringReturnsFailureResultWithOptionalDataAndError() { @@ -175,7 +160,7 @@ class ResponseStringTestCase: BaseTestCase { var response: DataResponse? // When - Alamofire.request(urlString, parameters: ["foo": "bar"]).responseString { resp in + AF.request(urlString, parameters: ["foo": "bar"]).responseString { resp in response = resp expectation.fulfill() } @@ -185,12 +170,9 @@ class ResponseStringTestCase: BaseTestCase { // Then XCTAssertNotNil(response?.request) XCTAssertNil(response?.response) - XCTAssertNotNil(response?.data) + XCTAssertNil(response?.data) XCTAssertEqual(response?.result.isFailure, true) - - if #available(iOS 10.0, macOS 10.12, tvOS 10.0, *) { - XCTAssertNotNil(response?.metrics) - } + XCTAssertNotNil(response?.metrics) } } @@ -205,7 +187,7 @@ class ResponseJSONTestCase: BaseTestCase { var response: DataResponse? // When - Alamofire.request(urlString, parameters: ["foo": "bar"]).responseJSON { resp in + AF.request(urlString, parameters: ["foo": "bar"]).responseJSON { resp in response = resp expectation.fulfill() } @@ -218,21 +200,18 @@ class ResponseJSONTestCase: BaseTestCase { XCTAssertNotNil(response?.data) XCTAssertNotNil(response?.data) XCTAssertEqual(response?.result.isSuccess, true) - - if #available(iOS 10.0, macOS 10.12, tvOS 10.0, *) { - XCTAssertNotNil(response?.metrics) - } + XCTAssertNotNil(response?.metrics) } func testThatResponseStringReturnsFailureResultWithOptionalDataAndError() { // Given let urlString = "https://invalid-url-here.org/this/does/not/exist" - let expectation = self.expectation(description: "request should fail with 404") + let expectation = self.expectation(description: "request should fail") var response: DataResponse? // When - Alamofire.request(urlString, parameters: ["foo": "bar"]).responseJSON { resp in + AF.request(urlString, parameters: ["foo": "bar"]).responseJSON { resp in response = resp expectation.fulfill() } @@ -242,12 +221,9 @@ class ResponseJSONTestCase: BaseTestCase { // Then XCTAssertNotNil(response?.request) XCTAssertNil(response?.response) - XCTAssertNotNil(response?.data) + XCTAssertNil(response?.data) XCTAssertEqual(response?.result.isFailure, true) - - if #available(iOS 10.0, macOS 10.12, tvOS 10.0, *) { - XCTAssertNotNil(response?.metrics) - } + XCTAssertNotNil(response?.metrics) } func testThatResponseJSONReturnsSuccessResultForGETRequest() { @@ -258,7 +234,7 @@ class ResponseJSONTestCase: BaseTestCase { var response: DataResponse? // When - Alamofire.request(urlString, parameters: ["foo": "bar"]).responseJSON { resp in + AF.request(urlString, parameters: ["foo": "bar"]).responseJSON { resp in response = resp expectation.fulfill() } @@ -271,10 +247,7 @@ class ResponseJSONTestCase: BaseTestCase { XCTAssertNotNil(response?.data) XCTAssertNotNil(response?.data) XCTAssertEqual(response?.result.isSuccess, true) - - if #available(iOS 10.0, macOS 10.12, tvOS 10.0, watchOS 3.0, *) { - XCTAssertNotNil(response?.metrics) - } + XCTAssertNotNil(response?.metrics) if let responseDictionary = response?.result.value as? [String: Any], @@ -294,7 +267,7 @@ class ResponseJSONTestCase: BaseTestCase { var response: DataResponse? // When - Alamofire.request(urlString, method: .post, parameters: ["foo": "bar"]).responseJSON { resp in + AF.request(urlString, method: .post, parameters: ["foo": "bar"]).responseJSON { resp in response = resp expectation.fulfill() } @@ -307,10 +280,7 @@ class ResponseJSONTestCase: BaseTestCase { XCTAssertNotNil(response?.data) XCTAssertNotNil(response?.data) XCTAssertEqual(response?.result.isSuccess, true) - - if #available(iOS 10.0, macOS 10.12, tvOS 10.0, *) { - XCTAssertNotNil(response?.metrics) - } + XCTAssertNotNil(response?.metrics) if let responseDictionary = response?.result.value as? [String: Any], @@ -323,6 +293,55 @@ class ResponseJSONTestCase: BaseTestCase { } } +class ResponseJSONDecodableTestCase: BaseTestCase { + func testThatResponseJSONReturnsSuccessResultWithValidJSON() { + // Given + let urlString = "https://httpbin.org/get" + let expectation = self.expectation(description: "request should succeed") + + var response: DataResponse? + + // When + AF.request(urlString, parameters: [:]).responseDecodable { (resp: DataResponse) in + response = resp + expectation.fulfill() + } + + waitForExpectations(timeout: timeout, handler: nil) + + // Then + XCTAssertNotNil(response?.request) + XCTAssertNotNil(response?.response) + XCTAssertNotNil(response?.data) + XCTAssertEqual(response?.result.isSuccess, true) + XCTAssertEqual(response?.result.value?.url, "https://httpbin.org/get") + XCTAssertNotNil(response?.metrics) + } + + func testThatResponseStringReturnsFailureResultWithOptionalDataAndError() { + // Given + let urlString = "https://invalid-url-here.org/this/does/not/exist" + let expectation = self.expectation(description: "request should fail") + + var response: DataResponse? + + // When + AF.request(urlString, parameters: [:]).responseDecodable { (resp: DataResponse) in + response = resp + expectation.fulfill() + } + + waitForExpectations(timeout: timeout, handler: nil) + + // Then + XCTAssertNotNil(response?.request) + XCTAssertNil(response?.response) + XCTAssertNil(response?.data) + XCTAssertEqual(response?.result.isFailure, true) + XCTAssertNotNil(response?.metrics) + } +} + // MARK: - class ResponseMapTestCase: BaseTestCase { @@ -334,7 +353,7 @@ class ResponseMapTestCase: BaseTestCase { var response: DataResponse? // When - Alamofire.request(urlString, parameters: ["foo": "bar"]).responseJSON { resp in + AF.request(urlString, parameters: ["foo": "bar"]).responseJSON { resp in response = resp.map { json in // json["args"]["foo"] is "bar": use this invariant to test the map function return ((json as? [String: Any])?["args"] as? [String: Any])?["foo"] as? String ?? "invalid" @@ -351,10 +370,7 @@ class ResponseMapTestCase: BaseTestCase { XCTAssertNotNil(response?.data) XCTAssertEqual(response?.result.isSuccess, true) XCTAssertEqual(response?.result.value, "bar") - - if #available(iOS 10.0, macOS 10.12, tvOS 10.0, *) { - XCTAssertNotNil(response?.metrics) - } + XCTAssertNotNil(response?.metrics) } func testThatMapPreservesFailureError() { @@ -365,7 +381,7 @@ class ResponseMapTestCase: BaseTestCase { var response: DataResponse? // When - Alamofire.request(urlString, parameters: ["foo": "bar"]).responseData { resp in + AF.request(urlString, parameters: ["foo": "bar"]).responseData { resp in response = resp.map { _ in "ignored" } expectation.fulfill() } @@ -375,12 +391,9 @@ class ResponseMapTestCase: BaseTestCase { // Then XCTAssertNotNil(response?.request) XCTAssertNil(response?.response) - XCTAssertNotNil(response?.data) + XCTAssertNil(response?.data) XCTAssertEqual(response?.result.isFailure, true) - - if #available(iOS 10.0, macOS 10.12, tvOS 10.0, *) { - XCTAssertNotNil(response?.metrics) - } + XCTAssertNotNil(response?.metrics) } } @@ -395,7 +408,7 @@ class ResponseFlatMapTestCase: BaseTestCase { var response: DataResponse? // When - Alamofire.request(urlString, parameters: ["foo": "bar"]).responseJSON { resp in + AF.request(urlString, parameters: ["foo": "bar"]).responseJSON { resp in response = resp.flatMap { json in // json["args"]["foo"] is "bar": use this invariant to test the flatMap function return ((json as? [String: Any])?["args"] as? [String: Any])?["foo"] as? String ?? "invalid" @@ -412,10 +425,7 @@ class ResponseFlatMapTestCase: BaseTestCase { XCTAssertNotNil(response?.data) XCTAssertEqual(response?.result.isSuccess, true) XCTAssertEqual(response?.result.value, "bar") - - if #available(iOS 10.0, macOS 10.12, tvOS 10.0, *) { - XCTAssertNotNil(response?.metrics) - } + XCTAssertNotNil(response?.metrics) } func testThatFlatMapCatchesTransformationError() { @@ -428,7 +438,7 @@ class ResponseFlatMapTestCase: BaseTestCase { var response: DataResponse? // When - Alamofire.request(urlString, parameters: ["foo": "bar"]).responseData { resp in + AF.request(urlString, parameters: ["foo": "bar"]).responseData { resp in response = resp.flatMap { json in throw TransformError() } @@ -450,9 +460,7 @@ class ResponseFlatMapTestCase: BaseTestCase { XCTFail("flatMap should catch the transformation error") } - if #available(iOS 10.0, macOS 10.12, tvOS 10.0, *) { - XCTAssertNotNil(response?.metrics) - } + XCTAssertNotNil(response?.metrics) } func testThatFlatMapPreservesFailureError() { @@ -463,7 +471,7 @@ class ResponseFlatMapTestCase: BaseTestCase { var response: DataResponse? // When - Alamofire.request(urlString, parameters: ["foo": "bar"]).responseData { resp in + AF.request(urlString, parameters: ["foo": "bar"]).responseData { resp in response = resp.flatMap { _ in "ignored" } expectation.fulfill() } @@ -473,12 +481,9 @@ class ResponseFlatMapTestCase: BaseTestCase { // Then XCTAssertNotNil(response?.request) XCTAssertNil(response?.response) - XCTAssertNotNil(response?.data) + XCTAssertNil(response?.data) XCTAssertEqual(response?.result.isFailure, true) - - if #available(iOS 10.0, macOS 10.12, tvOS 10.0, *) { - XCTAssertNotNil(response?.metrics) - } + XCTAssertNotNil(response?.metrics) } } @@ -505,7 +510,7 @@ class ResponseMapErrorTestCase: BaseTestCase { var response: DataResponse? // When - Alamofire.request(urlString).responseJSON { resp in + AF.request(urlString).responseJSON { resp in response = resp.mapError { error in return TestError.error(error: error) } @@ -518,13 +523,11 @@ class ResponseMapErrorTestCase: BaseTestCase { // Then XCTAssertNotNil(response?.request) XCTAssertNil(response?.response) - XCTAssertNotNil(response?.data) + XCTAssertNil(response?.data) XCTAssertEqual(response?.result.isFailure, true) guard let error = response?.error as? TestError, case .error = error else { XCTFail(); return } - if #available(iOS 10.0, macOS 10.12, tvOS 10.0, *) { - XCTAssertNotNil(response?.metrics) - } + XCTAssertNotNil(response?.metrics) } func testThatMapErrorPreservesSuccessValue() { @@ -535,7 +538,7 @@ class ResponseMapErrorTestCase: BaseTestCase { var response: DataResponse? // When - Alamofire.request(urlString).responseData { resp in + AF.request(urlString).responseData { resp in response = resp.mapError { TestError.error(error: $0) } expectation.fulfill() } @@ -547,10 +550,7 @@ class ResponseMapErrorTestCase: BaseTestCase { XCTAssertNotNil(response?.response) XCTAssertNotNil(response?.data) XCTAssertEqual(response?.result.isSuccess, true) - - if #available(iOS 10.0, macOS 10.12, tvOS 10.0, *) { - XCTAssertNotNil(response?.metrics) - } + XCTAssertNotNil(response?.metrics) } } @@ -565,7 +565,7 @@ class ResponseFlatMapErrorTestCase: BaseTestCase { var response: DataResponse? // When - Alamofire.request(urlString).responseData { resp in + AF.request(urlString).responseData { resp in response = resp.flatMapError { TestError.error(error: $0) } expectation.fulfill() } @@ -577,10 +577,7 @@ class ResponseFlatMapErrorTestCase: BaseTestCase { XCTAssertNotNil(response?.response) XCTAssertNotNil(response?.data) XCTAssertEqual(response?.result.isSuccess, true) - - if #available(iOS 10.0, macOS 10.12, tvOS 10.0, *) { - XCTAssertNotNil(response?.metrics) - } + XCTAssertNotNil(response?.metrics) } func testThatFlatMapErrorCatchesTransformationError() { @@ -591,7 +588,7 @@ class ResponseFlatMapErrorTestCase: BaseTestCase { var response: DataResponse? // When - Alamofire.request(urlString).responseData { resp in + AF.request(urlString).responseData { resp in response = resp.flatMapError { _ in try TransformationError.error.alwaysFails() } expectation.fulfill() } @@ -601,7 +598,7 @@ class ResponseFlatMapErrorTestCase: BaseTestCase { // Then XCTAssertNotNil(response?.request) XCTAssertNil(response?.response) - XCTAssertNotNil(response?.data) + XCTAssertNil(response?.data) XCTAssertEqual(response?.result.isFailure, true) if let error = response?.result.error { @@ -610,9 +607,7 @@ class ResponseFlatMapErrorTestCase: BaseTestCase { XCTFail("flatMapError should catch the transformation error") } - if #available(iOS 10.0, macOS 10.12, tvOS 10.0, *) { - XCTAssertNotNil(response?.metrics) - } + XCTAssertNotNil(response?.metrics) } func testThatFlatMapErrorTransformsError() { @@ -623,7 +618,7 @@ class ResponseFlatMapErrorTestCase: BaseTestCase { var response: DataResponse? // When - Alamofire.request(urlString).responseData { resp in + AF.request(urlString).responseData { resp in response = resp.flatMapError { TestError.error(error: $0) } expectation.fulfill() } @@ -633,12 +628,11 @@ class ResponseFlatMapErrorTestCase: BaseTestCase { // Then XCTAssertNotNil(response?.request) XCTAssertNil(response?.response) - XCTAssertNotNil(response?.data) + XCTAssertNil(response?.data) XCTAssertEqual(response?.result.isFailure, true) + guard let error = response?.error as? TestError, case .error = error else { XCTFail(); return } - if #available(iOS 10.0, macOS 10.12, tvOS 10.0, *) { - XCTAssertNotNil(response?.metrics) - } + XCTAssertNotNil(response?.metrics) } } diff --git a/Tests/ResultTests.swift b/Tests/ResultTests.swift index 7dc47cab5..16e3f4515 100644 --- a/Tests/ResultTests.swift +++ b/Tests/ResultTests.swift @@ -213,11 +213,7 @@ class ResultTestCase: BaseTestCase { let result = Result.success("success value") // When - #if swift(>=3.2) let mappedResult = result.map { $0.count } - #else - let mappedResult = result.map { $0.characters.count } - #endif // Then XCTAssertEqual(mappedResult.value, 13) @@ -229,11 +225,7 @@ class ResultTestCase: BaseTestCase { let result = Result.failure(ResultError()) // When - #if swift(>=3.2) let mappedResult = result.map { $0.count } - #else - let mappedResult = result.map { $0.characters.count } - #endif // Then if let error = mappedResult.error { @@ -250,11 +242,7 @@ class ResultTestCase: BaseTestCase { let result = Result.success("success value") // When - #if swift(>=3.2) let mappedResult = result.map { $0.count } - #else - let mappedResult = result.map { $0.characters.count } - #endif // Then XCTAssertEqual(mappedResult.value, 13) @@ -400,13 +388,7 @@ class ResultTestCase: BaseTestCase { result.withError { string = "\(type(of: $0))" } // Then - #if swift(>=4.0) XCTAssertEqual(string, "ResultError") - #elseif swift(>=3.2) - XCTAssertEqual(string, "ResultError #1") - #else - XCTAssertEqual(string, "(ResultError #1)") - #endif } func testWithErrorDoesNotExecuteWhenSuccess() { diff --git a/Tests/ServerTrustPolicyTests.swift b/Tests/ServerTrustEvaluatorTests.swift similarity index 56% rename from Tests/ServerTrustPolicyTests.swift rename to Tests/ServerTrustEvaluatorTests.swift index 382a8b3b3..3854f313b 100644 --- a/Tests/ServerTrustPolicyTests.swift +++ b/Tests/ServerTrustEvaluatorTests.swift @@ -1,5 +1,5 @@ // -// MultipartFormDataTests.swift +// ServerTrustPolicyTests.swift // // Copyright (c) 2014-2018 Alamofire Software Foundation (http://alamofire.org/) // @@ -28,28 +28,28 @@ import XCTest private struct TestCertificates { // Root Certificates - static let rootCA = TestCertificates.certificateWithFileName("alamofire-root-ca") + static let rootCA = TestCertificates.certificate(filename:"alamofire-root-ca") // Intermediate Certificates - static let intermediateCA1 = TestCertificates.certificateWithFileName("alamofire-signing-ca1") - static let intermediateCA2 = TestCertificates.certificateWithFileName("alamofire-signing-ca2") + static let intermediateCA1 = TestCertificates.certificate(filename:"alamofire-signing-ca1") + static let intermediateCA2 = TestCertificates.certificate(filename:"alamofire-signing-ca2") // Leaf Certificates - Signed by CA1 - static let leafWildcard = TestCertificates.certificateWithFileName("wildcard.alamofire.org") - static let leafMultipleDNSNames = TestCertificates.certificateWithFileName("multiple-dns-names") - static let leafSignedByCA1 = TestCertificates.certificateWithFileName("signed-by-ca1") - static let leafDNSNameAndURI = TestCertificates.certificateWithFileName("test.alamofire.org") + static let leafWildcard = TestCertificates.certificate(filename:"wildcard.alamofire.org") + static let leafMultipleDNSNames = TestCertificates.certificate(filename:"multiple-dns-names") + static let leafSignedByCA1 = TestCertificates.certificate(filename:"signed-by-ca1") + static let leafDNSNameAndURI = TestCertificates.certificate(filename:"test.alamofire.org") // Leaf Certificates - Signed by CA2 - static let leafExpired = TestCertificates.certificateWithFileName("expired") - static let leafMissingDNSNameAndURI = TestCertificates.certificateWithFileName("missing-dns-name-and-uri") - static let leafSignedByCA2 = TestCertificates.certificateWithFileName("signed-by-ca2") - static let leafValidDNSName = TestCertificates.certificateWithFileName("valid-dns-name") - static let leafValidURI = TestCertificates.certificateWithFileName("valid-uri") - - static func certificateWithFileName(_ fileName: String) -> SecCertificate { - class Locater {} - let filePath = Bundle(for: Locater.self).path(forResource: fileName, ofType: "cer")! + static let leafExpired = TestCertificates.certificate(filename:"expired") + static let leafMissingDNSNameAndURI = TestCertificates.certificate(filename:"missing-dns-name-and-uri") + static let leafSignedByCA2 = TestCertificates.certificate(filename:"signed-by-ca2") + static let leafValidDNSName = TestCertificates.certificate(filename:"valid-dns-name") + static let leafValidURI = TestCertificates.certificate(filename:"valid-uri") + + static func certificate(filename: String) -> SecCertificate { + class Locator {} + let filePath = Bundle(for: Locator.self).path(forResource: filename, ofType: "cer")! let data = try! Data(contentsOf: URL(fileURLWithPath: filePath)) let certificate = SecCertificateCreateWithData(nil, data as CFData)! @@ -59,40 +59,6 @@ private struct TestCertificates { // MARK: - -private struct TestPublicKeys { - // Root Public Keys - static let rootCA = TestPublicKeys.publicKey(for: TestCertificates.rootCA) - - // Intermediate Public Keys - static let intermediateCA1 = TestPublicKeys.publicKey(for: TestCertificates.intermediateCA1) - static let intermediateCA2 = TestPublicKeys.publicKey(for: TestCertificates.intermediateCA2) - - // Leaf Public Keys - Signed by CA1 - static let leafWildcard = TestPublicKeys.publicKey(for: TestCertificates.leafWildcard) - static let leafMultipleDNSNames = TestPublicKeys.publicKey(for: TestCertificates.leafMultipleDNSNames) - static let leafSignedByCA1 = TestPublicKeys.publicKey(for: TestCertificates.leafSignedByCA1) - static let leafDNSNameAndURI = TestPublicKeys.publicKey(for: TestCertificates.leafDNSNameAndURI) - - // Leaf Public Keys - Signed by CA2 - static let leafExpired = TestPublicKeys.publicKey(for: TestCertificates.leafExpired) - static let leafMissingDNSNameAndURI = TestPublicKeys.publicKey(for: TestCertificates.leafMissingDNSNameAndURI) - static let leafSignedByCA2 = TestPublicKeys.publicKey(for: TestCertificates.leafSignedByCA2) - static let leafValidDNSName = TestPublicKeys.publicKey(for: TestCertificates.leafValidDNSName) - static let leafValidURI = TestPublicKeys.publicKey(for: TestCertificates.leafValidURI) - - static func publicKey(for certificate: SecCertificate) -> SecKey { - let policy = SecPolicyCreateBasicX509() - var trust: SecTrust? - SecTrustCreateWithCertificates(certificate, policy, &trust) - - let publicKey = SecTrustCopyPublicKey(trust!)! - - return publicKey - } -} - -// MARK: - - private enum TestTrusts { // Leaf Trusts - Signed by CA1 case leafWildcard @@ -201,21 +167,17 @@ class ServerTrustPolicyTestCase: BaseTestCase { SecTrustSetAnchorCertificates(trust, [TestCertificates.rootCA] as CFArray) SecTrustSetAnchorCertificatesOnly(trust, true) } +} - func trustIsValid(_ trust: SecTrust) -> Bool { - var isValid = false - var result = SecTrustResultType.invalid - - let status = SecTrustEvaluate(trust, &result) - - if status == errSecSuccess { - let unspecified = SecTrustResultType.unspecified - let proceed = SecTrustResultType.proceed +// MARK: - SecTrust Extension - isValid = result == unspecified || result == proceed - } +extension SecTrust { + /// Evaluates `self` and returns `true` if the evaluation succeeds with a value of `.unspecified` or `.proceed`. + var isValid: Bool { + var result = SecTrustResultType.invalid + let status = SecTrustEvaluate(self, &result) - return isValid + return (status == errSecSuccess) ? (result == .unspecified || result == .proceed) : false } } @@ -237,7 +199,7 @@ class ServerTrustPolicyExplorationBasicX509PolicyValidationTestCase: ServerTrust SecTrustSetPolicies(trust, policies as CFTypeRef) // Then - XCTAssertTrue(trustIsValid(trust), "trust should be valid") + XCTAssertTrue(trust.isValid, "trust should be valid") } func testThatAnchoredRootCertificatePassesBasicX509ValidationWithoutRootInTrust() { @@ -250,7 +212,7 @@ class ServerTrustPolicyExplorationBasicX509PolicyValidationTestCase: ServerTrust SecTrustSetPolicies(trust, policies as CFTypeRef) // Then - XCTAssertTrue(trustIsValid(trust), "trust should be valid") + XCTAssertTrue(trust.isValid, "trust should be valid") } func testThatCertificateMissingDNSNamePassesBasicX509Validation() { @@ -263,7 +225,7 @@ class ServerTrustPolicyExplorationBasicX509PolicyValidationTestCase: ServerTrust SecTrustSetPolicies(trust, policies as CFTypeRef) // Then - XCTAssertTrue(trustIsValid(trust), "trust should be valid") + XCTAssertTrue(trust.isValid, "trust should be valid") } func testThatExpiredCertificateFailsBasicX509Validation() { @@ -276,7 +238,7 @@ class ServerTrustPolicyExplorationBasicX509PolicyValidationTestCase: ServerTrust SecTrustSetPolicies(trust, policies as CFTypeRef) // Then - XCTAssertFalse(trustIsValid(trust), "trust should not be valid") + XCTAssertFalse(trust.isValid, "trust should not be valid") } } @@ -298,7 +260,7 @@ class ServerTrustPolicyExplorationSSLPolicyValidationTestCase: ServerTrustPolicy SecTrustSetPolicies(trust, policies as CFTypeRef) // Then - XCTAssertTrue(trustIsValid(trust), "trust should be valid") + XCTAssertTrue(trust.isValid, "trust should be valid") } func testThatAnchoredRootCertificatePassesSSLValidationWithoutRootInTrust() { @@ -311,7 +273,7 @@ class ServerTrustPolicyExplorationSSLPolicyValidationTestCase: ServerTrustPolicy SecTrustSetPolicies(trust, policies as CFTypeRef) // Then - XCTAssertTrue(trustIsValid(trust), "trust should be valid") + XCTAssertTrue(trust.isValid, "trust should be valid") } func testThatCertificateMissingDNSNameFailsSSLValidation() { @@ -324,7 +286,7 @@ class ServerTrustPolicyExplorationSSLPolicyValidationTestCase: ServerTrustPolicy SecTrustSetPolicies(trust, policies as CFTypeRef) // Then - XCTAssertFalse(trustIsValid(trust), "trust should not be valid") + XCTAssertFalse(trust.isValid, "trust should not be valid") } func testThatWildcardCertificatePassesSSLValidation() { @@ -337,7 +299,7 @@ class ServerTrustPolicyExplorationSSLPolicyValidationTestCase: ServerTrustPolicy SecTrustSetPolicies(trust, policies as CFTypeRef) // Then - XCTAssertTrue(trustIsValid(trust), "trust should be valid") + XCTAssertTrue(trust.isValid, "trust should be valid") } func testThatDNSNameCertificatePassesSSLValidation() { @@ -350,7 +312,7 @@ class ServerTrustPolicyExplorationSSLPolicyValidationTestCase: ServerTrustPolicy SecTrustSetPolicies(trust, policies as CFTypeRef) // Then - XCTAssertTrue(trustIsValid(trust), "trust should be valid") + XCTAssertTrue(trust.isValid, "trust should be valid") } func testThatURICertificateFailsSSLValidation() { @@ -363,7 +325,7 @@ class ServerTrustPolicyExplorationSSLPolicyValidationTestCase: ServerTrustPolicy SecTrustSetPolicies(trust, policies as CFTypeRef) // Then - XCTAssertFalse(trustIsValid(trust), "trust should not be valid") + XCTAssertFalse(trust.isValid, "trust should not be valid") } func testThatMultipleDNSNamesCertificatePassesSSLValidationForAllEntries() { @@ -380,7 +342,7 @@ class ServerTrustPolicyExplorationSSLPolicyValidationTestCase: ServerTrustPolicy SecTrustSetPolicies(trust, policies as CFTypeRef) // Then - XCTAssertTrue(trustIsValid(trust), "trust should not be valid") + XCTAssertTrue(trust.isValid, "trust should not be valid") } func testThatPassingNilForHostParameterAllowsCertificateMissingDNSNameToPassSSLValidation() { @@ -393,7 +355,7 @@ class ServerTrustPolicyExplorationSSLPolicyValidationTestCase: ServerTrustPolicy SecTrustSetPolicies(trust, policies as CFTypeRef) // Then - XCTAssertTrue(trustIsValid(trust), "trust should not be valid") + XCTAssertTrue(trust.isValid, "trust should not be valid") } func testThatExpiredCertificateFailsSSLValidation() { @@ -406,7 +368,7 @@ class ServerTrustPolicyExplorationSSLPolicyValidationTestCase: ServerTrustPolicy SecTrustSetPolicies(trust, policies as CFTypeRef) // Then - XCTAssertFalse(trustIsValid(trust), "trust should not be valid") + XCTAssertFalse(trust.isValid, "trust should not be valid") } } @@ -420,14 +382,14 @@ class ServerTrustPolicyPerformDefaultEvaluationTestCase: ServerTrustPolicyTestCa // Given let host = "test.alamofire.org" let serverTrust = TestTrusts.leafValidDNSName.trust - let serverTrustPolicy = ServerTrustPolicy.performDefaultEvaluation(validateHost: false) + let serverTrustPolicy = DefaultTrustEvaluator(validateHost: false) // When setRootCertificateAsLoneAnchorCertificateForTrust(serverTrust) - let serverTrustIsValid = serverTrustPolicy.evaluate(serverTrust, forHost: host) + let result = Result { try serverTrustPolicy.evaluate(serverTrust, forHost: host) } // Then - XCTAssertTrue(serverTrustIsValid, "server trust should pass evaluation") + XCTAssertTrue(result.isSuccess, "server trust should pass evaluation") } func testThatNonAnchoredRootCertificateChainFailsEvaluationWithoutHostValidation() { @@ -437,55 +399,55 @@ class ServerTrustPolicyPerformDefaultEvaluationTestCase: ServerTrustPolicyTestCa TestCertificates.leafValidDNSName, TestCertificates.intermediateCA2 ]) - let serverTrustPolicy = ServerTrustPolicy.performDefaultEvaluation(validateHost: false) + let serverTrustPolicy = DefaultTrustEvaluator(validateHost: false) // When - let serverTrustIsValid = serverTrustPolicy.evaluate(serverTrust, forHost: host) + let result = Result { try serverTrustPolicy.evaluate(serverTrust, forHost: host) } // Then - XCTAssertFalse(serverTrustIsValid, "server trust should not pass evaluation") + XCTAssertFalse(result.isSuccess, "server trust should not pass evaluation") } func testThatMissingDNSNameLeafCertificatePassesEvaluationWithoutHostValidation() { // Given let host = "test.alamofire.org" let serverTrust = TestTrusts.leafMissingDNSNameAndURI.trust - let serverTrustPolicy = ServerTrustPolicy.performDefaultEvaluation(validateHost: false) + let serverTrustPolicy = DefaultTrustEvaluator(validateHost: false) // When setRootCertificateAsLoneAnchorCertificateForTrust(serverTrust) - let serverTrustIsValid = serverTrustPolicy.evaluate(serverTrust, forHost: host) + let result = Result { try serverTrustPolicy.evaluate(serverTrust, forHost: host) } // Then - XCTAssertTrue(serverTrustIsValid, "server trust should pass evaluation") + XCTAssertTrue(result.isSuccess, "server trust should pass evaluation") } func testThatExpiredCertificateChainFailsEvaluationWithoutHostValidation() { // Given let host = "test.alamofire.org" let serverTrust = TestTrusts.leafExpired.trust - let serverTrustPolicy = ServerTrustPolicy.performDefaultEvaluation(validateHost: false) + let serverTrustPolicy = DefaultTrustEvaluator(validateHost: false) // When setRootCertificateAsLoneAnchorCertificateForTrust(serverTrust) - let serverTrustIsValid = serverTrustPolicy.evaluate(serverTrust, forHost: host) + let result = Result { try serverTrustPolicy.evaluate(serverTrust, forHost: host) } // Then - XCTAssertFalse(serverTrustIsValid, "server trust should not pass evaluation") + XCTAssertFalse(result.isSuccess, "server trust should not pass evaluation") } func testThatMissingIntermediateCertificateInChainFailsEvaluationWithoutHostValidation() { // Given let host = "test.alamofire.org" let serverTrust = TestTrusts.leafValidDNSNameMissingIntermediate.trust - let serverTrustPolicy = ServerTrustPolicy.performDefaultEvaluation(validateHost: false) + let serverTrustPolicy = DefaultTrustEvaluator(validateHost: false) // When setRootCertificateAsLoneAnchorCertificateForTrust(serverTrust) - let serverTrustIsValid = serverTrustPolicy.evaluate(serverTrust, forHost: host) + let result = Result { try serverTrustPolicy.evaluate(serverTrust, forHost: host) } // Then - XCTAssertFalse(serverTrustIsValid, "server trust should not pass evaluation") + XCTAssertFalse(result.isSuccess, "server trust should not pass evaluation") } // MARK: Validate Host @@ -494,14 +456,14 @@ class ServerTrustPolicyPerformDefaultEvaluationTestCase: ServerTrustPolicyTestCa // Given let host = "test.alamofire.org" let serverTrust = TestTrusts.leafValidDNSName.trust - let serverTrustPolicy = ServerTrustPolicy.performDefaultEvaluation(validateHost: true) + let serverTrustPolicy = DefaultTrustEvaluator(validateHost: true) // When setRootCertificateAsLoneAnchorCertificateForTrust(serverTrust) - let serverTrustIsValid = serverTrustPolicy.evaluate(serverTrust, forHost: host) + let result = Result { try serverTrustPolicy.evaluate(serverTrust, forHost: host) } // Then - XCTAssertTrue(serverTrustIsValid, "server trust should pass evaluation") + XCTAssertTrue(result.isSuccess, "server trust should pass evaluation") } func testThatNonAnchoredRootCertificateChainFailsEvaluationWithHostValidation() { @@ -511,69 +473,72 @@ class ServerTrustPolicyPerformDefaultEvaluationTestCase: ServerTrustPolicyTestCa TestCertificates.leafValidDNSName, TestCertificates.intermediateCA2 ]) - let serverTrustPolicy = ServerTrustPolicy.performDefaultEvaluation(validateHost: true) + let serverTrustPolicy = DefaultTrustEvaluator(validateHost: true) // When - let serverTrustIsValid = serverTrustPolicy.evaluate(serverTrust, forHost: host) + let result = Result { try serverTrustPolicy.evaluate(serverTrust, forHost: host) } // Then - XCTAssertFalse(serverTrustIsValid, "server trust should not pass evaluation") + XCTAssertFalse(result.isSuccess, "server trust should not pass evaluation") } func testThatMissingDNSNameLeafCertificateFailsEvaluationWithHostValidation() { // Given let host = "test.alamofire.org" let serverTrust = TestTrusts.leafMissingDNSNameAndURI.trust - let serverTrustPolicy = ServerTrustPolicy.performDefaultEvaluation(validateHost: true) + let serverTrustPolicy = DefaultTrustEvaluator(validateHost: true) // When setRootCertificateAsLoneAnchorCertificateForTrust(serverTrust) - let serverTrustIsValid = serverTrustPolicy.evaluate(serverTrust, forHost: host) + let result = Result { try serverTrustPolicy.evaluate(serverTrust, forHost: host) } // Then - XCTAssertFalse(serverTrustIsValid, "server trust should not pass evaluation") + XCTAssertFalse(result.isSuccess, "server trust should not pass evaluation") } func testThatWildcardedLeafCertificateChainPassesEvaluationWithHostValidation() { // Given let host = "test.alamofire.org" let serverTrust = TestTrusts.leafWildcard.trust - let serverTrustPolicy = ServerTrustPolicy.performDefaultEvaluation(validateHost: true) + let serverTrustPolicy = DefaultTrustEvaluator(validateHost: true) // When setRootCertificateAsLoneAnchorCertificateForTrust(serverTrust) - let serverTrustIsValid = serverTrustPolicy.evaluate(serverTrust, forHost: host) + let result = Result { try serverTrustPolicy.evaluate(serverTrust, forHost: host) } // Then - XCTAssertTrue(serverTrustIsValid, "server trust should pass evaluation") + XCTAssertTrue(result.isSuccess, "server trust should pass evaluation") } func testThatExpiredCertificateChainFailsEvaluationWithHostValidation() { // Given let host = "test.alamofire.org" let serverTrust = TestTrusts.leafExpired.trust - let serverTrustPolicy = ServerTrustPolicy.performDefaultEvaluation(validateHost: true) + let serverTrustPolicy = DefaultTrustEvaluator(validateHost: true) // When setRootCertificateAsLoneAnchorCertificateForTrust(serverTrust) - let serverTrustIsValid = serverTrustPolicy.evaluate(serverTrust, forHost: host) + let result = Result { try serverTrustPolicy.evaluate(serverTrust, forHost: host) } // Then - XCTAssertFalse(serverTrustIsValid, "server trust should not pass evaluation") + XCTAssertFalse(result.isSuccess, "server trust should not pass evaluation") } func testThatMissingIntermediateCertificateInChainFailsEvaluationWithHostValidation() { // Given let host = "test.alamofire.org" let serverTrust = TestTrusts.leafValidDNSNameMissingIntermediate.trust - let serverTrustPolicy = ServerTrustPolicy.performDefaultEvaluation(validateHost: true) + let serverTrustPolicy = DefaultTrustEvaluator(validateHost: false) // When setRootCertificateAsLoneAnchorCertificateForTrust(serverTrust) - let serverTrustIsValid = serverTrustPolicy.evaluate(serverTrust, forHost: host) + let result = Result { try serverTrustPolicy.evaluate(serverTrust, forHost: host) } // Then - XCTAssertFalse(serverTrustIsValid, "server trust should not pass evaluation") + XCTAssertFalse(result.isSuccess, "server trust should not pass evaluation") + assertErrorIsAFError(result.error) { (error) in + XCTAssertTrue(error.isServerTrustEvaluationError) + } } } @@ -587,17 +552,14 @@ class ServerTrustPolicyPerformRevokedEvaluationTestCase: ServerTrustPolicyTestCa // Given let host = "test.alamofire.org" let serverTrust = TestTrusts.leafValidDNSName.trust - let serverTrustPolicy = ServerTrustPolicy.performRevokedEvaluation( - validateHost: false, - revocationFlags: kSecRevocationUseAnyAvailableMethod - ) + let serverTrustPolicy = RevocationTrustEvaluator(validateHost: false) // When setRootCertificateAsLoneAnchorCertificateForTrust(serverTrust) - let serverTrustIsValid = serverTrustPolicy.evaluate(serverTrust, forHost: host) + let result = Result { try serverTrustPolicy.evaluate(serverTrust, forHost: host) } // Then - XCTAssertTrue(serverTrustIsValid, "server trust should pass evaluation") + XCTAssertTrue(result.isSuccess, "server trust should pass evaluation") } func testThatNonAnchoredRootCertificateChainFailsEvaluationWithoutHostValidation() { @@ -607,67 +569,55 @@ class ServerTrustPolicyPerformRevokedEvaluationTestCase: ServerTrustPolicyTestCa TestCertificates.leafValidDNSName, TestCertificates.intermediateCA2 ]) - let serverTrustPolicy = ServerTrustPolicy.performRevokedEvaluation( - validateHost: false, - revocationFlags: kSecRevocationUseAnyAvailableMethod - ) + let serverTrustPolicy = RevocationTrustEvaluator(validateHost: false) // When - let serverTrustIsValid = serverTrustPolicy.evaluate(serverTrust, forHost: host) + let result = Result { try serverTrustPolicy.evaluate(serverTrust, forHost: host) } // Then - XCTAssertFalse(serverTrustIsValid, "server trust should not pass evaluation") + XCTAssertFalse(result.isSuccess, "server trust should not pass evaluation") } func testThatMissingDNSNameLeafCertificatePassesEvaluationWithoutHostValidation() { // Given let host = "test.alamofire.org" let serverTrust = TestTrusts.leafMissingDNSNameAndURI.trust - let serverTrustPolicy = ServerTrustPolicy.performRevokedEvaluation( - validateHost: false, - revocationFlags: kSecRevocationUseAnyAvailableMethod - ) + let serverTrustPolicy = RevocationTrustEvaluator(validateHost: false) // When setRootCertificateAsLoneAnchorCertificateForTrust(serverTrust) - let serverTrustIsValid = serverTrustPolicy.evaluate(serverTrust, forHost: host) + let result = Result { try serverTrustPolicy.evaluate(serverTrust, forHost: host) } // Then - XCTAssertTrue(serverTrustIsValid, "server trust should pass evaluation") + XCTAssertTrue(result.isSuccess, "server trust should pass evaluation") } func testThatExpiredCertificateChainFailsEvaluationWithoutHostValidation() { // Given let host = "test.alamofire.org" let serverTrust = TestTrusts.leafExpired.trust - let serverTrustPolicy = ServerTrustPolicy.performRevokedEvaluation( - validateHost: false, - revocationFlags: kSecRevocationUseAnyAvailableMethod - ) + let serverTrustPolicy = RevocationTrustEvaluator(validateHost: false) // When setRootCertificateAsLoneAnchorCertificateForTrust(serverTrust) - let serverTrustIsValid = serverTrustPolicy.evaluate(serverTrust, forHost: host) + let result = Result { try serverTrustPolicy.evaluate(serverTrust, forHost: host) } // Then - XCTAssertFalse(serverTrustIsValid, "server trust should not pass evaluation") + XCTAssertFalse(result.isSuccess, "server trust should not pass evaluation") } func testThatMissingIntermediateCertificateInChainFailsEvaluationWithoutHostValidation() { // Given let host = "test.alamofire.org" let serverTrust = TestTrusts.leafValidDNSNameMissingIntermediate.trust - let serverTrustPolicy = ServerTrustPolicy.performRevokedEvaluation( - validateHost: false, - revocationFlags: kSecRevocationUseAnyAvailableMethod - ) + let serverTrustPolicy = RevocationTrustEvaluator(validateHost: false) // When setRootCertificateAsLoneAnchorCertificateForTrust(serverTrust) - let serverTrustIsValid = serverTrustPolicy.evaluate(serverTrust, forHost: host) + let result = Result { try serverTrustPolicy.evaluate(serverTrust, forHost: host) } // Then - XCTAssertFalse(serverTrustIsValid, "server trust should not pass evaluation") + XCTAssertFalse(result.isSuccess, "server trust should not pass evaluation") } // MARK: Validate Host @@ -676,17 +626,14 @@ class ServerTrustPolicyPerformRevokedEvaluationTestCase: ServerTrustPolicyTestCa // Given let host = "test.alamofire.org" let serverTrust = TestTrusts.leafValidDNSName.trust - let serverTrustPolicy = ServerTrustPolicy.performRevokedEvaluation( - validateHost: true, - revocationFlags: kSecRevocationUseAnyAvailableMethod - ) + let serverTrustPolicy = RevocationTrustEvaluator() // When setRootCertificateAsLoneAnchorCertificateForTrust(serverTrust) - let serverTrustIsValid = serverTrustPolicy.evaluate(serverTrust, forHost: host) + let result = Result { try serverTrustPolicy.evaluate(serverTrust, forHost: host) } // Then - XCTAssertTrue(serverTrustIsValid, "server trust should pass evaluation") + XCTAssertTrue(result.isSuccess, "server trust should pass evaluation") } func testThatNonAnchoredRootCertificateChainFailsEvaluationWithHostValidation() { @@ -696,84 +643,69 @@ class ServerTrustPolicyPerformRevokedEvaluationTestCase: ServerTrustPolicyTestCa TestCertificates.leafValidDNSName, TestCertificates.intermediateCA2 ]) - let serverTrustPolicy = ServerTrustPolicy.performRevokedEvaluation( - validateHost: true, - revocationFlags: kSecRevocationUseAnyAvailableMethod - ) + let serverTrustPolicy = RevocationTrustEvaluator() // When - let serverTrustIsValid = serverTrustPolicy.evaluate(serverTrust, forHost: host) + let result = Result { try serverTrustPolicy.evaluate(serverTrust, forHost: host) } // Then - XCTAssertFalse(serverTrustIsValid, "server trust should not pass evaluation") + XCTAssertFalse(result.isSuccess, "server trust should not pass evaluation") } func testThatMissingDNSNameLeafCertificateFailsEvaluationWithHostValidation() { // Given let host = "test.alamofire.org" let serverTrust = TestTrusts.leafMissingDNSNameAndURI.trust - let serverTrustPolicy = ServerTrustPolicy.performRevokedEvaluation( - validateHost: true, - revocationFlags: kSecRevocationUseAnyAvailableMethod - ) + let serverTrustPolicy = RevocationTrustEvaluator() // When setRootCertificateAsLoneAnchorCertificateForTrust(serverTrust) - let serverTrustIsValid = serverTrustPolicy.evaluate(serverTrust, forHost: host) + let result = Result { try serverTrustPolicy.evaluate(serverTrust, forHost: host) } // Then - XCTAssertFalse(serverTrustIsValid, "server trust should not pass evaluation") + XCTAssertFalse(result.isSuccess, "server trust should not pass evaluation") } func testThatWildcardedLeafCertificateChainPassesEvaluationWithHostValidation() { // Given let host = "test.alamofire.org" let serverTrust = TestTrusts.leafWildcard.trust - let serverTrustPolicy = ServerTrustPolicy.performRevokedEvaluation( - validateHost: true, - revocationFlags: kSecRevocationUseAnyAvailableMethod - ) + let serverTrustPolicy = RevocationTrustEvaluator() // When setRootCertificateAsLoneAnchorCertificateForTrust(serverTrust) - let serverTrustIsValid = serverTrustPolicy.evaluate(serverTrust, forHost: host) + let result = Result { try serverTrustPolicy.evaluate(serverTrust, forHost: host) } // Then - XCTAssertTrue(serverTrustIsValid, "server trust should pass evaluation") + XCTAssertTrue(result.isSuccess, "server trust should pass evaluation") } func testThatExpiredCertificateChainFailsEvaluationWithHostValidation() { // Given let host = "test.alamofire.org" let serverTrust = TestTrusts.leafExpired.trust - let serverTrustPolicy = ServerTrustPolicy.performRevokedEvaluation( - validateHost: true, - revocationFlags: kSecRevocationUseAnyAvailableMethod - ) + let serverTrustPolicy = RevocationTrustEvaluator() // When setRootCertificateAsLoneAnchorCertificateForTrust(serverTrust) - let serverTrustIsValid = serverTrustPolicy.evaluate(serverTrust, forHost: host) + let result = Result { try serverTrustPolicy.evaluate(serverTrust, forHost: host) } // Then - XCTAssertFalse(serverTrustIsValid, "server trust should not pass evaluation") + XCTAssertFalse(result.isSuccess, "server trust should not pass evaluation") } func testThatMissingIntermediateCertificateInChainFailsEvaluationWithHostValidation() { // Given let host = "test.alamofire.org" let serverTrust = TestTrusts.leafValidDNSNameMissingIntermediate.trust - let serverTrustPolicy = ServerTrustPolicy.performRevokedEvaluation( - validateHost: true, - revocationFlags: kSecRevocationUseAnyAvailableMethod - ) + let serverTrustPolicy = RevocationTrustEvaluator() // When setRootCertificateAsLoneAnchorCertificateForTrust(serverTrust) - let serverTrustIsValid = serverTrustPolicy.evaluate(serverTrust, forHost: host) + let result = Result { try serverTrustPolicy.evaluate(serverTrust, forHost: host) } // Then - XCTAssertFalse(serverTrustIsValid, "server trust should not pass evaluation") + XCTAssertFalse(result.isSuccess, "server trust should not pass evaluation") } } @@ -788,17 +720,15 @@ class ServerTrustPolicyPinCertificatesTestCase: ServerTrustPolicyTestCase { let host = "test.alamofire.org" let serverTrust = TestTrusts.leafValidDNSName.trust let certificates = [TestCertificates.leafValidDNSName] - let serverTrustPolicy = ServerTrustPolicy.pinCertificates( - certificates: certificates, - validateCertificateChain: true, - validateHost: false - ) + let serverTrustPolicy = PinnedCertificatesTrustEvaluator(certificates: certificates, + performDefaultValidation: false, + validateHost: false) // When - let serverTrustIsValid = serverTrustPolicy.evaluate(serverTrust, forHost: host) + let result = Result { try serverTrustPolicy.evaluate(serverTrust, forHost: host) } // Then - XCTAssertTrue(serverTrustIsValid, "server trust should pass evaluation") + XCTAssertTrue(result.isSuccess, "server trust should pass evaluation") } func testThatPinnedIntermediateCertificatePassesEvaluationWithoutHostValidation() { @@ -806,17 +736,15 @@ class ServerTrustPolicyPinCertificatesTestCase: ServerTrustPolicyTestCase { let host = "test.alamofire.org" let serverTrust = TestTrusts.leafValidDNSName.trust let certificates = [TestCertificates.intermediateCA2] - let serverTrustPolicy = ServerTrustPolicy.pinCertificates( - certificates: certificates, - validateCertificateChain: true, - validateHost: false - ) + let serverTrustPolicy = PinnedCertificatesTrustEvaluator(certificates: certificates, + performDefaultValidation: false, + validateHost: false) // When - let serverTrustIsValid = serverTrustPolicy.evaluate(serverTrust, forHost: host) + let result = Result { try serverTrustPolicy.evaluate(serverTrust, forHost: host) } // Then - XCTAssertTrue(serverTrustIsValid, "server trust should pass evaluation") + XCTAssertTrue(result.isSuccess, "server trust should pass evaluation") } func testThatPinnedRootCertificatePassesEvaluationWithoutHostValidation() { @@ -824,17 +752,15 @@ class ServerTrustPolicyPinCertificatesTestCase: ServerTrustPolicyTestCase { let host = "test.alamofire.org" let serverTrust = TestTrusts.leafValidDNSName.trust let certificates = [TestCertificates.rootCA] - let serverTrustPolicy = ServerTrustPolicy.pinCertificates( - certificates: certificates, - validateCertificateChain: true, - validateHost: false - ) + let serverTrustPolicy = PinnedCertificatesTrustEvaluator(certificates: certificates, + performDefaultValidation: false, + validateHost: false) // When - let serverTrustIsValid = serverTrustPolicy.evaluate(serverTrust, forHost: host) + let result = Result { try serverTrustPolicy.evaluate(serverTrust, forHost: host) } // Then - XCTAssertTrue(serverTrustIsValid, "server trust should pass evaluation") + XCTAssertTrue(result.isSuccess, "server trust should pass evaluation") } func testThatPinningLeafCertificateNotInCertificateChainFailsEvaluationWithoutHostValidation() { @@ -842,17 +768,17 @@ class ServerTrustPolicyPinCertificatesTestCase: ServerTrustPolicyTestCase { let host = "test.alamofire.org" let serverTrust = TestTrusts.leafValidDNSName.trust let certificates = [TestCertificates.leafSignedByCA2] - let serverTrustPolicy = ServerTrustPolicy.pinCertificates( + let serverTrustPolicy = PinnedCertificatesTrustEvaluator( certificates: certificates, - validateCertificateChain: true, + performDefaultValidation: true, validateHost: false ) // When - let serverTrustIsValid = serverTrustPolicy.evaluate(serverTrust, forHost: host) + let result = Result { try serverTrustPolicy.evaluate(serverTrust, forHost: host) } // Then - XCTAssertFalse(serverTrustIsValid, "server trust should not pass evaluation") + XCTAssertFalse(result.isSuccess, "server trust should not pass evaluation") } func testThatPinningIntermediateCertificateNotInCertificateChainFailsEvaluationWithoutHostValidation() { @@ -860,17 +786,13 @@ class ServerTrustPolicyPinCertificatesTestCase: ServerTrustPolicyTestCase { let host = "test.alamofire.org" let serverTrust = TestTrusts.leafValidDNSName.trust let certificates = [TestCertificates.intermediateCA1] - let serverTrustPolicy = ServerTrustPolicy.pinCertificates( - certificates: certificates, - validateCertificateChain: true, - validateHost: false - ) + let serverTrustPolicy = PinnedCertificatesTrustEvaluator(certificates: certificates, validateHost: false) // When - let serverTrustIsValid = serverTrustPolicy.evaluate(serverTrust, forHost: host) + let result = Result { try serverTrustPolicy.evaluate(serverTrust, forHost: host) } // Then - XCTAssertFalse(serverTrustIsValid, "server trust should not pass evaluation") + XCTAssertFalse(result.isSuccess, "server trust should not pass evaluation") } func testThatPinningExpiredLeafCertificateFailsEvaluationWithoutHostValidation() { @@ -878,17 +800,13 @@ class ServerTrustPolicyPinCertificatesTestCase: ServerTrustPolicyTestCase { let host = "test.alamofire.org" let serverTrust = TestTrusts.leafExpired.trust let certificates = [TestCertificates.leafExpired] - let serverTrustPolicy = ServerTrustPolicy.pinCertificates( - certificates: certificates, - validateCertificateChain: true, - validateHost: false - ) + let serverTrustPolicy = PinnedCertificatesTrustEvaluator(certificates: certificates, validateHost: false) // When - let serverTrustIsValid = serverTrustPolicy.evaluate(serverTrust, forHost: host) + let result = Result { try serverTrustPolicy.evaluate(serverTrust, forHost: host) } // Then - XCTAssertFalse(serverTrustIsValid, "server trust should not pass evaluation") + XCTAssertFalse(result.isSuccess, "server trust should not pass evaluation") } func testThatPinningIntermediateCertificateWithExpiredLeafCertificateFailsEvaluationWithoutHostValidation() { @@ -896,73 +814,57 @@ class ServerTrustPolicyPinCertificatesTestCase: ServerTrustPolicyTestCase { let host = "test.alamofire.org" let serverTrust = TestTrusts.leafExpired.trust let certificates = [TestCertificates.intermediateCA2] - let serverTrustPolicy = ServerTrustPolicy.pinCertificates( - certificates: certificates, - validateCertificateChain: true, - validateHost: false - ) + let serverTrustPolicy = PinnedCertificatesTrustEvaluator(certificates: certificates, validateHost: false) // When - let serverTrustIsValid = serverTrustPolicy.evaluate(serverTrust, forHost: host) + let result = Result { try serverTrustPolicy.evaluate(serverTrust, forHost: host) } // Then - XCTAssertFalse(serverTrustIsValid, "server trust should not pass evaluation") + XCTAssertFalse(result.isSuccess, "server trust should not pass evaluation") } // MARK: Validate Certificate Chain and Host - func testThatPinnedLeafCertificatePassesEvaluationWithHostValidation() { + func testThatPinnedLeafCertificatePassesEvaluationWithSelfSignedSupportAndHostValidation() { // Given let host = "test.alamofire.org" let serverTrust = TestTrusts.leafValidDNSName.trust let certificates = [TestCertificates.leafValidDNSName] - let serverTrustPolicy = ServerTrustPolicy.pinCertificates( - certificates: certificates, - validateCertificateChain: true, - validateHost: true - ) + let serverTrustPolicy = PinnedCertificatesTrustEvaluator(certificates: certificates, acceptSelfSignedCertificates: true) // When - let serverTrustIsValid = serverTrustPolicy.evaluate(serverTrust, forHost: host) + let result = Result { try serverTrustPolicy.evaluate(serverTrust, forHost: host) } // Then - XCTAssertTrue(serverTrustIsValid, "server trust should pass evaluation") + XCTAssertTrue(result.isSuccess, "server trust should pass evaluation") } - func testThatPinnedIntermediateCertificatePassesEvaluationWithHostValidation() { + func testThatPinnedIntermediateCertificatePassesEvaluationWithSelfSignedSupportAndHostValidation() { // Given let host = "test.alamofire.org" let serverTrust = TestTrusts.leafValidDNSName.trust let certificates = [TestCertificates.intermediateCA2] - let serverTrustPolicy = ServerTrustPolicy.pinCertificates( - certificates: certificates, - validateCertificateChain: true, - validateHost: true - ) + let serverTrustPolicy = PinnedCertificatesTrustEvaluator(certificates: certificates, acceptSelfSignedCertificates: true) // When - let serverTrustIsValid = serverTrustPolicy.evaluate(serverTrust, forHost: host) + let result = Result { try serverTrustPolicy.evaluate(serverTrust, forHost: host) } // Then - XCTAssertTrue(serverTrustIsValid, "server trust should pass evaluation") + XCTAssertTrue(result.isSuccess, "server trust should pass evaluation") } - func testThatPinnedRootCertificatePassesEvaluationWithHostValidation() { + func testThatPinnedRootCertificatePassesEvaluationWithSelfSignedSupportAndHostValidation() { // Given let host = "test.alamofire.org" let serverTrust = TestTrusts.leafValidDNSName.trust let certificates = [TestCertificates.rootCA] - let serverTrustPolicy = ServerTrustPolicy.pinCertificates( - certificates: certificates, - validateCertificateChain: true, - validateHost: true - ) + let serverTrustPolicy = PinnedCertificatesTrustEvaluator(certificates: certificates, acceptSelfSignedCertificates: true) // When - let serverTrustIsValid = serverTrustPolicy.evaluate(serverTrust, forHost: host) + let result = Result { try serverTrustPolicy.evaluate(serverTrust, forHost: host) } // Then - XCTAssertTrue(serverTrustIsValid, "server trust should pass evaluation") + XCTAssertTrue(result.isSuccess, "server trust should pass evaluation") } func testThatPinningLeafCertificateNotInCertificateChainFailsEvaluationWithHostValidation() { @@ -970,17 +872,17 @@ class ServerTrustPolicyPinCertificatesTestCase: ServerTrustPolicyTestCase { let host = "test.alamofire.org" let serverTrust = TestTrusts.leafValidDNSName.trust let certificates = [TestCertificates.leafSignedByCA2] - let serverTrustPolicy = ServerTrustPolicy.pinCertificates( + let serverTrustPolicy = PinnedCertificatesTrustEvaluator( certificates: certificates, - validateCertificateChain: true, + performDefaultValidation: true, validateHost: true ) // When - let serverTrustIsValid = serverTrustPolicy.evaluate(serverTrust, forHost: host) + let result = Result { try serverTrustPolicy.evaluate(serverTrust, forHost: host) } // Then - XCTAssertFalse(serverTrustIsValid, "server trust should not pass evaluation") + XCTAssertFalse(result.isSuccess, "server trust should not pass evaluation") } func testThatPinningIntermediateCertificateNotInCertificateChainFailsEvaluationWithHostValidation() { @@ -988,17 +890,13 @@ class ServerTrustPolicyPinCertificatesTestCase: ServerTrustPolicyTestCase { let host = "test.alamofire.org" let serverTrust = TestTrusts.leafValidDNSName.trust let certificates = [TestCertificates.intermediateCA1] - let serverTrustPolicy = ServerTrustPolicy.pinCertificates( - certificates: certificates, - validateCertificateChain: true, - validateHost: true - ) + let serverTrustPolicy = PinnedCertificatesTrustEvaluator(certificates: certificates) // When - let serverTrustIsValid = serverTrustPolicy.evaluate(serverTrust, forHost: host) + let result = Result { try serverTrustPolicy.evaluate(serverTrust, forHost: host) } // Then - XCTAssertFalse(serverTrustIsValid, "server trust should not pass evaluation") + XCTAssertFalse(result.isSuccess, "server trust should not pass evaluation") } func testThatPinningExpiredLeafCertificateFailsEvaluationWithHostValidation() { @@ -1006,17 +904,13 @@ class ServerTrustPolicyPinCertificatesTestCase: ServerTrustPolicyTestCase { let host = "test.alamofire.org" let serverTrust = TestTrusts.leafExpired.trust let certificates = [TestCertificates.leafExpired] - let serverTrustPolicy = ServerTrustPolicy.pinCertificates( - certificates: certificates, - validateCertificateChain: true, - validateHost: true - ) + let serverTrustPolicy = PinnedCertificatesTrustEvaluator(certificates: certificates) // When - let serverTrustIsValid = serverTrustPolicy.evaluate(serverTrust, forHost: host) + let result = Result { try serverTrustPolicy.evaluate(serverTrust, forHost: host) } // Then - XCTAssertFalse(serverTrustIsValid, "server trust should not pass evaluation") + XCTAssertFalse(result.isSuccess, "server trust should not pass evaluation") } func testThatPinningIntermediateCertificateWithExpiredLeafCertificateFailsEvaluationWithHostValidation() { @@ -1024,17 +918,13 @@ class ServerTrustPolicyPinCertificatesTestCase: ServerTrustPolicyTestCase { let host = "test.alamofire.org" let serverTrust = TestTrusts.leafExpired.trust let certificates = [TestCertificates.intermediateCA2] - let serverTrustPolicy = ServerTrustPolicy.pinCertificates( - certificates: certificates, - validateCertificateChain: true, - validateHost: true - ) + let serverTrustPolicy = PinnedCertificatesTrustEvaluator(certificates: certificates) // When - let serverTrustIsValid = serverTrustPolicy.evaluate(serverTrust, forHost: host) + let result = Result { try serverTrustPolicy.evaluate(serverTrust, forHost: host) } // Then - XCTAssertFalse(serverTrustIsValid, "server trust should not pass evaluation") + XCTAssertFalse(result.isSuccess, "server trust should not pass evaluation") } // MARK: Do NOT Validate Certificate Chain or Host @@ -1044,17 +934,17 @@ class ServerTrustPolicyPinCertificatesTestCase: ServerTrustPolicyTestCase { let host = "test.alamofire.org" let serverTrust = TestTrusts.leafValidDNSName.trust let certificates = [TestCertificates.leafValidDNSName] - let serverTrustPolicy = ServerTrustPolicy.pinCertificates( + let serverTrustPolicy = PinnedCertificatesTrustEvaluator( certificates: certificates, - validateCertificateChain: false, + performDefaultValidation: false, validateHost: false ) // When - let serverTrustIsValid = serverTrustPolicy.evaluate(serverTrust, forHost: host) + let result = Result { try serverTrustPolicy.evaluate(serverTrust, forHost: host) } // Then - XCTAssertTrue(serverTrustIsValid, "server trust should pass evaluation") + XCTAssertTrue(result.isSuccess, "server trust should pass evaluation") } func testThatPinnedIntermediateCertificateWithoutCertificateChainValidationPassesEvaluation() { @@ -1062,17 +952,17 @@ class ServerTrustPolicyPinCertificatesTestCase: ServerTrustPolicyTestCase { let host = "test.alamofire.org" let serverTrust = TestTrusts.leafValidDNSName.trust let certificates = [TestCertificates.intermediateCA2] - let serverTrustPolicy = ServerTrustPolicy.pinCertificates( + let serverTrustPolicy = PinnedCertificatesTrustEvaluator( certificates: certificates, - validateCertificateChain: false, + performDefaultValidation: false, validateHost: false ) // When - let serverTrustIsValid = serverTrustPolicy.evaluate(serverTrust, forHost: host) + let result = Result { try serverTrustPolicy.evaluate(serverTrust, forHost: host) } // Then - XCTAssertTrue(serverTrustIsValid, "server trust should pass evaluation") + XCTAssertTrue(result.isSuccess, "server trust should pass evaluation") } func testThatPinnedRootCertificateWithoutCertificateChainValidationPassesEvaluation() { @@ -1080,17 +970,17 @@ class ServerTrustPolicyPinCertificatesTestCase: ServerTrustPolicyTestCase { let host = "test.alamofire.org" let serverTrust = TestTrusts.leafValidDNSName.trust let certificates = [TestCertificates.rootCA] - let serverTrustPolicy = ServerTrustPolicy.pinCertificates( + let serverTrustPolicy = PinnedCertificatesTrustEvaluator( certificates: certificates, - validateCertificateChain: false, + performDefaultValidation: false, validateHost: false ) // When - let serverTrustIsValid = serverTrustPolicy.evaluate(serverTrust, forHost: host) + let result = Result { try serverTrustPolicy.evaluate(serverTrust, forHost: host) } // Then - XCTAssertTrue(serverTrustIsValid, "server trust should pass evaluation") + XCTAssertTrue(result.isSuccess, "server trust should pass evaluation") } func testThatPinningLeafCertificateNotInCertificateChainWithoutCertificateChainValidationFailsEvaluation() { @@ -1098,17 +988,17 @@ class ServerTrustPolicyPinCertificatesTestCase: ServerTrustPolicyTestCase { let host = "test.alamofire.org" let serverTrust = TestTrusts.leafValidDNSName.trust let certificates = [TestCertificates.leafSignedByCA2] - let serverTrustPolicy = ServerTrustPolicy.pinCertificates( + let serverTrustPolicy = PinnedCertificatesTrustEvaluator( certificates: certificates, - validateCertificateChain: false, + performDefaultValidation: false, validateHost: false ) // When - let serverTrustIsValid = serverTrustPolicy.evaluate(serverTrust, forHost: host) + let result = Result { try serverTrustPolicy.evaluate(serverTrust, forHost: host) } // Then - XCTAssertFalse(serverTrustIsValid, "server trust should not pass evaluation") + XCTAssertFalse(result.isSuccess, "server trust should not pass evaluation") } func testThatPinningIntermediateCertificateNotInCertificateChainWithoutCertificateChainValidationFailsEvaluation() { @@ -1116,17 +1006,17 @@ class ServerTrustPolicyPinCertificatesTestCase: ServerTrustPolicyTestCase { let host = "test.alamofire.org" let serverTrust = TestTrusts.leafValidDNSName.trust let certificates = [TestCertificates.intermediateCA1] - let serverTrustPolicy = ServerTrustPolicy.pinCertificates( + let serverTrustPolicy = PinnedCertificatesTrustEvaluator( certificates: certificates, - validateCertificateChain: false, + performDefaultValidation: false, validateHost: false ) // When - let serverTrustIsValid = serverTrustPolicy.evaluate(serverTrust, forHost: host) + let result = Result { try serverTrustPolicy.evaluate(serverTrust, forHost: host) } // Then - XCTAssertFalse(serverTrustIsValid, "server trust should not pass evaluation") + XCTAssertFalse(result.isSuccess, "server trust should not pass evaluation") } func testThatPinningExpiredLeafCertificateWithoutCertificateChainValidationPassesEvaluation() { @@ -1134,17 +1024,17 @@ class ServerTrustPolicyPinCertificatesTestCase: ServerTrustPolicyTestCase { let host = "test.alamofire.org" let serverTrust = TestTrusts.leafExpired.trust let certificates = [TestCertificates.leafExpired] - let serverTrustPolicy = ServerTrustPolicy.pinCertificates( + let serverTrustPolicy = PinnedCertificatesTrustEvaluator( certificates: certificates, - validateCertificateChain: false, + performDefaultValidation: false, validateHost: false ) // When - let serverTrustIsValid = serverTrustPolicy.evaluate(serverTrust, forHost: host) + let result = Result { try serverTrustPolicy.evaluate(serverTrust, forHost: host) } // Then - XCTAssertTrue(serverTrustIsValid, "server trust should pass evaluation") + XCTAssertTrue(result.isSuccess, "server trust should pass evaluation") } func testThatPinningIntermediateCertificateWithExpiredLeafCertificateWithoutCertificateChainValidationPassesEvaluation() { @@ -1152,17 +1042,17 @@ class ServerTrustPolicyPinCertificatesTestCase: ServerTrustPolicyTestCase { let host = "test.alamofire.org" let serverTrust = TestTrusts.leafExpired.trust let certificates = [TestCertificates.intermediateCA2] - let serverTrustPolicy = ServerTrustPolicy.pinCertificates( + let serverTrustPolicy = PinnedCertificatesTrustEvaluator( certificates: certificates, - validateCertificateChain: false, + performDefaultValidation: false, validateHost: false ) // When - let serverTrustIsValid = serverTrustPolicy.evaluate(serverTrust, forHost: host) + let result = Result { try serverTrustPolicy.evaluate(serverTrust, forHost: host) } // Then - XCTAssertTrue(serverTrustIsValid, "server trust should pass evaluation") + XCTAssertTrue(result.isSuccess, "server trust should pass evaluation") } func testThatPinningRootCertificateWithExpiredLeafCertificateWithoutCertificateChainValidationPassesEvaluation() { @@ -1170,17 +1060,17 @@ class ServerTrustPolicyPinCertificatesTestCase: ServerTrustPolicyTestCase { let host = "test.alamofire.org" let serverTrust = TestTrusts.leafExpired.trust let certificates = [TestCertificates.rootCA] - let serverTrustPolicy = ServerTrustPolicy.pinCertificates( + let serverTrustPolicy = PinnedCertificatesTrustEvaluator( certificates: certificates, - validateCertificateChain: false, + performDefaultValidation: false, validateHost: false ) // When - let serverTrustIsValid = serverTrustPolicy.evaluate(serverTrust, forHost: host) + let result = Result { try serverTrustPolicy.evaluate(serverTrust, forHost: host) } // Then - XCTAssertTrue(serverTrustIsValid, "server trust should pass evaluation") + XCTAssertTrue(result.isSuccess, "server trust should pass evaluation") } func testThatPinningMultipleCertificatesWithoutCertificateChainValidationPassesEvaluation() { @@ -1196,17 +1086,17 @@ class ServerTrustPolicyPinCertificatesTestCase: ServerTrustPolicyTestCase { TestCertificates.leafDNSNameAndURI, // not in certificate chain ] - let serverTrustPolicy = ServerTrustPolicy.pinCertificates( + let serverTrustPolicy = PinnedCertificatesTrustEvaluator( certificates: certificates, - validateCertificateChain: false, + performDefaultValidation: false, validateHost: false ) // When - let serverTrustIsValid = serverTrustPolicy.evaluate(serverTrust, forHost: host) + let result = Result { try serverTrustPolicy.evaluate(serverTrust, forHost: host) } // Then - XCTAssertTrue(serverTrustIsValid, "server trust should pass evaluation") + XCTAssertTrue(result.isSuccess, "server trust should pass evaluation") } } @@ -1220,95 +1110,75 @@ class ServerTrustPolicyPinPublicKeysTestCase: ServerTrustPolicyTestCase { // Given let host = "test.alamofire.org" let serverTrust = TestTrusts.leafValidDNSName.trust - let publicKeys = [TestPublicKeys.leafValidDNSName] - let serverTrustPolicy = ServerTrustPolicy.pinPublicKeys( - publicKeys: publicKeys, - validateCertificateChain: true, - validateHost: false - ) + let keys = [TestCertificates.leafValidDNSName].publicKeys + let serverTrustPolicy = PublicKeysTrustEvaluator(keys: keys, validateHost: false) // When setRootCertificateAsLoneAnchorCertificateForTrust(serverTrust) - let serverTrustIsValid = serverTrustPolicy.evaluate(serverTrust, forHost: host) + let result = Result { try serverTrustPolicy.evaluate(serverTrust, forHost: host) } // Then - XCTAssertTrue(serverTrustIsValid, "server trust should pass evaluation") + XCTAssertTrue(result.isSuccess, "server trust should pass evaluation") } func testThatPinningIntermediateKeyPassesEvaluationWithoutHostValidation() { // Given let host = "test.alamofire.org" let serverTrust = TestTrusts.leafValidDNSName.trust - let publicKeys = [TestPublicKeys.intermediateCA2] - let serverTrustPolicy = ServerTrustPolicy.pinPublicKeys( - publicKeys: publicKeys, - validateCertificateChain: true, - validateHost: false - ) + let keys = [TestCertificates.intermediateCA2].publicKeys + let serverTrustPolicy = PublicKeysTrustEvaluator(keys: keys, validateHost: false) // When setRootCertificateAsLoneAnchorCertificateForTrust(serverTrust) - let serverTrustIsValid = serverTrustPolicy.evaluate(serverTrust, forHost: host) + let result = Result { try serverTrustPolicy.evaluate(serverTrust, forHost: host) } // Then - XCTAssertTrue(serverTrustIsValid, "server trust should pass evaluation") + XCTAssertTrue(result.isSuccess, "server trust should pass evaluation") } func testThatPinningRootKeyPassesEvaluationWithoutHostValidation() { // Given let host = "test.alamofire.org" let serverTrust = TestTrusts.leafValidDNSName.trust - let publicKeys = [TestPublicKeys.rootCA] - let serverTrustPolicy = ServerTrustPolicy.pinPublicKeys( - publicKeys: publicKeys, - validateCertificateChain: true, - validateHost: false - ) + let keys = [TestCertificates.rootCA].publicKeys + let serverTrustPolicy = PublicKeysTrustEvaluator(keys: keys, validateHost: false) // When setRootCertificateAsLoneAnchorCertificateForTrust(serverTrust) - let serverTrustIsValid = serverTrustPolicy.evaluate(serverTrust, forHost: host) + let result = Result { try serverTrustPolicy.evaluate(serverTrust, forHost: host) } // Then - XCTAssertTrue(serverTrustIsValid, "server trust should pass evaluation") + XCTAssertTrue(result.isSuccess, "server trust should pass evaluation") } func testThatPinningKeyNotInCertificateChainFailsEvaluationWithoutHostValidation() { // Given let host = "test.alamofire.org" let serverTrust = TestTrusts.leafValidDNSName.trust - let publicKeys = [TestPublicKeys.leafSignedByCA2] - let serverTrustPolicy = ServerTrustPolicy.pinPublicKeys( - publicKeys: publicKeys, - validateCertificateChain: true, - validateHost: false - ) + let keys = [TestCertificates.leafSignedByCA2].publicKeys + let serverTrustPolicy = PublicKeysTrustEvaluator(keys: keys, validateHost: false) // When setRootCertificateAsLoneAnchorCertificateForTrust(serverTrust) - let serverTrustIsValid = serverTrustPolicy.evaluate(serverTrust, forHost: host) + let result = Result { try serverTrustPolicy.evaluate(serverTrust, forHost: host) } // Then - XCTAssertFalse(serverTrustIsValid, "server trust should not pass evaluation") + XCTAssertFalse(result.isSuccess, "server trust should not pass evaluation") } func testThatPinningBackupKeyPassesEvaluationWithoutHostValidation() { // Given let host = "test.alamofire.org" let serverTrust = TestTrusts.leafValidDNSName.trust - let publicKeys = [TestPublicKeys.leafSignedByCA1, TestPublicKeys.intermediateCA1, TestPublicKeys.leafValidDNSName] - let serverTrustPolicy = ServerTrustPolicy.pinPublicKeys( - publicKeys: publicKeys, - validateCertificateChain: true, - validateHost: false - ) + let keys = [TestCertificates.leafSignedByCA1, TestCertificates.intermediateCA1, TestCertificates.leafValidDNSName].publicKeys + let serverTrustPolicy = PublicKeysTrustEvaluator(keys: keys, validateHost: false) // When setRootCertificateAsLoneAnchorCertificateForTrust(serverTrust) - let serverTrustIsValid = serverTrustPolicy.evaluate(serverTrust, forHost: host) + let result = Result { try serverTrustPolicy.evaluate(serverTrust, forHost: host) } // Then - XCTAssertTrue(serverTrustIsValid, "server trust should pass evaluation") + XCTAssertTrue(result.isSuccess, "server trust should pass evaluation") } // MARK: Validate Certificate Chain and Host @@ -1317,275 +1187,263 @@ class ServerTrustPolicyPinPublicKeysTestCase: ServerTrustPolicyTestCase { // Given let host = "test.alamofire.org" let serverTrust = TestTrusts.leafValidDNSName.trust - let publicKeys = [TestPublicKeys.leafValidDNSName] - let serverTrustPolicy = ServerTrustPolicy.pinPublicKeys( - publicKeys: publicKeys, - validateCertificateChain: true, - validateHost: true - ) + let keys = [TestCertificates.leafValidDNSName].publicKeys + let serverTrustPolicy = PublicKeysTrustEvaluator(keys: keys) // When setRootCertificateAsLoneAnchorCertificateForTrust(serverTrust) - let serverTrustIsValid = serverTrustPolicy.evaluate(serverTrust, forHost: host) + let result = Result { try serverTrustPolicy.evaluate(serverTrust, forHost: host) } // Then - XCTAssertTrue(serverTrustIsValid, "server trust should pass evaluation") + XCTAssertTrue(result.isSuccess, "server trust should pass evaluation") } func testThatPinningIntermediateKeyPassesEvaluationWithHostValidation() { // Given let host = "test.alamofire.org" let serverTrust = TestTrusts.leafValidDNSName.trust - let publicKeys = [TestPublicKeys.intermediateCA2] - let serverTrustPolicy = ServerTrustPolicy.pinPublicKeys( - publicKeys: publicKeys, - validateCertificateChain: true, - validateHost: true - ) + let keys = [TestCertificates.intermediateCA2].publicKeys + let serverTrustPolicy = PublicKeysTrustEvaluator(keys: keys) // When setRootCertificateAsLoneAnchorCertificateForTrust(serverTrust) - let serverTrustIsValid = serverTrustPolicy.evaluate(serverTrust, forHost: host) + let result = Result { try serverTrustPolicy.evaluate(serverTrust, forHost: host) } // Then - XCTAssertTrue(serverTrustIsValid, "server trust should pass evaluation") + XCTAssertTrue(result.isSuccess, "server trust should pass evaluation") } func testThatPinningRootKeyPassesEvaluationWithHostValidation() { // Given let host = "test.alamofire.org" let serverTrust = TestTrusts.leafValidDNSName.trust - let publicKeys = [TestPublicKeys.rootCA] - let serverTrustPolicy = ServerTrustPolicy.pinPublicKeys( - publicKeys: publicKeys, - validateCertificateChain: true, - validateHost: true - ) + let keys = [TestCertificates.rootCA].publicKeys + let serverTrustPolicy = PublicKeysTrustEvaluator(keys: keys) // When setRootCertificateAsLoneAnchorCertificateForTrust(serverTrust) - let serverTrustIsValid = serverTrustPolicy.evaluate(serverTrust, forHost: host) + let result = Result { try serverTrustPolicy.evaluate(serverTrust, forHost: host) } // Then - XCTAssertTrue(serverTrustIsValid, "server trust should pass evaluation") + XCTAssertTrue(result.isSuccess, "server trust should pass evaluation") } func testThatPinningKeyNotInCertificateChainFailsEvaluationWithHostValidation() { // Given let host = "test.alamofire.org" let serverTrust = TestTrusts.leafValidDNSName.trust - let publicKeys = [TestPublicKeys.leafSignedByCA2] - let serverTrustPolicy = ServerTrustPolicy.pinPublicKeys( - publicKeys: publicKeys, - validateCertificateChain: true, - validateHost: true - ) + let keys = [TestCertificates.leafSignedByCA2].publicKeys + let serverTrustPolicy = PublicKeysTrustEvaluator(keys: keys) // When setRootCertificateAsLoneAnchorCertificateForTrust(serverTrust) - let serverTrustIsValid = serverTrustPolicy.evaluate(serverTrust, forHost: host) + let result = Result { try serverTrustPolicy.evaluate(serverTrust, forHost: host) } // Then - XCTAssertFalse(serverTrustIsValid, "server trust should not pass evaluation") + XCTAssertFalse(result.isSuccess, "server trust should not pass evaluation") } func testThatPinningBackupKeyPassesEvaluationWithHostValidation() { // Given let host = "test.alamofire.org" let serverTrust = TestTrusts.leafValidDNSName.trust - let publicKeys = [TestPublicKeys.leafSignedByCA1, TestPublicKeys.intermediateCA1, TestPublicKeys.leafValidDNSName] - let serverTrustPolicy = ServerTrustPolicy.pinPublicKeys( - publicKeys: publicKeys, - validateCertificateChain: true, - validateHost: true - ) + let keys = [TestCertificates.leafSignedByCA1, TestCertificates.intermediateCA1, TestCertificates.leafValidDNSName].publicKeys + let serverTrustPolicy = PublicKeysTrustEvaluator(keys: keys) // When setRootCertificateAsLoneAnchorCertificateForTrust(serverTrust) - let serverTrustIsValid = serverTrustPolicy.evaluate(serverTrust, forHost: host) + let result = Result { try serverTrustPolicy.evaluate(serverTrust, forHost: host) } // Then - XCTAssertTrue(serverTrustIsValid, "server trust should pass evaluation") + XCTAssertTrue(result.isSuccess, "server trust should pass evaluation") } - // MARK: Do NOT Validate Certificate Chain or Host + // MARK: Do NOT perform default validation or validate host. func testThatPinningLeafKeyWithoutCertificateChainValidationPassesEvaluationWithMissingIntermediateCertificate() { // Given let host = "test.alamofire.org" let serverTrust = TestTrusts.leafValidDNSNameMissingIntermediate.trust - let publicKeys = [TestPublicKeys.leafValidDNSName] - let serverTrustPolicy = ServerTrustPolicy.pinPublicKeys( - publicKeys: publicKeys, - validateCertificateChain: false, - validateHost: false - ) + let keys = [TestCertificates.leafValidDNSName].publicKeys + let serverTrustPolicy = PublicKeysTrustEvaluator(keys: keys, + performDefaultValidation: false, + validateHost: false) // When setRootCertificateAsLoneAnchorCertificateForTrust(serverTrust) - let serverTrustIsValid = serverTrustPolicy.evaluate(serverTrust, forHost: host) + let result = Result { try serverTrustPolicy.evaluate(serverTrust, forHost: host) } // Then - XCTAssertTrue(serverTrustIsValid, "server trust should pass evaluation") + XCTAssertTrue(result.isSuccess, "server trust should pass evaluation") } func testThatPinningRootKeyWithoutCertificateChainValidationFailsEvaluationWithMissingIntermediateCertificate() { // Given let host = "test.alamofire.org" let serverTrust = TestTrusts.leafValidDNSNameMissingIntermediate.trust - let publicKeys = [TestPublicKeys.rootCA] - let serverTrustPolicy = ServerTrustPolicy.pinPublicKeys( - publicKeys: publicKeys, - validateCertificateChain: false, - validateHost: false - ) + let keys = [TestCertificates.rootCA].publicKeys + let serverTrustPolicy = PublicKeysTrustEvaluator(keys: keys, + performDefaultValidation: false, + validateHost: false) // When setRootCertificateAsLoneAnchorCertificateForTrust(serverTrust) - let serverTrustIsValid = serverTrustPolicy.evaluate(serverTrust, forHost: host) + let result = Result { try serverTrustPolicy.evaluate(serverTrust, forHost: host) } // Then - XCTAssertFalse(serverTrustIsValid, "server trust should not pass evaluation") + XCTAssertFalse(result.isSuccess, "server trust should not pass evaluation") } func testThatPinningLeafKeyWithoutCertificateChainValidationPassesEvaluationWithIncorrectIntermediateCertificate() { // Given let host = "test.alamofire.org" let serverTrust = TestTrusts.leafValidDNSNameWithIncorrectIntermediate.trust - let publicKeys = [TestPublicKeys.leafValidDNSName] - let serverTrustPolicy = ServerTrustPolicy.pinPublicKeys( - publicKeys: publicKeys, - validateCertificateChain: false, - validateHost: false - ) + let keys = [TestCertificates.leafValidDNSName].publicKeys + let serverTrustPolicy = PublicKeysTrustEvaluator(keys: keys, + performDefaultValidation: false, + validateHost: false) // When setRootCertificateAsLoneAnchorCertificateForTrust(serverTrust) - let serverTrustIsValid = serverTrustPolicy.evaluate(serverTrust, forHost: host) + let result = Result { try serverTrustPolicy.evaluate(serverTrust, forHost: host) } // Then - XCTAssertTrue(serverTrustIsValid, "server trust should pass evaluation") + XCTAssertTrue(result.isSuccess, "server trust should pass evaluation") } func testThatPinningLeafKeyWithoutCertificateChainValidationPassesEvaluationWithExpiredLeafCertificate() { // Given let host = "test.alamofire.org" let serverTrust = TestTrusts.leafExpired.trust - let publicKeys = [TestPublicKeys.leafExpired] - let serverTrustPolicy = ServerTrustPolicy.pinPublicKeys( - publicKeys: publicKeys, - validateCertificateChain: false, - validateHost: false - ) + let keys = [TestCertificates.leafExpired].publicKeys + let serverTrustPolicy = PublicKeysTrustEvaluator(keys: keys, + performDefaultValidation: false, + validateHost: false) // When setRootCertificateAsLoneAnchorCertificateForTrust(serverTrust) - let serverTrustIsValid = serverTrustPolicy.evaluate(serverTrust, forHost: host) + let result = Result { try serverTrustPolicy.evaluate(serverTrust, forHost: host) } // Then - XCTAssertTrue(serverTrustIsValid, "server trust should pass evaluation") + XCTAssertTrue(result.isSuccess, "server trust should pass evaluation") } func testThatPinningIntermediateKeyWithoutCertificateChainValidationPassesEvaluationWithExpiredLeafCertificate() { // Given let host = "test.alamofire.org" let serverTrust = TestTrusts.leafExpired.trust - let publicKeys = [TestPublicKeys.intermediateCA2] - let serverTrustPolicy = ServerTrustPolicy.pinPublicKeys( - publicKeys: publicKeys, - validateCertificateChain: false, - validateHost: false - ) + let keys = [TestCertificates.intermediateCA2].publicKeys + let serverTrustPolicy = PublicKeysTrustEvaluator(keys: keys, + performDefaultValidation: false, + validateHost: false) // When setRootCertificateAsLoneAnchorCertificateForTrust(serverTrust) - let serverTrustIsValid = serverTrustPolicy.evaluate(serverTrust, forHost: host) + let result = Result { try serverTrustPolicy.evaluate(serverTrust, forHost: host) } // Then - XCTAssertTrue(serverTrustIsValid, "server trust should pass evaluation") + XCTAssertTrue(result.isSuccess, "server trust should pass evaluation") } func testThatPinningRootKeyWithoutCertificateChainValidationPassesEvaluationWithExpiredLeafCertificate() { // Given let host = "test.alamofire.org" let serverTrust = TestTrusts.leafExpired.trust - let publicKeys = [TestPublicKeys.rootCA] - let serverTrustPolicy = ServerTrustPolicy.pinPublicKeys( - publicKeys: publicKeys, - validateCertificateChain: false, - validateHost: false - ) + let keys = [TestCertificates.rootCA].publicKeys + let serverTrustPolicy = PublicKeysTrustEvaluator(keys: keys, + performDefaultValidation: false, + validateHost: false) // When setRootCertificateAsLoneAnchorCertificateForTrust(serverTrust) - let serverTrustIsValid = serverTrustPolicy.evaluate(serverTrust, forHost: host) + let result = Result { try serverTrustPolicy.evaluate(serverTrust, forHost: host) } // Then - XCTAssertTrue(serverTrustIsValid, "server trust should pass evaluation") + XCTAssertTrue(result.isSuccess, "server trust should pass evaluation") } } // MARK: - class ServerTrustPolicyDisableEvaluationTestCase: ServerTrustPolicyTestCase { - func testThatCertificateChainMissingIntermediateCertificatePassesEvaluation() { + func testThatCertificateChainMissingIntermediateCertificatePassesEvaluation() throws { // Given let host = "test.alamofire.org" let serverTrust = TestTrusts.leafValidDNSNameMissingIntermediate.trust - let serverTrustPolicy = ServerTrustPolicy.disableEvaluation + let serverTrustPolicy = DisabledEvaluator() // When - let serverTrustIsValid = serverTrustPolicy.evaluate(serverTrust, forHost: host) + let result = Result { try serverTrustPolicy.evaluate(serverTrust, forHost: host) } // Then - XCTAssertTrue(serverTrustIsValid, "server trust should pass evaluation") + XCTAssertTrue(result.isSuccess, "server trust should pass evaluation") } - func testThatExpiredLeafCertificatePassesEvaluation() { + func testThatExpiredLeafCertificatePassesEvaluation() throws { // Given let host = "test.alamofire.org" let serverTrust = TestTrusts.leafExpired.trust - let serverTrustPolicy = ServerTrustPolicy.disableEvaluation + let serverTrustPolicy = DisabledEvaluator() // When - let serverTrustIsValid = serverTrustPolicy.evaluate(serverTrust, forHost: host) + let result = Result { try serverTrustPolicy.evaluate(serverTrust, forHost: host) } // Then - XCTAssertTrue(serverTrustIsValid, "server trust should pass evaluation") + XCTAssertTrue(result.isSuccess, "server trust should pass evaluation") } } // MARK: - -class ServerTrustPolicyCustomEvaluationTestCase: ServerTrustPolicyTestCase { - func testThatReturningTrueFromClosurePassesEvaluation() { +class ServerTrustPolicyCompositeTestCase: ServerTrustPolicyTestCase { + func testThatValidCertificateChainPassesDefaultAndRevocationCompositeChecks() throws { // Given let host = "test.alamofire.org" let serverTrust = TestTrusts.leafValidDNSName.trust - let serverTrustPolicy = ServerTrustPolicy.customEvaluation { _, _ in - return true - } + let defaultPolicy = DefaultTrustEvaluator(validateHost: false) + let revocationPolicy = RevocationTrustEvaluator(validateHost: false) + let compositePolicy = CompositeTrustEvaluator(evaluators: [defaultPolicy, revocationPolicy]) // When - let serverTrustIsValid = serverTrustPolicy.evaluate(serverTrust, forHost: host) + setRootCertificateAsLoneAnchorCertificateForTrust(serverTrust) + let result = Result { try compositePolicy.evaluate(serverTrust, forHost: host) } // Then - XCTAssertTrue(serverTrustIsValid, "server trust should pass evaluation") + XCTAssertTrue(result.isSuccess, "server trust should pass evaluation") } - func testThatReturningFalseFromClosurePassesEvaluation() { + func testThatNonAnchoredRootCertificateChainFailsEvaluationWithoutHostValidation() throws { // Given let host = "test.alamofire.org" - let serverTrust = TestTrusts.leafValidDNSName.trust - let serverTrustPolicy = ServerTrustPolicy.customEvaluation { _, _ in - return false - } + let serverTrust = TestTrusts.trustWithCertificates([ + TestCertificates.leafValidDNSName, + TestCertificates.intermediateCA2 + ]) + let defaultPolicy = DefaultTrustEvaluator(validateHost: false) + let revocationPolicy = RevocationTrustEvaluator(validateHost: false) + let compositePolicy = CompositeTrustEvaluator(evaluators: [defaultPolicy, revocationPolicy]) + + // When + let result = Result { try compositePolicy.evaluate(serverTrust, forHost: host) } + + // Then + XCTAssertFalse(result.isSuccess, "server trust should not pass evaluation") + } + + func testThatExpiredLeafCertificateFailsDefaultAndRevocationComposite() throws { + // Given + let host = "test.alamofire.org" + let serverTrust = TestTrusts.leafExpired.trust + let defaultPolicy = DefaultTrustEvaluator(validateHost: false) + let revocationPolicy = RevocationTrustEvaluator(validateHost: false) + let compositePolicy = CompositeTrustEvaluator(evaluators: [defaultPolicy, revocationPolicy]) // When - let serverTrustIsValid = serverTrustPolicy.evaluate(serverTrust, forHost: host) + setRootCertificateAsLoneAnchorCertificateForTrust(serverTrust) + let result = Result { try compositePolicy.evaluate(serverTrust, forHost: host) } // Then - XCTAssertFalse(serverTrustIsValid, "server trust should not pass evaluation") + XCTAssertFalse(result.isSuccess, "server trust should not pass evaluation") } } @@ -1603,16 +1461,14 @@ class ServerTrustPolicyCertificatesInBundleTestCase: ServerTrustPolicyTestCase { // keyDER.der: DER-encoded key, not a certificate, should fail // When - let certificates = ServerTrustPolicy.certificates( - in: Bundle(for: ServerTrustPolicyCertificatesInBundleTestCase.self) - ) + let certificates = Bundle(for: ServerTrustPolicyCertificatesInBundleTestCase.self).certificates // Then // Expectation: 19 well-formed certificates in the test bundle plus 4 invalid certificates. #if os(macOS) // For some reason, macOS is allowing all certificates to be considered valid. Need to file a // rdar demonstrating this behavior. - if #available(OSX 10.12, *) { + if #available(macOS 10.12, *) { XCTAssertEqual(certificates.count, 19, "Expected 19 well-formed certificates") } else { XCTAssertEqual(certificates.count, 23, "Expected 23 well-formed certificates") diff --git a/Tests/SessionDelegateTests.swift b/Tests/SessionDelegateTests.swift index c7d49074b..bf1206721 100644 --- a/Tests/SessionDelegateTests.swift +++ b/Tests/SessionDelegateTests.swift @@ -27,104 +27,13 @@ import Foundation import XCTest class SessionDelegateTestCase: BaseTestCase { - var manager: SessionManager! + var manager: Session! // MARK: - Setup and Teardown override func setUp() { super.setUp() - manager = SessionManager(configuration: .ephemeral) - } - - // MARK: - Tests - Session Invalidation - - func testThatSessionDidBecomeInvalidWithErrorClosureIsCalledWhenSet() { - // Given - let expectation = self.expectation(description: "Override closure should be called") - - var overrideClosureCalled = false - var invalidationError: Error? - - manager.delegate.sessionDidBecomeInvalidWithError = { _, error in - overrideClosureCalled = true - invalidationError = error - - expectation.fulfill() - } - - // When - manager.session.invalidateAndCancel() - waitForExpectations(timeout: timeout, handler: nil) - - // Then - XCTAssertTrue(overrideClosureCalled) - XCTAssertNil(invalidationError) - } - - // MARK: - Tests - Session Challenges - - func testThatSessionDidReceiveChallengeClosureIsCalledWhenSet() { - if #available(iOS 9.0, *) { - // Given - let expectation = self.expectation(description: "Override closure should be called") - - var overrideClosureCalled = false - var response: HTTPURLResponse? - - manager.delegate.sessionDidReceiveChallenge = { session, challenge in - overrideClosureCalled = true - return (.performDefaultHandling, nil) - } - - // When - manager.request("https://httpbin.org/get").responseJSON { closureResponse in - response = closureResponse.response - expectation.fulfill() - } - - waitForExpectations(timeout: timeout, handler: nil) - - // Then - XCTAssertTrue(overrideClosureCalled) - XCTAssertEqual(response?.statusCode, 200) - } else { - // This test MUST be disabled on iOS 8.x because `respondsToSelector` is not being called for the - // `URLSession:didReceiveChallenge:completionHandler:` selector when more than one test here is run - // at a time. Whether we flush the URL session of wipe all the shared credentials, the behavior is - // still the same. Until we find a better solution, we'll need to disable this test on iOS 8.x. - } - } - - func testThatSessionDidReceiveChallengeWithCompletionClosureIsCalledWhenSet() { - if #available(iOS 9.0, *) { - // Given - let expectation = self.expectation(description: "Override closure should be called") - - var overrideClosureCalled = false - var response: HTTPURLResponse? - - manager.delegate.sessionDidReceiveChallengeWithCompletion = { session, challenge, completion in - overrideClosureCalled = true - completion(.performDefaultHandling, nil) - } - - // When - manager.request("https://httpbin.org/get").responseJSON { closureResponse in - response = closureResponse.response - expectation.fulfill() - } - - waitForExpectations(timeout: timeout, handler: nil) - - // Then - XCTAssertTrue(overrideClosureCalled) - XCTAssertEqual(response?.statusCode, 200) - } else { - // This test MUST be disabled on iOS 8.x because `respondsToSelector` is not being called for the - // `URLSession:didReceiveChallenge:completionHandler:` selector when more than one test here is run - // at a time. Whether we flush the URL session of wipe all the shared credentials, the behavior is - // still the same. Until we find a better solution, we'll need to disable this test on iOS 8.x. - } + manager = Session(configuration: .ephemeral) } // MARK: - Tests - Redirects @@ -136,7 +45,7 @@ class SessionDelegateTestCase: BaseTestCase { let expectation = self.expectation(description: "Request should redirect to \(redirectURLString)") - var response: DefaultDataResponse? + var response: DataResponse? // When manager.request(urlString) @@ -164,237 +73,7 @@ class SessionDelegateTestCase: BaseTestCase { let expectation = self.expectation(description: "Request should redirect to \(redirectURLString)") - var response: DefaultDataResponse? - - // When - manager.request(urlString) - .response { resp in - response = resp - expectation.fulfill() - } - - waitForExpectations(timeout: timeout, handler: nil) - - // Then - XCTAssertNotNil(response?.request) - XCTAssertNotNil(response?.response) - XCTAssertNotNil(response?.data) - XCTAssertNil(response?.error) - - XCTAssertEqual(response?.response?.url?.absoluteString, redirectURLString) - XCTAssertEqual(response?.response?.statusCode, 200) - } - - func testThatTaskOverrideClosureCanPerformHTTPRedirection() { - // Given - let redirectURLString = "https://www.apple.com/" - let urlString = "https://httpbin.org/redirect-to?url=\(redirectURLString)" - - let expectation = self.expectation(description: "Request should redirect to \(redirectURLString)") - let callbackExpectation = self.expectation(description: "Redirect callback should be made") - let delegate: SessionDelegate = manager.delegate - - delegate.taskWillPerformHTTPRedirection = { _, _, _, request in - callbackExpectation.fulfill() - return request - } - - var response: DefaultDataResponse? - - // When - manager.request(urlString) - .response { resp in - response = resp - expectation.fulfill() - } - - waitForExpectations(timeout: timeout, handler: nil) - - // Then - XCTAssertNotNil(response?.request) - XCTAssertNotNil(response?.response) - XCTAssertNotNil(response?.data) - XCTAssertNil(response?.error) - - XCTAssertEqual(response?.response?.url?.absoluteString, redirectURLString) - XCTAssertEqual(response?.response?.statusCode, 200) - } - - func testThatTaskOverrideClosureWithCompletionCanPerformHTTPRedirection() { - // Given - let redirectURLString = "https://www.apple.com/" - let urlString = "https://httpbin.org/redirect-to?url=\(redirectURLString)" - - let expectation = self.expectation(description: "Request should redirect to \(redirectURLString)") - let callbackExpectation = self.expectation(description: "Redirect callback should be made") - let delegate: SessionDelegate = manager.delegate - - delegate.taskWillPerformHTTPRedirectionWithCompletion = { _, _, _, request, completion in - completion(request) - callbackExpectation.fulfill() - } - - var response: DefaultDataResponse? - - // When - manager.request(urlString) - .response { resp in - response = resp - expectation.fulfill() - } - - waitForExpectations(timeout: timeout, handler: nil) - - // Then - XCTAssertNotNil(response?.request) - XCTAssertNotNil(response?.response) - XCTAssertNotNil(response?.data) - XCTAssertNil(response?.error) - - XCTAssertEqual(response?.response?.url?.absoluteString, redirectURLString) - XCTAssertEqual(response?.response?.statusCode, 200) - } - - func testThatTaskOverrideClosureCanCancelHTTPRedirection() { - // Given - let redirectURLString = "https://www.apple.com" - let urlString = "https://httpbin.org/redirect-to?url=\(redirectURLString)" - - let expectation = self.expectation(description: "Request should not redirect to \(redirectURLString)") - let callbackExpectation = self.expectation(description: "Redirect callback should be made") - let delegate: SessionDelegate = manager.delegate - - delegate.taskWillPerformHTTPRedirectionWithCompletion = { _, _, _, _, completion in - callbackExpectation.fulfill() - completion(nil) - } - - var response: DefaultDataResponse? - - // When - manager.request(urlString) - .response { resp in - response = resp - expectation.fulfill() - } - - waitForExpectations(timeout: timeout, handler: nil) - - // Then - XCTAssertNotNil(response?.request) - XCTAssertNotNil(response?.response) - XCTAssertNotNil(response?.data) - XCTAssertNil(response?.error) - - XCTAssertEqual(response?.response?.url?.absoluteString, urlString) - XCTAssertEqual(response?.response?.statusCode, 302) - } - - func testThatTaskOverrideClosureWithCompletionCanCancelHTTPRedirection() { - // Given - let redirectURLString = "https://www.apple.com" - let urlString = "https://httpbin.org/redirect-to?url=\(redirectURLString)" - - let expectation = self.expectation(description: "Request should not redirect to \(redirectURLString)") - let callbackExpectation = self.expectation(description: "Redirect callback should be made") - let delegate: SessionDelegate = manager.delegate - - delegate.taskWillPerformHTTPRedirection = { _, _, _, _ in - callbackExpectation.fulfill() - return nil - } - - var response: DefaultDataResponse? - - // When - manager.request(urlString) - .response { resp in - response = resp - expectation.fulfill() - } - - waitForExpectations(timeout: timeout, handler: nil) - - // Then - XCTAssertNotNil(response?.request) - XCTAssertNotNil(response?.response) - XCTAssertNotNil(response?.data) - XCTAssertNil(response?.error) - - XCTAssertEqual(response?.response?.url?.absoluteString, urlString) - XCTAssertEqual(response?.response?.statusCode, 302) - } - - func testThatTaskOverrideClosureIsCalledMultipleTimesForMultipleHTTPRedirects() { - // Given - let redirectCount = 5 - let redirectURLString = "https://httpbin.org/get" - let urlString = "https://httpbin.org/redirect/\(redirectCount)" - - let expectation = self.expectation(description: "Request should redirect to \(redirectURLString)") - let delegate: SessionDelegate = manager.delegate - var redirectExpectations = [XCTestExpectation]() - for index in 0..? // When manager.request(urlString) @@ -415,178 +94,39 @@ class SessionDelegateTestCase: BaseTestCase { XCTAssertEqual(response?.response?.statusCode, 200) } - func testThatRedirectedRequestContainsAllHeadersFromOriginalRequest() { + func testThatAppropriateNotificationsAreCalledWithRequestForDataRequest() { // Given - let redirectURLString = "https://httpbin.org/get" - let urlString = "https://httpbin.org/redirect-to?url=\(redirectURLString)" - let headers = [ - "Authorization": "1234", - "Custom-Header": "foobar", - ] - - // NOTE: It appears that most headers are maintained during a redirect with the exception of the `Authorization` - // header. It appears that Apple's strips the `Authorization` header from the redirected URL request. If you - // need to maintain the `Authorization` header, you need to manually append it to the redirected request. - - manager.delegate.taskWillPerformHTTPRedirection = { session, task, response, request in - var redirectedRequest = request - - if - let originalRequest = task.originalRequest, - let headers = originalRequest.allHTTPHeaderFields, - let authorizationHeaderValue = headers["Authorization"] - { - var mutableRequest = request - mutableRequest.setValue(authorizationHeaderValue, forHTTPHeaderField: "Authorization") - redirectedRequest = mutableRequest - } - - return redirectedRequest - } - - let expectation = self.expectation(description: "Request should redirect to \(redirectURLString)") - - var response: DataResponse? - - // When - manager.request(urlString, headers: headers) - .responseJSON { closureResponse in - response = closureResponse - expectation.fulfill() - } - - waitForExpectations(timeout: timeout, handler: nil) - - // Then - XCTAssertNotNil(response?.request) - XCTAssertNotNil(response?.response) - XCTAssertNotNil(response?.data) - XCTAssertEqual(response?.result.isSuccess, true) - - if let json = response?.result.value as? [String: Any], let headers = json["headers"] as? [String: String] { - XCTAssertEqual(headers["Authorization"], "1234") - XCTAssertEqual(headers["Custom-Header"], "foobar") - } - } - - // MARK: - Tests - Data Task Responses - - func testThatDataTaskDidReceiveResponseClosureIsCalledWhenSet() { - // Given - let expectation = self.expectation(description: "Override closure should be called") - - var overrideClosureCalled = false - var response: HTTPURLResponse? - - manager.delegate.dataTaskDidReceiveResponse = { session, task, response in - overrideClosureCalled = true - return .allow + var request: Request? + _ = expectation(forNotification: Request.didResume, object: nil, handler: nil) + _ = expectation(forNotification: Request.didComplete, object: nil) { (notification) in + request = notification.request + return (request != nil) } // When - manager.request("https://httpbin.org/get").responseJSON { closureResponse in - response = closureResponse.response - expectation.fulfill() - } + manager.request("https://httpbin.org/get").response { _ in } waitForExpectations(timeout: timeout, handler: nil) // Then - XCTAssertTrue(overrideClosureCalled) - XCTAssertEqual(response?.statusCode, 200) + XCTAssertEqual(request?.response?.statusCode, 200) } - func testThatDataTaskDidReceiveResponseWithCompletionClosureIsCalledWhenSet() { + func testThatDidCompleteNotificationIsCalledWithRequestForDownloadRequests() { // Given - let expectation = self.expectation(description: "Override closure should be called") - - var overrideClosureCalled = false - var response: HTTPURLResponse? - - manager.delegate.dataTaskDidReceiveResponseWithCompletion = { session, task, response, completion in - overrideClosureCalled = true - completion(.allow) + var request: Request? + _ = expectation(forNotification: Request.didResume, object: nil, handler: nil) + _ = expectation(forNotification: Request.didComplete, object: nil) { (notification) in + request = notification.request + return (request != nil) } // When - manager.request("https://httpbin.org/get").responseJSON { closureResponse in - response = closureResponse.response - expectation.fulfill() - } + manager.download("https://httpbin.org/get").response { _ in } waitForExpectations(timeout: timeout, handler: nil) // Then - XCTAssertTrue(overrideClosureCalled) - XCTAssertEqual(response?.statusCode, 200) - } - - func testThatDidCompleteNotificationIsCalledWithResponseDataForDataTasks() { - // Given - var notificationCalledWithResponseData = false - var response: HTTPURLResponse? - #if swift(>=4.1) - let notification = Notification.Name.Task.DidComplete - #else - let notification = Notification.Name.Task.DidComplete.rawValue - #endif - let expectation = self.expectation(forNotification: notification, object: nil) { notif -> Bool in - - // check that we are handling notif for a dataTask - guard let task = notif.userInfo?[Notification.Key.Task] as? URLSessionDataTask else { - return false - } - - response = task.response as? HTTPURLResponse - - // check that responseData are set in userInfo-dict and it's not empty - if let responseData = notif.userInfo?[Notification.Key.ResponseData] as? Data { - notificationCalledWithResponseData = responseData.count > 0 - } - - return notificationCalledWithResponseData - } - - // When - manager.request("https://httpbin.org/get").responseJSON { resp in } - - wait(for: [expectation], timeout: timeout) - - // Then - XCTAssertTrue(notificationCalledWithResponseData) - XCTAssertEqual(response?.statusCode, 200) - } - - func testThatDidCompleteNotificationIsntCalledForDownloadTasks() { - // Given - var notificationCalledWithNilResponseData = false - var response: HTTPURLResponse? - #if swift(>=4.1) - let notification = Notification.Name.Task.DidComplete - #else - let notification = Notification.Name.Task.DidComplete.rawValue - #endif - let expectation = self.expectation(forNotification: notification, object: nil) { notif -> Bool in - - // check that we are handling notif for a downloadTask - guard let task = notif.userInfo?[Notification.Key.Task] as? URLSessionDownloadTask else { - return false - } - - response = task.response as? HTTPURLResponse - - // check that responseData are NOT set in userInfo-dict - notificationCalledWithNilResponseData = notif.userInfo?[Notification.Key.ResponseData] == nil - return notificationCalledWithNilResponseData - } - - // When - manager.download("https://httpbin.org/get").response { resp in } - - wait(for: [expectation], timeout: timeout) - - // Then - XCTAssertTrue(notificationCalledWithNilResponseData) - XCTAssertEqual(response?.statusCode, 200) + XCTAssertEqual(request?.response?.statusCode, 200) } } diff --git a/Tests/SessionManagerTests.swift b/Tests/SessionTests.swift similarity index 60% rename from Tests/SessionManagerTests.swift rename to Tests/SessionTests.swift index d7b507891..6d37df099 100644 --- a/Tests/SessionManagerTests.swift +++ b/Tests/SessionTests.swift @@ -1,5 +1,5 @@ // -// SessionManagerTests.swift +// SessionTests.swift // // Copyright (c) 2014-2018 Alamofire Software Foundation (http://alamofire.org/) // @@ -26,7 +26,7 @@ import Foundation import XCTest -class SessionManagerTestCase: BaseTestCase { +class SessionTestCase: BaseTestCase { // MARK: Helper Types @@ -39,13 +39,17 @@ class SessionManagerTestCase: BaseTestCase { self.throwsError = throwsError } - func adapt(_ urlRequest: URLRequest) throws -> URLRequest { - guard !throwsError else { throw AFError.invalidURL(url: "") } + func adapt(_ urlRequest: URLRequest, completion: @escaping (Result) -> Void) { + let result: Result = Result { + guard !throwsError else { throw AFError.invalidURL(url: "") } - var urlRequest = urlRequest - urlRequest.httpMethod = method.rawValue + var urlRequest = urlRequest + urlRequest.httpMethod = method.rawValue - return urlRequest + return urlRequest + } + + completion(result) } } @@ -57,26 +61,28 @@ class SessionManagerTestCase: BaseTestCase { var shouldApplyAuthorizationHeader = false var throwsErrorOnSecondAdapt = false - func adapt(_ urlRequest: URLRequest) throws -> URLRequest { - if throwsErrorOnSecondAdapt && adaptedCount == 1 { - throwsErrorOnSecondAdapt = false - throw AFError.invalidURL(url: "") - } + func adapt(_ urlRequest: URLRequest, completion: @escaping (Result) -> Void) { + let result: Result = Result { + if throwsErrorOnSecondAdapt && adaptedCount == 1 { + throwsErrorOnSecondAdapt = false + throw AFError.invalidURL(url: "") + } - var urlRequest = urlRequest + var urlRequest = urlRequest - adaptedCount += 1 + adaptedCount += 1 - if shouldApplyAuthorizationHeader && adaptedCount > 1 { - if let header = Request.authorizationHeader(user: "user", password: "password") { - urlRequest.setValue(header.value, forHTTPHeaderField: header.key) + if shouldApplyAuthorizationHeader && adaptedCount > 1 { + urlRequest.httpHeaders.update(.authorization(username: "user", password: "password")) } + + return urlRequest } - return urlRequest + completion(result) } - func should(_ manager: SessionManager, retry request: Request, with error: Error, completion: @escaping RequestRetryCompletion) { + func should(_ manager: Session, retry request: Request, with error: Error, completion: @escaping RequestRetryCompletion) { retryCount += 1 retryErrors.append(error) @@ -93,15 +99,19 @@ class SessionManagerTestCase: BaseTestCase { var retryCount = 0 var retryErrors: [Error] = [] - func adapt(_ urlRequest: URLRequest) throws -> URLRequest { - adaptedCount += 1 + func adapt(_ urlRequest: URLRequest, completion: @escaping (Result) -> Void) { + let result: Result = Result { + adaptedCount += 1 - if adaptedCount == 1 { throw AFError.invalidURL(url: "") } + if adaptedCount == 1 { throw AFError.invalidURL(url: "") } - return urlRequest + return urlRequest + } + + completion(result) } - func should(_ manager: SessionManager, retry request: Request, with error: Error, completion: @escaping RequestRetryCompletion) { + func should(_ manager: Session, retry request: Request, with error: Error, completion: @escaping RequestRetryCompletion) { retryCount += 1 retryErrors.append(error) @@ -113,110 +123,77 @@ class SessionManagerTestCase: BaseTestCase { func testInitializerWithDefaultArguments() { // Given, When - let manager = SessionManager() + let manager = Session() // Then XCTAssertNotNil(manager.session.delegate, "session delegate should not be nil") XCTAssertTrue(manager.delegate === manager.session.delegate, "manager delegate should equal session delegate") - XCTAssertNil(manager.session.serverTrustPolicyManager, "session server trust policy manager should be nil") + XCTAssertNil(manager.serverTrustManager, "session server trust policy manager should be nil") } func testInitializerWithSpecifiedArguments() { // Given let configuration = URLSessionConfiguration.default let delegate = SessionDelegate() - let serverTrustPolicyManager = ServerTrustPolicyManager(policies: [:]) + let serverTrustManager = ServerTrustManager(evaluators: [:]) // When - let manager = SessionManager( - configuration: configuration, - delegate: delegate, - serverTrustPolicyManager: serverTrustPolicyManager - ) + let manager = Session(configuration: configuration, + delegate: delegate, + serverTrustManager: serverTrustManager) // Then XCTAssertNotNil(manager.session.delegate, "session delegate should not be nil") XCTAssertTrue(manager.delegate === manager.session.delegate, "manager delegate should equal session delegate") - XCTAssertNotNil(manager.session.serverTrustPolicyManager, "session server trust policy manager should not be nil") - } - - func testThatFailableInitializerSucceedsWithDefaultArguments() { - // Given - let delegate = SessionDelegate() - let session: URLSession = { - let configuration = URLSessionConfiguration.default - return URLSession(configuration: configuration, delegate: delegate, delegateQueue: nil) - }() - - // When - let manager = SessionManager(session: session, delegate: delegate) - - // Then - if let manager = manager { - XCTAssertTrue(manager.delegate === manager.session.delegate, "manager delegate should equal session delegate") - XCTAssertNil(manager.session.serverTrustPolicyManager, "session server trust policy manager should be nil") - } else { - XCTFail("manager should not be nil") - } + XCTAssertNotNil(manager.serverTrustManager, "session server trust policy manager should not be nil") } - func testThatFailableInitializerSucceedsWithSpecifiedArguments() { + func testThatSessionInitializerSucceedsWithDefaultArguments() { // Given let delegate = SessionDelegate() + let underlyingQueue = DispatchQueue(label: "underlyingQueue") let session: URLSession = { let configuration = URLSessionConfiguration.default - return URLSession(configuration: configuration, delegate: delegate, delegateQueue: nil) + let queue = OperationQueue(underlyingQueue: underlyingQueue, name: "delegateQueue") + return URLSession(configuration: configuration, delegate: delegate, delegateQueue: queue) }() - let serverTrustPolicyManager = ServerTrustPolicyManager(policies: [:]) - // When - let manager = SessionManager(session: session, delegate: delegate, serverTrustPolicyManager: serverTrustPolicyManager) + let manager = Session(session: session, delegate: delegate, rootQueue: underlyingQueue) // Then - if let manager = manager { - XCTAssertTrue(manager.delegate === manager.session.delegate, "manager delegate should equal session delegate") - XCTAssertNotNil(manager.session.serverTrustPolicyManager, "session server trust policy manager should not be nil") - } else { - XCTFail("manager should not be nil") - } + XCTAssertTrue(manager.delegate === manager.session.delegate, "manager delegate should equal session delegate") + XCTAssertNil(manager.serverTrustManager, "session server trust policy manager should be nil") } - func testThatFailableInitializerFailsWithWhenDelegateDoesNotEqualSessionDelegate() { + func testThatSessionInitializerSucceedsWithSpecifiedArguments() { // Given let delegate = SessionDelegate() + let underlyingQueue = DispatchQueue(label: "underlyingQueue") let session: URLSession = { let configuration = URLSessionConfiguration.default - return URLSession(configuration: configuration, delegate: SessionDelegate(), delegateQueue: nil) + let queue = OperationQueue(underlyingQueue: underlyingQueue, name: "delegateQueue") + return URLSession(configuration: configuration, delegate: delegate, delegateQueue: queue) }() - // When - let manager = SessionManager(session: session, delegate: delegate) - - // Then - XCTAssertNil(manager, "manager should be nil") - } - - func testThatFailableInitializerFailsWhenSessionDelegateIsNil() { - // Given - let delegate = SessionDelegate() - let session: URLSession = { - let configuration = URLSessionConfiguration.default - return URLSession(configuration: configuration, delegate: nil, delegateQueue: nil) - }() + let serverTrustManager = ServerTrustManager(evaluators: [:]) // When - let manager = SessionManager(session: session, delegate: delegate) + let manager = Session(session: session, + delegate: delegate, + rootQueue: underlyingQueue, + serverTrustManager: serverTrustManager) // Then - XCTAssertNil(manager, "manager should be nil") + XCTAssertTrue(manager.delegate === manager.session.delegate, "manager delegate should equal session delegate") + XCTAssertNotNil(manager.serverTrustManager, "session server trust policy manager should not be nil") } // MARK: Tests - Default HTTP Headers func testDefaultUserAgentHeader() { // Given, When - let userAgent = SessionManager.defaultHTTPHeaders["User-Agent"] + let userAgent = HTTPHeaders.default["User-Agent"] // Then let osNameVersion: String = { @@ -231,7 +208,7 @@ class SessionManagerTestCase: BaseTestCase { #elseif os(tvOS) return "tvOS" #elseif os(macOS) - return "OS X" + return "macOS" #elseif os(Linux) return "Linux" #else @@ -244,7 +221,7 @@ class SessionManagerTestCase: BaseTestCase { let alamofireVersion: String = { guard - let afInfo = Bundle(for: SessionManager.self).infoDictionary, + let afInfo = Bundle(for: Session.self).infoDictionary, let build = afInfo["CFBundleShortVersionString"] else { return "Unknown" } @@ -255,12 +232,54 @@ class SessionManagerTestCase: BaseTestCase { XCTAssertEqual(userAgent, expectedUserAgent) } + // MARK: Tests - Supported Accept-Encodings + + func testDefaultAcceptEncodingSupportsAppropriateEncodingsOnAppropriateSystems() { + // Given + let brotliURL = URL(string: "https://httpbin.org/brotli")! + let gzipURL = URL(string: "https://httpbin.org/gzip")! + let deflateURL = URL(string: "https://httpbin.org/deflate")! + let brotliExpectation = expectation(description: "brotli request should complete") + let gzipExpectation = expectation(description: "gzip request should complete") + let deflateExpectation = expectation(description: "deflate request should complete") + var brotliResponse: DataResponse? + var gzipResponse: DataResponse? + var deflateResponse: DataResponse? + + // When + AF.request(brotliURL).responseJSON { (response) in + brotliResponse = response + brotliExpectation.fulfill() + } + + AF.request(gzipURL).responseJSON { (response) in + gzipResponse = response + gzipExpectation.fulfill() + } + + AF.request(deflateURL).responseJSON { (response) in + deflateResponse = response + deflateExpectation.fulfill() + } + + waitForExpectations(timeout: timeout, handler: nil) + + // Then + if #available(iOS 11.0, macOS 10.13, tvOS 11.0, watchOS 4.0, *) { + XCTAssertTrue(brotliResponse?.result.isSuccess == true) + } else { + XCTAssertTrue(brotliResponse?.result.isFailure == true) + } + + XCTAssertTrue(gzipResponse?.result.isSuccess == true) + XCTAssertTrue(deflateResponse?.result.isSuccess == true) + } + // MARK: Tests - Start Requests Immediately func testSetStartRequestsImmediatelyToFalseAndResumeRequest() { // Given - let manager = SessionManager() - manager.startRequestsImmediately = false + let manager = Session(startRequestsImmediately: false) let url = URL(string: "https://httpbin.org/get")! let urlRequest = URLRequest(url: url) @@ -284,12 +303,111 @@ class SessionManagerTestCase: BaseTestCase { XCTAssertTrue(response?.statusCode == 200, "response status code should be 200") } + func testSetStartRequestsImmediatelyToFalseAndCancelledCallsResponseHandlers() { + // Given + let manager = Session(startRequestsImmediately: false) + + let url = URL(string: "https://httpbin.org/get")! + let urlRequest = URLRequest(url: url) + + let expectation = self.expectation(description: "\(url)") + + var response: DataResponse? + + // When + let request = manager.request(urlRequest) + .cancel() + .response { resp in + response = resp + expectation.fulfill() + } + + waitForExpectations(timeout: timeout, handler: nil) + + // Then + XCTAssertNotNil(response, "response should not be nil") + XCTAssertTrue(request.isCancelled) + XCTAssertTrue((request.task == nil) || (request.task?.state == .canceling || request.task?.state == .completed)) + guard let error = request.error?.asAFError, case .explicitlyCancelled = error else { + XCTFail("Request should have an .explicitlyCancelled error.") + return + } + } + + func testSetStartRequestsImmediatelyToFalseAndResumeThenCancelRequestHasCorrectOutput() { + // Given + let manager = Session(startRequestsImmediately: false) + + let url = URL(string: "https://httpbin.org/get")! + let urlRequest = URLRequest(url: url) + + let expectation = self.expectation(description: "\(url)") + + var response: DataResponse? + + // When + let request = manager.request(urlRequest) + .resume() + .cancel() + .response { resp in + response = resp + expectation.fulfill() + } + + + waitForExpectations(timeout: timeout, handler: nil) + + // Then + XCTAssertNotNil(response, "response should not be nil") + XCTAssertTrue(request.isCancelled) + XCTAssertTrue((request.task == nil) || (request.task?.state == .canceling || request.task?.state == .completed)) + guard let error = request.error?.asAFError, case .explicitlyCancelled = error else { + XCTFail("Request should have an .explicitlyCancelled error.") + return + } + } + + func testSetStartRequestsImmediatelyToFalseAndCancelThenResumeRequestDoesntCreateTaskAndStaysCancelled() { + // Given + let manager = Session(startRequestsImmediately: false) + + let url = URL(string: "https://httpbin.org/get")! + let urlRequest = URLRequest(url: url) + + let expectation = self.expectation(description: "\(url)") + + var response: DataResponse? + + // When + let request = manager.request(urlRequest) + .cancel() + .resume() + .response { resp in + response = resp + expectation.fulfill() + } + + + waitForExpectations(timeout: timeout, handler: nil) + + // Then + XCTAssertNotNil(response, "response should not be nil") + XCTAssertTrue(request.isCancelled) + XCTAssertTrue((request.task == nil) || (request.task?.state == .canceling || request.task?.state == .completed)) + guard let error = request.error?.asAFError, case .explicitlyCancelled = error else { + XCTFail("Request should have an .explicitlyCancelled error.") + return + } + } + // MARK: Tests - Deinitialization func testReleasingManagerWithPendingRequestDeinitializesSuccessfully() { // Given - var manager: SessionManager? = SessionManager() - manager?.startRequestsImmediately = false + let monitor = ClosureEventMonitor() + let expectation = self.expectation(description: "Request created") + monitor.requestDidCreateTask = { (_, _) in expectation.fulfill() } + var manager: Session? = Session(startRequestsImmediately: false, eventMonitors: [monitor]) let url = URL(string: "https://httpbin.org/get")! let urlRequest = URLRequest(url: url) @@ -298,27 +416,28 @@ class SessionManagerTestCase: BaseTestCase { let request = manager?.request(urlRequest) manager = nil + waitForExpectations(timeout: timeout, handler: nil) + // Then - XCTAssertTrue(request?.task?.state == .suspended, "request task state should be '.Suspended'") + XCTAssertEqual(request?.task?.state, .suspended) XCTAssertNil(manager, "manager should be nil") } func testReleasingManagerWithPendingCanceledRequestDeinitializesSuccessfully() { // Given - var manager: SessionManager? = SessionManager() - manager!.startRequestsImmediately = false + var manager: Session? = Session(startRequestsImmediately: false) let url = URL(string: "https://httpbin.org/get")! let urlRequest = URLRequest(url: url) // When - let request = manager!.request(urlRequest) - request.cancel() + let request = manager?.request(urlRequest) + request?.cancel() manager = nil // Then - let state = request.task?.state - XCTAssertTrue(state == .canceling || state == .completed, "state should be .Canceling or .Completed") + let state = request?.state + XCTAssertTrue(state == .cancelled, "state should be .cancelled") XCTAssertNil(manager, "manager should be nil") } @@ -326,10 +445,10 @@ class SessionManagerTestCase: BaseTestCase { func testThatDataRequestWithInvalidURLStringThrowsResponseHandlerError() { // Given - let sessionManager = SessionManager() + let sessionManager = Session() let expectation = self.expectation(description: "Request should fail with error") - var response: DefaultDataResponse? + var response: DataResponse? // When sessionManager.request("https://httpbin.org/get/äëïöü").response { resp in @@ -342,11 +461,10 @@ class SessionManagerTestCase: BaseTestCase { // Then XCTAssertNil(response?.request) XCTAssertNil(response?.response) - XCTAssertNotNil(response?.data) - XCTAssertEqual(response?.data?.count, 0) + XCTAssertNil(response?.data) XCTAssertNotNil(response?.error) - if let error = response?.error as? AFError { + if let error = response?.error?.asAFError { XCTAssertTrue(error.isInvalidURLError) XCTAssertEqual(error.urlConvertible as? String, "https://httpbin.org/get/äëïöü") } else { @@ -356,10 +474,10 @@ class SessionManagerTestCase: BaseTestCase { func testThatDownloadRequestWithInvalidURLStringThrowsResponseHandlerError() { // Given - let sessionManager = SessionManager() + let sessionManager = Session() let expectation = self.expectation(description: "Download should fail with error") - var response: DefaultDownloadResponse? + var response: DownloadResponse? // When sessionManager.download("https://httpbin.org/get/äëïöü").response { resp in @@ -372,12 +490,11 @@ class SessionManagerTestCase: BaseTestCase { // Then XCTAssertNil(response?.request) XCTAssertNil(response?.response) - XCTAssertNil(response?.temporaryURL) - XCTAssertNil(response?.destinationURL) + XCTAssertNil(response?.fileURL) XCTAssertNil(response?.resumeData) XCTAssertNotNil(response?.error) - if let error = response?.error as? AFError { + if let error = response?.error?.asAFError { XCTAssertTrue(error.isInvalidURLError) XCTAssertEqual(error.urlConvertible as? String, "https://httpbin.org/get/äëïöü") } else { @@ -387,10 +504,10 @@ class SessionManagerTestCase: BaseTestCase { func testThatUploadDataRequestWithInvalidURLStringThrowsResponseHandlerError() { // Given - let sessionManager = SessionManager() + let sessionManager = Session() let expectation = self.expectation(description: "Upload should fail with error") - var response: DefaultDataResponse? + var response: DataResponse? // When sessionManager.upload(Data(), to: "https://httpbin.org/get/äëïöü").response { resp in @@ -403,11 +520,10 @@ class SessionManagerTestCase: BaseTestCase { // Then XCTAssertNil(response?.request) XCTAssertNil(response?.response) - XCTAssertNotNil(response?.data) - XCTAssertEqual(response?.data?.count, 0) + XCTAssertNil(response?.data) XCTAssertNotNil(response?.error) - if let error = response?.error as? AFError { + if let error = response?.error?.asAFError { XCTAssertTrue(error.isInvalidURLError) XCTAssertEqual(error.urlConvertible as? String, "https://httpbin.org/get/äëïöü") } else { @@ -417,10 +533,10 @@ class SessionManagerTestCase: BaseTestCase { func testThatUploadFileRequestWithInvalidURLStringThrowsResponseHandlerError() { // Given - let sessionManager = SessionManager() + let sessionManager = Session() let expectation = self.expectation(description: "Upload should fail with error") - var response: DefaultDataResponse? + var response: DataResponse? // When sessionManager.upload(URL(fileURLWithPath: "/invalid"), to: "https://httpbin.org/get/äëïöü").response { resp in @@ -433,11 +549,10 @@ class SessionManagerTestCase: BaseTestCase { // Then XCTAssertNil(response?.request) XCTAssertNil(response?.response) - XCTAssertNotNil(response?.data) - XCTAssertEqual(response?.data?.count, 0) + XCTAssertNil(response?.data) XCTAssertNotNil(response?.error) - if let error = response?.error as? AFError { + if let error = response?.error?.asAFError { XCTAssertTrue(error.isInvalidURLError) XCTAssertEqual(error.urlConvertible as? String, "https://httpbin.org/get/äëïöü") } else { @@ -447,10 +562,10 @@ class SessionManagerTestCase: BaseTestCase { func testThatUploadStreamRequestWithInvalidURLStringThrowsResponseHandlerError() { // Given - let sessionManager = SessionManager() + let sessionManager = Session() let expectation = self.expectation(description: "Upload should fail with error") - var response: DefaultDataResponse? + var response: DataResponse? // When sessionManager.upload(InputStream(data: Data()), to: "https://httpbin.org/get/äëïöü").response { resp in @@ -463,11 +578,10 @@ class SessionManagerTestCase: BaseTestCase { // Then XCTAssertNil(response?.request) XCTAssertNil(response?.response) - XCTAssertNotNil(response?.data) - XCTAssertEqual(response?.data?.count, 0) + XCTAssertNil(response?.data) XCTAssertNotNil(response?.error) - if let error = response?.error as? AFError { + if let error = response?.error?.asAFError { XCTAssertTrue(error.isInvalidURLError) XCTAssertEqual(error.urlConvertible as? String, "https://httpbin.org/get/äëïöü") } else { @@ -480,14 +594,16 @@ class SessionManagerTestCase: BaseTestCase { func testThatSessionManagerCallsRequestAdapterWhenCreatingDataRequest() { // Given let adapter = HTTPMethodAdapter(method: .post) - - let sessionManager = SessionManager() - sessionManager.adapter = adapter - sessionManager.startRequestsImmediately = false + let monitor = ClosureEventMonitor() + let expectation = self.expectation(description: "Request created") + monitor.requestDidCreateTask = { (_, _) in expectation.fulfill() } + let sessionManager = Session(startRequestsImmediately: false, adapter: adapter, eventMonitors: [monitor]) // When let request = sessionManager.request("https://httpbin.org/get") + waitForExpectations(timeout: timeout, handler: nil) + // Then XCTAssertEqual(request.task?.originalRequest?.httpMethod, adapter.method.rawValue) } @@ -495,14 +611,15 @@ class SessionManagerTestCase: BaseTestCase { func testThatSessionManagerCallsRequestAdapterWhenCreatingDownloadRequest() { // Given let adapter = HTTPMethodAdapter(method: .post) - - let sessionManager = SessionManager() - sessionManager.adapter = adapter - sessionManager.startRequestsImmediately = false + let monitor = ClosureEventMonitor() + let expectation = self.expectation(description: "Request created") + monitor.requestDidCreateTask = { (_, _) in expectation.fulfill() } + let sessionManager = Session(startRequestsImmediately: false, adapter: adapter, eventMonitors: [monitor]) // When - let destination = DownloadRequest.suggestedDownloadDestination() - let request = sessionManager.download("https://httpbin.org/get", to: destination) + let request = sessionManager.download("https://httpbin.org/get") + + waitForExpectations(timeout: timeout, handler: nil) // Then XCTAssertEqual(request.task?.originalRequest?.httpMethod, adapter.method.rawValue) @@ -512,12 +629,15 @@ class SessionManagerTestCase: BaseTestCase { // Given let adapter = HTTPMethodAdapter(method: .get) - let sessionManager = SessionManager() - sessionManager.adapter = adapter - sessionManager.startRequestsImmediately = false + let monitor = ClosureEventMonitor() + let expectation = self.expectation(description: "Request created") + monitor.requestDidCreateTask = { (_, _) in expectation.fulfill() } + let sessionManager = Session(startRequestsImmediately: false, adapter: adapter, eventMonitors: [monitor]) // When - let request = sessionManager.upload("data".data(using: .utf8)!, to: "https://httpbin.org/post") + let request = sessionManager.upload(Data("data".utf8), to: "https://httpbin.org/post") + + waitForExpectations(timeout: timeout, handler: nil) // Then XCTAssertEqual(request.task?.originalRequest?.httpMethod, adapter.method.rawValue) @@ -527,14 +647,17 @@ class SessionManagerTestCase: BaseTestCase { // Given let adapter = HTTPMethodAdapter(method: .get) - let sessionManager = SessionManager() - sessionManager.adapter = adapter - sessionManager.startRequestsImmediately = false + let monitor = ClosureEventMonitor() + let expectation = self.expectation(description: "Request created") + monitor.requestDidCreateTask = { (_, _) in expectation.fulfill() } + let sessionManager = Session(startRequestsImmediately: false, adapter: adapter, eventMonitors: [monitor]) // When let fileURL = URL(fileURLWithPath: "/path/to/some/file.txt") let request = sessionManager.upload(fileURL, to: "https://httpbin.org/post") + waitForExpectations(timeout: timeout, handler: nil) + // Then XCTAssertEqual(request.task?.originalRequest?.httpMethod, adapter.method.rawValue) } @@ -543,14 +666,17 @@ class SessionManagerTestCase: BaseTestCase { // Given let adapter = HTTPMethodAdapter(method: .get) - let sessionManager = SessionManager() - sessionManager.adapter = adapter - sessionManager.startRequestsImmediately = false + let monitor = ClosureEventMonitor() + let expectation = self.expectation(description: "Request created") + monitor.requestDidCreateTask = { (_, _) in expectation.fulfill() } + let sessionManager = Session(startRequestsImmediately: false, adapter: adapter, eventMonitors: [monitor]) // When - let inputStream = InputStream(data: "data".data(using: .utf8)!) + let inputStream = InputStream(data: Data("data".utf8)) let request = sessionManager.upload(inputStream, to: "https://httpbin.org/post") + waitForExpectations(timeout: timeout, handler: nil) + // Then XCTAssertEqual(request.task?.originalRequest?.httpMethod, adapter.method.rawValue) } @@ -559,15 +685,18 @@ class SessionManagerTestCase: BaseTestCase { // Given let adapter = HTTPMethodAdapter(method: .post, throwsError: true) - let sessionManager = SessionManager() - sessionManager.adapter = adapter - sessionManager.startRequestsImmediately = false + let monitor = ClosureEventMonitor() + let expectation = self.expectation(description: "Request created") + monitor.requestDidFailToAdaptURLRequestWithError = { (_, _, _) in expectation.fulfill() } + let sessionManager = Session(startRequestsImmediately: false, adapter: adapter, eventMonitors: [monitor]) // When let request = sessionManager.request("https://httpbin.org/get") + waitForExpectations(timeout: timeout, handler: nil) + // Then - if let error = request.delegate.error as? AFError { + if let error = request.error?.asAFError { XCTAssertTrue(error.isInvalidURLError) XCTAssertEqual(error.urlConvertible as? String, "") } else { @@ -581,9 +710,7 @@ class SessionManagerTestCase: BaseTestCase { // Given let handler = RequestHandler() - let sessionManager = SessionManager() - sessionManager.adapter = handler - sessionManager.retrier = handler + let sessionManager = Session(adapter: handler, retrier: handler) let expectation = self.expectation(description: "request should eventually fail") var response: DataResponse? @@ -603,7 +730,7 @@ class SessionManagerTestCase: BaseTestCase { XCTAssertEqual(handler.retryCount, 2) XCTAssertEqual(request.retryCount, 1) XCTAssertEqual(response?.result.isSuccess, false) - XCTAssertTrue(sessionManager.delegate.requests.isEmpty) + XCTAssertTrue(sessionManager.requestTaskMap.isEmpty) } func testThatSessionManagerCallsRequestRetrierWhenRequestInitiallyEncountersAdaptError() { @@ -613,9 +740,7 @@ class SessionManagerTestCase: BaseTestCase { handler.throwsErrorOnSecondAdapt = true handler.shouldApplyAuthorizationHeader = true - let sessionManager = SessionManager() - sessionManager.adapter = handler - sessionManager.retrier = handler + let sessionManager = Session(adapter: handler, retrier: handler) let expectation = self.expectation(description: "request should eventually fail") var response: DataResponse? @@ -634,9 +759,7 @@ class SessionManagerTestCase: BaseTestCase { XCTAssertEqual(handler.adaptedCount, 2) XCTAssertEqual(handler.retryCount, 1) XCTAssertEqual(response?.result.isSuccess, true) - XCTAssertTrue(sessionManager.delegate.requests.isEmpty) - - handler.retryErrors.forEach { XCTAssertFalse($0 is AdaptError) } + XCTAssertTrue(sessionManager.requestTaskMap.isEmpty) } func testThatSessionManagerCallsRequestRetrierWhenDownloadInitiallyEncountersAdaptError() { @@ -646,14 +769,12 @@ class SessionManagerTestCase: BaseTestCase { handler.throwsErrorOnSecondAdapt = true handler.shouldApplyAuthorizationHeader = true - let sessionManager = SessionManager() - sessionManager.adapter = handler - sessionManager.retrier = handler + let sessionManager = Session(adapter: handler, retrier: handler) let expectation = self.expectation(description: "request should eventually fail") var response: DownloadResponse? - let destination: DownloadRequest.DownloadFileDestination = { _, _ in + let destination: DownloadRequest.Destination = { _, _ in let fileURL = self.testDirectoryURL.appendingPathComponent("test-output.json") return (fileURL, [.removePreviousFile]) } @@ -672,23 +793,19 @@ class SessionManagerTestCase: BaseTestCase { XCTAssertEqual(handler.adaptedCount, 2) XCTAssertEqual(handler.retryCount, 1) XCTAssertEqual(response?.result.isSuccess, true) - XCTAssertTrue(sessionManager.delegate.requests.isEmpty) - - handler.retryErrors.forEach { XCTAssertFalse($0 is AdaptError) } + XCTAssertTrue(sessionManager.requestTaskMap.isEmpty) } func testThatSessionManagerCallsRequestRetrierWhenUploadInitiallyEncountersAdaptError() { // Given let handler = UploadHandler() - let sessionManager = SessionManager() - sessionManager.adapter = handler - sessionManager.retrier = handler + let sessionManager = Session(adapter: handler, retrier: handler) let expectation = self.expectation(description: "request should eventually fail") var response: DataResponse? - let uploadData = "upload data".data(using: .utf8, allowLossyConversion: false)! + let uploadData = Data("upload data".utf8) // When sessionManager.upload(uploadData, to: "https://httpbin.org/post") @@ -704,9 +821,7 @@ class SessionManagerTestCase: BaseTestCase { XCTAssertEqual(handler.adaptedCount, 2) XCTAssertEqual(handler.retryCount, 1) XCTAssertEqual(response?.result.isSuccess, true) - XCTAssertTrue(sessionManager.delegate.requests.isEmpty) - - handler.retryErrors.forEach { XCTAssertFalse($0 is AdaptError) } + XCTAssertTrue(sessionManager.requestTaskMap.isEmpty) } func testThatSessionManagerCallsAdapterWhenRequestIsRetried() { @@ -714,11 +829,9 @@ class SessionManagerTestCase: BaseTestCase { let handler = RequestHandler() handler.shouldApplyAuthorizationHeader = true - let sessionManager = SessionManager() - sessionManager.adapter = handler - sessionManager.retrier = handler + let sessionManager = Session(adapter: handler, retrier: handler) - let expectation = self.expectation(description: "request should eventually fail") + let expectation = self.expectation(description: "request should eventually succeed") var response: DataResponse? // When @@ -736,17 +849,15 @@ class SessionManagerTestCase: BaseTestCase { XCTAssertEqual(handler.retryCount, 1) XCTAssertEqual(request.retryCount, 1) XCTAssertEqual(response?.result.isSuccess, true) - XCTAssertTrue(sessionManager.delegate.requests.isEmpty) + XCTAssertTrue(sessionManager.requestTaskMap.isEmpty) } - + // TODO: Confirm retry logic. func testThatRequestAdapterErrorThrowsResponseHandlerErrorWhenRequestIsRetried() { // Given let handler = RequestHandler() handler.throwsErrorOnSecondAdapt = true - let sessionManager = SessionManager() - sessionManager.adapter = handler - sessionManager.retrier = handler + let sessionManager = Session(adapter: handler, retrier: handler) let expectation = self.expectation(description: "request should eventually fail") var response: DataResponse? @@ -763,12 +874,12 @@ class SessionManagerTestCase: BaseTestCase { // Then XCTAssertEqual(handler.adaptedCount, 1) - XCTAssertEqual(handler.retryCount, 1) - XCTAssertEqual(request.retryCount, 0) + XCTAssertEqual(handler.retryCount, 2) + XCTAssertEqual(request.retryCount, 1) XCTAssertEqual(response?.result.isSuccess, false) - XCTAssertTrue(sessionManager.delegate.requests.isEmpty) + XCTAssertTrue(sessionManager.requestTaskMap.isEmpty) - if let error = response?.result.error as? AFError { + if let error = response?.result.error?.asAFError { XCTAssertTrue(error.isInvalidURLError) XCTAssertEqual(error.urlConvertible as? String, "") } else { @@ -802,7 +913,7 @@ class SessionManagerConfigurationHeadersTestCase: BaseTestCase { private func executeAuthorizationHeaderTest(for type: ConfigurationType) { // Given - let manager: SessionManager = { + let manager: Session = { let configuration: URLSessionConfiguration = { let configuration: URLSessionConfiguration @@ -816,14 +927,14 @@ class SessionManagerConfigurationHeadersTestCase: BaseTestCase { configuration = .background(withIdentifier: identifier) } - var headers = SessionManager.defaultHTTPHeaders + var headers = HTTPHeaders.default headers["Authorization"] = "Bearer 123456" - configuration.httpAdditionalHeaders = headers + configuration.httpHeaders = headers return configuration }() - return SessionManager(configuration: configuration) + return Session(configuration: configuration) }() let expectation = self.expectation(description: "request should complete successfully") diff --git a/Tests/TLSEvaluationTests.swift b/Tests/TLSEvaluationTests.swift index dfb450f59..d71b5a43d 100644 --- a/Tests/TLSEvaluationTests.swift +++ b/Tests/TLSEvaluationTests.swift @@ -27,14 +27,14 @@ import Foundation import XCTest private struct TestCertificates { - static let rootCA = TestCertificates.certificate(withFileName: "expired.badssl.com-root-ca") - static let intermediateCA1 = TestCertificates.certificate(withFileName: "expired.badssl.com-intermediate-ca-1") - static let intermediateCA2 = TestCertificates.certificate(withFileName: "expired.badssl.com-intermediate-ca-2") - static let leaf = TestCertificates.certificate(withFileName: "expired.badssl.com-leaf") - - static func certificate(withFileName fileName: String) -> SecCertificate { - class Locater {} - let filePath = Bundle(for: Locater.self).path(forResource: fileName, ofType: "cer")! + static let rootCA = TestCertificates.certificate(filename: "expired.badssl.com-root-ca") + static let intermediateCA1 = TestCertificates.certificate(filename: "expired.badssl.com-intermediate-ca-1") + static let intermediateCA2 = TestCertificates.certificate(filename: "expired.badssl.com-intermediate-ca-2") + static let leaf = TestCertificates.certificate(filename: "expired.badssl.com-leaf") + + static func certificate(filename: String) -> SecCertificate { + class Locator {} + let filePath = Bundle(for: Locator.self).path(forResource: filename, ofType: "cer")! let data = try! Data(contentsOf: URL(fileURLWithPath: filePath)) let certificate = SecCertificateCreateWithData(nil, data as CFData)! @@ -44,25 +44,6 @@ private struct TestCertificates { // MARK: - -private struct TestPublicKeys { - static let rootCA = TestPublicKeys.publicKey(for: TestCertificates.rootCA) - static let intermediateCA1 = TestPublicKeys.publicKey(for: TestCertificates.intermediateCA1) - static let intermediateCA2 = TestPublicKeys.publicKey(for: TestCertificates.intermediateCA2) - static let leaf = TestPublicKeys.publicKey(for: TestCertificates.leaf) - - static func publicKey(for certificate: SecCertificate) -> SecKey { - let policy = SecPolicyCreateBasicX509() - var trust: SecTrust? - SecTrustCreateWithCertificates(certificate, policy, &trust) - - let publicKey = SecTrustCopyPublicKey(trust!)! - - return publicKey - } -} - -// MARK: - - class TLSEvaluationExpiredLeafCertificateTestCase: BaseTestCase { private let expiredURLString = "https://expired.badssl.com/" private let expiredHost = "expired.badssl.com" @@ -85,14 +66,9 @@ class TLSEvaluationExpiredLeafCertificateTestCase: BaseTestCase { // MARK: Default Behavior Tests func testThatExpiredCertificateRequestFailsWithNoServerTrustPolicy() { - // On iOS 8.0 - 8.4, this test passes by itself, but fails for no explanable reason when run with the rest of - // the suite. Because of this, there's no reliable way to run all these tests together pre iOS 9, so let's - // disable this one when run against the entire test suite. - guard #available(iOS 9.0, *) else { return } - // Given let expectation = self.expectation(description: "\(expiredURLString)") - let manager = SessionManager(configuration: configuration) + let manager = Session(configuration: configuration) var error: Error? // When @@ -123,7 +99,7 @@ class TLSEvaluationExpiredLeafCertificateTestCase: BaseTestCase { // Given let expectation = self.expectation(description: "\(revokedURLString)") - let manager = SessionManager(configuration: configuration) + let manager = Session(configuration: configuration) var error: Error? @@ -149,10 +125,10 @@ class TLSEvaluationExpiredLeafCertificateTestCase: BaseTestCase { func testThatExpiredCertificateRequestFailsWithDefaultServerTrustPolicy() { // Given - let policies = [expiredHost: ServerTrustPolicy.performDefaultEvaluation(validateHost: true)] - let manager = SessionManager( + let evaluators = [expiredHost: DefaultTrustEvaluator(validateHost: true)] + let manager = Session( configuration: configuration, - serverTrustPolicyManager: ServerTrustPolicyManager(policies: policies) + serverTrustManager: ServerTrustManager(evaluators: evaluators) ) let expectation = self.expectation(description: "\(expiredURLString)") @@ -170,10 +146,10 @@ class TLSEvaluationExpiredLeafCertificateTestCase: BaseTestCase { // Then XCTAssertNotNil(error, "error should not be nil") - if let error = error as? URLError { - XCTAssertEqual(error.code, .cancelled, "code should be cancelled") + if let error = error?.asAFError { + XCTAssertTrue(error.isServerTrustEvaluationError, "should be .certificatePinningFailed") } else { - XCTFail("error should be an URLError") + XCTFail("error should be an AFError") } } @@ -182,12 +158,12 @@ class TLSEvaluationExpiredLeafCertificateTestCase: BaseTestCase { // test is left for debugging purposes only. Should not be committed into the test suite while enabled. // Given - let defaultPolicy = ServerTrustPolicy.performDefaultEvaluation(validateHost: true) - let policies = [revokedHost: defaultPolicy] + let defaultPolicy = DefaultTrustEvaluator() + let evaluators = [revokedHost: defaultPolicy] - let manager = SessionManager( + let manager = Session( configuration: configuration, - serverTrustPolicyManager: ServerTrustPolicyManager(policies: policies) + serverTrustManager: ServerTrustManager(evaluators: evaluators) ) let expectation = self.expectation(description: "\(revokedURLString)") @@ -215,16 +191,13 @@ class TLSEvaluationExpiredLeafCertificateTestCase: BaseTestCase { func testThatExpiredCertificateRequestFailsWithRevokedServerTrustPolicy() { // Given - let policy = ServerTrustPolicy.performRevokedEvaluation( - validateHost: true, - revocationFlags: kSecRevocationUseAnyAvailableMethod - ) + let policy = RevocationTrustEvaluator() - let policies = [expiredHost: policy] + let evaluators = [expiredHost: policy] - let manager = SessionManager( + let manager = Session( configuration: configuration, - serverTrustPolicyManager: ServerTrustPolicyManager(policies: policies) + serverTrustManager: ServerTrustManager(evaluators: evaluators) ) let expectation = self.expectation(description: "\(expiredURLString)") @@ -242,25 +215,22 @@ class TLSEvaluationExpiredLeafCertificateTestCase: BaseTestCase { // Then XCTAssertNotNil(error, "error should not be nil") - if let error = error as? URLError { - XCTAssertEqual(error.code, .cancelled, "code should be cancelled") + if let error = error?.asAFError { + XCTAssertTrue(error.isServerTrustEvaluationError, "should be .certificatePinningFailed") } else { - XCTFail("error should be an URLError") + XCTFail("error should be an AFError") } } func testThatRevokedCertificateRequestFailsWithRevokedServerTrustPolicy() { // Given - let policy = ServerTrustPolicy.performRevokedEvaluation( - validateHost: true, - revocationFlags: kSecRevocationUseAnyAvailableMethod - ) + let policy = RevocationTrustEvaluator() - let policies = [revokedHost: policy] + let evaluators = [revokedHost: policy] - let manager = SessionManager( + let manager = Session( configuration: configuration, - serverTrustPolicyManager: ServerTrustPolicyManager(policies: policies) + serverTrustManager: ServerTrustManager(evaluators: evaluators) ) let expectation = self.expectation(description: "\(revokedURLString)") @@ -278,10 +248,10 @@ class TLSEvaluationExpiredLeafCertificateTestCase: BaseTestCase { // Then XCTAssertNotNil(error, "error should not be nil") - if let error = error as? URLError { - XCTAssertEqual(error.code, .cancelled, "code should be cancelled") + if let error = error?.asAFError { + XCTAssertTrue(error.isServerTrustEvaluationError, "should be .certificatePinningFailed") } else { - XCTFail("error should be an URLError") + XCTFail("error should be an AFError") } } @@ -290,13 +260,13 @@ class TLSEvaluationExpiredLeafCertificateTestCase: BaseTestCase { func testThatExpiredCertificateRequestFailsWhenPinningLeafCertificateWithCertificateChainValidation() { // Given let certificates = [TestCertificates.leaf] - let policies: [String: ServerTrustPolicy] = [ - expiredHost: .pinCertificates(certificates: certificates, validateCertificateChain: true, validateHost: true) + let evaluators = [ + expiredHost: PinnedCertificatesTrustEvaluator(certificates: certificates) ] - let manager = SessionManager( + let manager = Session( configuration: configuration, - serverTrustPolicyManager: ServerTrustPolicyManager(policies: policies) + serverTrustManager: ServerTrustManager(evaluators: evaluators) ) let expectation = self.expectation(description: "\(expiredURLString)") @@ -314,10 +284,10 @@ class TLSEvaluationExpiredLeafCertificateTestCase: BaseTestCase { // Then XCTAssertNotNil(error, "error should not be nil") - if let error = error as? URLError { - XCTAssertEqual(error.code, .cancelled, "code should be cancelled") + if let error = error?.asAFError { + XCTAssertTrue(error.isServerTrustEvaluationError, "should be .certificatePinningFailed") } else { - XCTFail("error should be an URLError") + XCTFail("error should be an AFError") } } @@ -330,13 +300,13 @@ class TLSEvaluationExpiredLeafCertificateTestCase: BaseTestCase { TestCertificates.rootCA ] - let policies: [String: ServerTrustPolicy] = [ - expiredHost: .pinCertificates(certificates: certificates, validateCertificateChain: true, validateHost: true) + let evaluators = [ + expiredHost: PinnedCertificatesTrustEvaluator(certificates: certificates) ] - let manager = SessionManager( + let manager = Session( configuration: configuration, - serverTrustPolicyManager: ServerTrustPolicyManager(policies: policies) + serverTrustManager: ServerTrustManager(evaluators: evaluators) ) let expectation = self.expectation(description: "\(expiredURLString)") @@ -354,23 +324,23 @@ class TLSEvaluationExpiredLeafCertificateTestCase: BaseTestCase { // Then XCTAssertNotNil(error, "error should not be nil") - if let error = error as? URLError { - XCTAssertEqual(error.code, .cancelled, "code should be cancelled") + if let error = error?.asAFError { + XCTAssertTrue(error.isServerTrustEvaluationError, "should be .certificatePinningFailed") } else { - XCTFail("error should be an URLError") + XCTFail("error should be an AFError") } } - func testThatExpiredCertificateRequestSucceedsWhenPinningLeafCertificateWithoutCertificateChainValidation() { + func testThatExpiredCertificateRequestSucceedsWhenPinningLeafCertificateWithoutCertificateChainOrHostValidation() { // Given let certificates = [TestCertificates.leaf] - let policies: [String: ServerTrustPolicy] = [ - expiredHost: .pinCertificates(certificates: certificates, validateCertificateChain: false, validateHost: true) + let evaluators = [ + expiredHost: PinnedCertificatesTrustEvaluator(certificates: certificates, performDefaultValidation: false, validateHost: false) ] - let manager = SessionManager( + let manager = Session( configuration: configuration, - serverTrustPolicyManager: ServerTrustPolicyManager(policies: policies) + serverTrustManager: ServerTrustManager(evaluators: evaluators) ) let expectation = self.expectation(description: "\(expiredURLString)") @@ -389,16 +359,16 @@ class TLSEvaluationExpiredLeafCertificateTestCase: BaseTestCase { XCTAssertNil(error, "error should be nil") } - func testThatExpiredCertificateRequestSucceedsWhenPinningIntermediateCACertificateWithoutCertificateChainValidation() { + func testThatExpiredCertificateRequestSucceedsWhenPinningIntermediateCACertificateWithoutCertificateChainOrHostValidation() { // Given let certificates = [TestCertificates.intermediateCA2] - let policies: [String: ServerTrustPolicy] = [ - expiredHost: .pinCertificates(certificates: certificates, validateCertificateChain: false, validateHost: true) + let evaluators = [ + expiredHost: PinnedCertificatesTrustEvaluator(certificates: certificates, performDefaultValidation: false, validateHost: false) ] - let manager = SessionManager( + let manager = Session( configuration: configuration, - serverTrustPolicyManager: ServerTrustPolicyManager(policies: policies) + serverTrustManager: ServerTrustManager(evaluators: evaluators) ) let expectation = self.expectation(description: "\(expiredURLString)") @@ -420,13 +390,13 @@ class TLSEvaluationExpiredLeafCertificateTestCase: BaseTestCase { func testThatExpiredCertificateRequestSucceedsWhenPinningRootCACertificateWithoutCertificateChainValidation() { // Given let certificates = [TestCertificates.rootCA] - let policies: [String: ServerTrustPolicy] = [ - expiredHost: .pinCertificates(certificates: certificates, validateCertificateChain: false, validateHost: true) + let evaluators = [ + expiredHost: PinnedCertificatesTrustEvaluator(certificates: certificates, performDefaultValidation: false) ] - let manager = SessionManager( + let manager = Session( configuration: configuration, - serverTrustPolicyManager: ServerTrustPolicyManager(policies: policies) + serverTrustManager: ServerTrustManager(evaluators: evaluators) ) let expectation = self.expectation(description: "\(expiredURLString)") @@ -453,14 +423,14 @@ class TLSEvaluationExpiredLeafCertificateTestCase: BaseTestCase { func testThatExpiredCertificateRequestFailsWhenPinningLeafPublicKeyWithCertificateChainValidation() { // Given - let publicKeys = [TestPublicKeys.leaf] - let policies: [String: ServerTrustPolicy] = [ - expiredHost: .pinPublicKeys(publicKeys: publicKeys, validateCertificateChain: true, validateHost: true) + let keys = [TestCertificates.leaf].publicKeys + let evaluators = [ + expiredHost: PublicKeysTrustEvaluator(keys: keys) ] - let manager = SessionManager( + let manager = Session( configuration: configuration, - serverTrustPolicyManager: ServerTrustPolicyManager(policies: policies) + serverTrustManager: ServerTrustManager(evaluators: evaluators) ) let expectation = self.expectation(description: "\(expiredURLString)") @@ -478,23 +448,23 @@ class TLSEvaluationExpiredLeafCertificateTestCase: BaseTestCase { // Then XCTAssertNotNil(error, "error should not be nil") - if let error = error as? URLError { - XCTAssertEqual(error.code, .cancelled, "code should be cancelled") + if let error = error?.asAFError { + XCTAssertTrue(error.isServerTrustEvaluationError, "should be .certificatePinningFailed") } else { - XCTFail("error should be an URLError") + XCTFail("error should be an AFError") } } - func testThatExpiredCertificateRequestSucceedsWhenPinningLeafPublicKeyWithoutCertificateChainValidation() { + func testThatExpiredCertificateRequestSucceedsWhenPinningLeafPublicKeyWithoutCertificateChainOrHostValidation() { // Given - let publicKeys = [TestPublicKeys.leaf] - let policies: [String: ServerTrustPolicy] = [ - expiredHost: .pinPublicKeys(publicKeys: publicKeys, validateCertificateChain: false, validateHost: true) + let keys = [TestCertificates.leaf].publicKeys + let evaluators = [ + expiredHost: PublicKeysTrustEvaluator(keys: keys, performDefaultValidation: false, validateHost: false) ] - let manager = SessionManager( + let manager = Session( configuration: configuration, - serverTrustPolicyManager: ServerTrustPolicyManager(policies: policies) + serverTrustManager: ServerTrustManager(evaluators: evaluators) ) let expectation = self.expectation(description: "\(expiredURLString)") @@ -513,16 +483,16 @@ class TLSEvaluationExpiredLeafCertificateTestCase: BaseTestCase { XCTAssertNil(error, "error should be nil") } - func testThatExpiredCertificateRequestSucceedsWhenPinningIntermediateCAPublicKeyWithoutCertificateChainValidation() { + func testThatExpiredCertificateRequestSucceedsWhenPinningIntermediateCAPublicKeyWithoutCertificateChainOrHostValidation() { // Given - let publicKeys = [TestPublicKeys.intermediateCA2] - let policies: [String: ServerTrustPolicy] = [ - expiredHost: .pinPublicKeys(publicKeys: publicKeys, validateCertificateChain: false, validateHost: true) + let keys = [TestCertificates.intermediateCA2].publicKeys + let evaluators = [ + expiredHost: PublicKeysTrustEvaluator(keys: keys, performDefaultValidation: false, validateHost: false) ] - let manager = SessionManager( + let manager = Session( configuration: configuration, - serverTrustPolicyManager: ServerTrustPolicyManager(policies: policies) + serverTrustManager: ServerTrustManager(evaluators: evaluators) ) let expectation = self.expectation(description: "\(expiredURLString)") @@ -543,14 +513,14 @@ class TLSEvaluationExpiredLeafCertificateTestCase: BaseTestCase { func testThatExpiredCertificateRequestSucceedsWhenPinningRootCAPublicKeyWithoutCertificateChainValidation() { // Given - let publicKeys = [TestPublicKeys.rootCA] - let policies: [String: ServerTrustPolicy] = [ - expiredHost: .pinPublicKeys(publicKeys: publicKeys, validateCertificateChain: false, validateHost: true) + let keys = [TestCertificates.rootCA].publicKeys + let evaluators = [ + expiredHost: PublicKeysTrustEvaluator(keys: keys, performDefaultValidation: false, validateHost: false) ] - let manager = SessionManager( + let manager = Session( configuration: configuration, - serverTrustPolicyManager: ServerTrustPolicyManager(policies: policies) + serverTrustManager: ServerTrustManager(evaluators: evaluators) ) let expectation = self.expectation(description: "\(expiredURLString)") @@ -577,42 +547,10 @@ class TLSEvaluationExpiredLeafCertificateTestCase: BaseTestCase { func testThatExpiredCertificateRequestSucceedsWhenDisablingEvaluation() { // Given - let policies = [expiredHost: ServerTrustPolicy.disableEvaluation] - let manager = SessionManager( - configuration: configuration, - serverTrustPolicyManager: ServerTrustPolicyManager(policies: policies) - ) - - let expectation = self.expectation(description: "\(expiredURLString)") - var error: Error? - - // When - manager.request(expiredURLString) - .response { resp in - error = resp.error - expectation.fulfill() - } - - waitForExpectations(timeout: timeout, handler: nil) - - // Then - XCTAssertNil(error, "error should be nil") - } - - // MARK: Server Trust Policy - Custom Evaluation Tests - - func testThatExpiredCertificateRequestSucceedsWhenCustomEvaluationReturnsTrue() { - // Given - let policies = [ - expiredHost: ServerTrustPolicy.customEvaluation { _, _ in - // Implement a custom evaluation routine here... - return true - } - ] - - let manager = SessionManager( + let evaluators = [expiredHost: DisabledEvaluator()] + let manager = Session( configuration: configuration, - serverTrustPolicyManager: ServerTrustPolicyManager(policies: policies) + serverTrustManager: ServerTrustManager(evaluators: evaluators) ) let expectation = self.expectation(description: "\(expiredURLString)") @@ -630,40 +568,4 @@ class TLSEvaluationExpiredLeafCertificateTestCase: BaseTestCase { // Then XCTAssertNil(error, "error should be nil") } - - func testThatExpiredCertificateRequestFailsWhenCustomEvaluationReturnsFalse() { - // Given - let policies = [ - expiredHost: ServerTrustPolicy.customEvaluation { _, _ in - // Implement a custom evaluation routine here... - return false - } - ] - - let manager = SessionManager( - configuration: configuration, - serverTrustPolicyManager: ServerTrustPolicyManager(policies: policies) - ) - - let expectation = self.expectation(description: "\(expiredURLString)") - var error: Error? - - // When - manager.request(expiredURLString) - .response { resp in - error = resp.error - expectation.fulfill() - } - - waitForExpectations(timeout: timeout, handler: nil) - - // Then - XCTAssertNotNil(error, "error should not be nil") - - if let error = error as? URLError { - XCTAssertEqual(error.code, .cancelled, "code should be cancelled") - } else { - XCTFail("error should be an URLError") - } - } } diff --git a/Tests/URLProtocolTests.swift b/Tests/URLProtocolTests.swift index c005a0d7e..7cf486ae7 100644 --- a/Tests/URLProtocolTests.swift +++ b/Tests/URLProtocolTests.swift @@ -37,7 +37,7 @@ class ProxyURLProtocol: URLProtocol { lazy var session: URLSession = { let configuration: URLSessionConfiguration = { let configuration = URLSessionConfiguration.ephemeral - configuration.httpAdditionalHeaders = SessionManager.defaultHTTPHeaders + configuration.httpHeaders = HTTPHeaders.default return configuration }() @@ -112,7 +112,7 @@ extension ProxyURLProtocol: URLSessionDataDelegate { // MARK: - class URLProtocolTestCase: BaseTestCase { - var manager: SessionManager! + var manager: Session! // MARK: Setup and Teardown @@ -128,7 +128,7 @@ class URLProtocolTestCase: BaseTestCase { return configuration }() - return SessionManager(configuration: configuration) + return Session(configuration: configuration) }() } @@ -145,7 +145,7 @@ class URLProtocolTestCase: BaseTestCase { let expectation = self.expectation(description: "GET request should succeed") - var response: DefaultDataResponse? + var response: DataResponse? // When manager.request(urlRequest) @@ -163,14 +163,8 @@ class URLProtocolTestCase: BaseTestCase { XCTAssertNil(response?.error) if let headers = response?.response?.allHeaderFields as? [String: String] { - XCTAssertEqual(headers["Request-Header"], "foobar") - - // Configuration headers are only passed in on iOS 9.0+ - if #available(iOS 9.0, *) { - XCTAssertEqual(headers["Session-Configuration-Header"], "foo") - } else { - XCTAssertNil(headers["Session-Configuration-Header"]) - } + XCTAssertEqual(headers["Request-Header"] ?? headers["request-header"], "foobar") + XCTAssertEqual(headers["Session-Configuration-Header"] ?? headers["session-configuration-header"], "foo") } else { XCTFail("headers should not be nil") } diff --git a/Tests/UploadTests.swift b/Tests/UploadTests.swift index 78a9ab1a5..eaff24665 100644 --- a/Tests/UploadTests.swift +++ b/Tests/UploadTests.swift @@ -29,37 +29,47 @@ import XCTest class UploadFileInitializationTestCase: BaseTestCase { func testUploadClassMethodWithMethodURLAndFile() { // Given - let urlString = "https://httpbin.org/" + let urlString = "https://httpbin.org/post" let imageURL = url(forResource: "rainbow", withExtension: "jpg") + let expectation = self.expectation(description: "upload should complete") // When - let request = Alamofire.upload(imageURL, to: urlString) + let request = AF.upload(imageURL, to: urlString).response { _ in + expectation.fulfill() + } + + waitForExpectations(timeout: timeout, handler: nil) // Then XCTAssertNotNil(request.request, "request should not be nil") - XCTAssertEqual(request.request?.httpMethod ?? "", "POST", "request HTTP method should be POST") + XCTAssertEqual(request.request?.httpMethod, "POST", "request HTTP method should be POST") XCTAssertEqual(request.request?.url?.absoluteString, urlString, "request URL string should be equal") - XCTAssertNil(request.response, "response should be nil") + XCTAssertNotNil(request.response, "response should not be nil") } func testUploadClassMethodWithMethodURLHeadersAndFile() { // Given - let urlString = "https://httpbin.org/" - let headers = ["Authorization": "123456"] + let urlString = "https://httpbin.org/post" + let headers: HTTPHeaders = ["Authorization": "123456"] let imageURL = url(forResource: "rainbow", withExtension: "jpg") + let expectation = self.expectation(description: "upload should complete") // When - let request = Alamofire.upload(imageURL, to: urlString, method: .post, headers: headers) + let request = AF.upload(imageURL, to: urlString, method: .post, headers: headers).response { _ in + expectation.fulfill() + } + + waitForExpectations(timeout: timeout, handler: nil) // Then XCTAssertNotNil(request.request, "request should not be nil") - XCTAssertEqual(request.request?.httpMethod ?? "", "POST", "request HTTP method should be POST") + XCTAssertEqual(request.request?.httpMethod, "POST", "request HTTP method should be POST") XCTAssertEqual(request.request?.url?.absoluteString, urlString, "request URL string should be equal") let authorizationHeader = request.request?.value(forHTTPHeaderField: "Authorization") ?? "" XCTAssertEqual(authorizationHeader, "123456", "Authorization header is incorrect") - XCTAssertNil(request.response, "response should be nil") + XCTAssertNotNil(request.response, "response should not be nil") } } @@ -68,35 +78,45 @@ class UploadFileInitializationTestCase: BaseTestCase { class UploadDataInitializationTestCase: BaseTestCase { func testUploadClassMethodWithMethodURLAndData() { // Given - let urlString = "https://httpbin.org/" + let urlString = "https://httpbin.org/post" + let expectation = self.expectation(description: "upload should complete") // When - let request = Alamofire.upload(Data(), to: urlString) + let request = AF.upload(Data(), to: urlString).response { _ in + expectation.fulfill() + } + + waitForExpectations(timeout: timeout, handler: nil) // Then XCTAssertNotNil(request.request, "request should not be nil") XCTAssertEqual(request.request?.httpMethod ?? "", "POST", "request HTTP method should be POST") XCTAssertEqual(request.request?.url?.absoluteString, urlString, "request URL string should be equal") - XCTAssertNil(request.response, "response should be nil") + XCTAssertNotNil(request.response, "response should not be nil") } func testUploadClassMethodWithMethodURLHeadersAndData() { // Given - let urlString = "https://httpbin.org/" - let headers = ["Authorization": "123456"] + let urlString = "https://httpbin.org/post" + let headers: HTTPHeaders = ["Authorization": "123456"] + let expectation = self.expectation(description: "upload should complete") // When - let request = Alamofire.upload(Data(), to: urlString, headers: headers) + let request = AF.upload(Data(), to: urlString, headers: headers).response { _ in + expectation.fulfill() + } + + waitForExpectations(timeout: timeout, handler: nil) // Then XCTAssertNotNil(request.request, "request should not be nil") - XCTAssertEqual(request.request?.httpMethod ?? "", "POST", "request HTTP method should be POST") + XCTAssertEqual(request.request?.httpMethod, "POST", "request HTTP method should be POST") XCTAssertEqual(request.request?.url?.absoluteString, urlString, "request URL string should be equal") let authorizationHeader = request.request?.value(forHTTPHeaderField: "Authorization") ?? "" XCTAssertEqual(authorizationHeader, "123456", "Authorization header is incorrect") - XCTAssertNil(request.response, "response should be nil") + XCTAssertNotNil(request.response, "response should not be nil") } } @@ -105,39 +125,49 @@ class UploadDataInitializationTestCase: BaseTestCase { class UploadStreamInitializationTestCase: BaseTestCase { func testUploadClassMethodWithMethodURLAndStream() { // Given - let urlString = "https://httpbin.org/" + let urlString = "https://httpbin.org/post" let imageURL = url(forResource: "rainbow", withExtension: "jpg") let imageStream = InputStream(url: imageURL)! + let expectation = self.expectation(description: "upload should complete") // When - let request = Alamofire.upload(imageStream, to: urlString) + let request = AF.upload(imageStream, to: urlString).response { _ in + expectation.fulfill() + } + + waitForExpectations(timeout: timeout, handler: nil) // Then XCTAssertNotNil(request.request, "request should not be nil") - XCTAssertEqual(request.request?.httpMethod ?? "", "POST", "request HTTP method should be POST") + XCTAssertEqual(request.request?.httpMethod, "POST", "request HTTP method should be POST") XCTAssertEqual(request.request?.url?.absoluteString, urlString, "request URL string should be equal") - XCTAssertNil(request.response, "response should be nil") + XCTAssertNotNil(request.response, "response should not be nil") } func testUploadClassMethodWithMethodURLHeadersAndStream() { // Given - let urlString = "https://httpbin.org/" + let urlString = "https://httpbin.org/post" let imageURL = url(forResource: "rainbow", withExtension: "jpg") - let headers = ["Authorization": "123456"] + let headers: HTTPHeaders = ["Authorization": "123456"] let imageStream = InputStream(url: imageURL)! + let expectation = self.expectation(description: "upload should complete") // When - let request = Alamofire.upload(imageStream, to: urlString, headers: headers) + let request = AF.upload(imageStream, to: urlString, headers: headers).response { _ in + expectation.fulfill() + } + + waitForExpectations(timeout: timeout, handler: nil) // Then XCTAssertNotNil(request.request, "request should not be nil") - XCTAssertEqual(request.request?.httpMethod ?? "", "POST", "request HTTP method should be POST") + XCTAssertEqual(request.request?.httpMethod, "POST", "request HTTP method should be POST") XCTAssertEqual(request.request?.url?.absoluteString, urlString, "request URL string should be equal") let authorizationHeader = request.request?.value(forHTTPHeaderField: "Authorization") ?? "" XCTAssertEqual(authorizationHeader, "123456", "Authorization header is incorrect") - XCTAssertNil(request.response, "response should be nil") + XCTAssertNotNil(request.response, "response should not be nil, tasks: \(request.tasks)") } } @@ -147,13 +177,13 @@ class UploadDataTestCase: BaseTestCase { func testUploadDataRequest() { // Given let urlString = "https://httpbin.org/post" - let data = "Lorem ipsum dolor sit amet".data(using: .utf8, allowLossyConversion: false)! + let data = Data("Lorem ipsum dolor sit amet".utf8) let expectation = self.expectation(description: "Upload request should succeed: \(urlString)") - var response: DefaultDataResponse? + var response: DataResponse? // When - Alamofire.upload(data, to: urlString) + AF.upload(data, to: urlString) .response { resp in response = resp expectation.fulfill() @@ -170,24 +200,18 @@ class UploadDataTestCase: BaseTestCase { func testUploadDataRequestWithProgress() { // Given let urlString = "https://httpbin.org/post" - let data: Data = { - var text = "" - for _ in 1...3_000 { - text += "Lorem ipsum dolor sit amet, consectetur adipiscing elit. " - } - - return text.data(using: .utf8, allowLossyConversion: false)! - }() + let string = String(repeating: "Lorem ipsum dolor sit amet, consectetur adipiscing elit. ", count: 100) + let data = Data(string.utf8) let expectation = self.expectation(description: "Bytes upload progress should be reported: \(urlString)") var uploadProgressValues: [Double] = [] var downloadProgressValues: [Double] = [] - var response: DefaultDataResponse? + var response: DataResponse? // When - Alamofire.upload(data, to: urlString) + AF.upload(data, to: urlString) .uploadProgress { progress in uploadProgressValues.append(progress.fractionCompleted) } @@ -244,32 +268,24 @@ class UploadMultipartFormDataTestCase: BaseTestCase { func testThatUploadingMultipartFormDataSetsContentTypeHeader() { // Given let urlString = "https://httpbin.org/post" - let uploadData = "upload_data".data(using: .utf8, allowLossyConversion: false)! + let uploadData = Data("upload_data".utf8) let expectation = self.expectation(description: "multipart form data upload should succeed") var formData: MultipartFormData? - var response: DefaultDataResponse? + var response: DataResponse? // When - Alamofire.upload( + AF.upload( multipartFormData: { multipartFormData in multipartFormData.append(uploadData, withName: "upload_data") formData = multipartFormData }, - to: urlString, - encodingCompletion: { result in - switch result { - case .success(let upload, _, _): - upload.response { resp in - response = resp - expectation.fulfill() - } - case .failure: - expectation.fulfill() - } + to: urlString) + .response { resp in + response = resp + expectation.fulfill() } - ) waitForExpectations(timeout: timeout, handler: nil) @@ -293,31 +309,23 @@ class UploadMultipartFormDataTestCase: BaseTestCase { func testThatUploadingMultipartFormDataSucceedsWithDefaultParameters() { // Given let urlString = "https://httpbin.org/post" - let frenchData = "français".data(using: .utf8, allowLossyConversion: false)! - let japaneseData = "日本語".data(using: .utf8, allowLossyConversion: false)! + let frenchData = Data("français".utf8) + let japaneseData = Data("日本語".utf8) let expectation = self.expectation(description: "multipart form data upload should succeed") - var response: DefaultDataResponse? + var response: DataResponse? // When - Alamofire.upload( + AF.upload( multipartFormData: { multipartFormData in multipartFormData.append(frenchData, withName: "french") multipartFormData.append(japaneseData, withName: "japanese") }, - to: urlString, - encodingCompletion: { result in - switch result { - case .success(let upload, _, _): - upload.response { resp in - response = resp - expectation.fulfill() - } - case .failure: - expectation.fulfill() - } + to: urlString) + .response { (resp) in + response = resp + expectation.fulfill() } - ) waitForExpectations(timeout: timeout, handler: nil) @@ -339,91 +347,67 @@ class UploadMultipartFormDataTestCase: BaseTestCase { func testThatUploadingMultipartFormDataBelowMemoryThresholdStreamsFromMemory() { // Given let urlString = "https://httpbin.org/post" - let frenchData = "français".data(using: .utf8, allowLossyConversion: false)! - let japaneseData = "日本語".data(using: .utf8, allowLossyConversion: false)! + let frenchData = Data("français".utf8) + let japaneseData = Data("日本語".utf8) let expectation = self.expectation(description: "multipart form data upload should succeed") - - var streamingFromDisk: Bool? - var streamFileURL: URL? + var response: DataResponse? // When - Alamofire.upload( - multipartFormData: { multipartFormData in - multipartFormData.append(frenchData, withName: "french") - multipartFormData.append(japaneseData, withName: "japanese") - }, - to: urlString, - encodingCompletion: { result in - switch result { - case let .success(upload, uploadStreamingFromDisk, uploadStreamFileURL): - streamingFromDisk = uploadStreamingFromDisk - streamFileURL = uploadStreamFileURL - - upload.response { _ in - expectation.fulfill() - } - case .failure: - expectation.fulfill() - } - } - ) + let request = AF.upload( + multipartFormData: { multipartFormData in + multipartFormData.append(frenchData, withName: "french") + multipartFormData.append(japaneseData, withName: "japanese") + }, + to: urlString) + .response { (resp) in + response = resp + expectation.fulfill() + } waitForExpectations(timeout: timeout, handler: nil) // Then - XCTAssertNotNil(streamingFromDisk, "streaming from disk should not be nil") - XCTAssertNil(streamFileURL, "stream file URL should be nil") - - if let streamingFromDisk = streamingFromDisk { - XCTAssertFalse(streamingFromDisk, "streaming from disk should be false") + guard let uploadable = request.uploadable, case .data = uploadable else { + XCTFail("Uploadable is not .data") + return } + + XCTAssertTrue(response?.result.isSuccess == true) } func testThatUploadingMultipartFormDataBelowMemoryThresholdSetsContentTypeHeader() { // Given let urlString = "https://httpbin.org/post" - let uploadData = "upload data".data(using: .utf8, allowLossyConversion: false)! + let uploadData = Data("upload_data".utf8) let expectation = self.expectation(description: "multipart form data upload should succeed") var formData: MultipartFormData? - var request: URLRequest? - var streamingFromDisk: Bool? + var response: DataResponse? // When - Alamofire.upload( - multipartFormData: { multipartFormData in - multipartFormData.append(uploadData, withName: "upload_data") - formData = multipartFormData - }, - to: urlString, - encodingCompletion: { result in - switch result { - case let .success(upload, uploadStreamingFromDisk, _): - streamingFromDisk = uploadStreamingFromDisk - - upload.response { resp in - request = resp.request - expectation.fulfill() - } - case .failure: - expectation.fulfill() - } - } - ) + let request = AF.upload( + multipartFormData: { multipartFormData in + multipartFormData.append(uploadData, withName: "upload_data") + formData = multipartFormData + }, + to: urlString) + .response { resp in + response = resp + expectation.fulfill() + } waitForExpectations(timeout: timeout, handler: nil) // Then - XCTAssertNotNil(streamingFromDisk, "streaming from disk should not be nil") - - if let streamingFromDisk = streamingFromDisk { - XCTAssertFalse(streamingFromDisk, "streaming from disk should be false") + guard let uploadable = request.uploadable, case .data = uploadable else { + XCTFail("Uploadable is not .data") + return } if - let request = request, + let request = response?.request, let multipartFormData = formData, let contentType = request.value(forHTTPHeaderField: "Content-Type") { @@ -436,94 +420,69 @@ class UploadMultipartFormDataTestCase: BaseTestCase { func testThatUploadingMultipartFormDataAboveMemoryThresholdStreamsFromDisk() { // Given let urlString = "https://httpbin.org/post" - let frenchData = "français".data(using: .utf8, allowLossyConversion: false)! - let japaneseData = "日本語".data(using: .utf8, allowLossyConversion: false)! + let frenchData = Data("français".utf8) + let japaneseData = Data("日本語".utf8) let expectation = self.expectation(description: "multipart form data upload should succeed") - - var streamingFromDisk: Bool? - var streamFileURL: URL? + var response: DataResponse? // When - Alamofire.upload( - multipartFormData: { multipartFormData in - multipartFormData.append(frenchData, withName: "french") - multipartFormData.append(japaneseData, withName: "japanese") - }, - usingThreshold: 0, - to: urlString, - encodingCompletion: { result in - switch result { - case let .success(upload, uploadStreamingFromDisk, uploadStreamFileURL): - streamingFromDisk = uploadStreamingFromDisk - streamFileURL = uploadStreamFileURL - - upload.response { _ in - expectation.fulfill() - } - case .failure: - expectation.fulfill() - } - } - ) + let request = AF.upload( + multipartFormData: { multipartFormData in + multipartFormData.append(frenchData, withName: "french") + multipartFormData.append(japaneseData, withName: "japanese") + }, + usingThreshold: 0, + to: urlString).response { resp in + response = resp + expectation.fulfill() + } waitForExpectations(timeout: timeout, handler: nil) // Then - XCTAssertNotNil(streamingFromDisk, "streaming from disk should not be nil") - XCTAssertNotNil(streamFileURL, "stream file URL should not be nil") - - if let streamingFromDisk = streamingFromDisk, let streamFilePath = streamFileURL?.path { - XCTAssertTrue(streamingFromDisk, "streaming from disk should be true") - XCTAssertFalse(FileManager.default.fileExists(atPath: streamFilePath), "stream file path should not exist") + guard let uploadable = request.uploadable, case let .file(url, _) = uploadable else { + XCTFail("Uploadable is not .file") + return } + + XCTAssertTrue(response?.result.isSuccess == true) + XCTAssertFalse(FileManager.default.fileExists(atPath: url.path)) } func testThatUploadingMultipartFormDataAboveMemoryThresholdSetsContentTypeHeader() { // Given let urlString = "https://httpbin.org/post" - let uploadData = "upload data".data(using: .utf8, allowLossyConversion: false)! + let uploadData = Data("upload_data".utf8) let expectation = self.expectation(description: "multipart form data upload should succeed") - + var response: DataResponse? var formData: MultipartFormData? - var request: URLRequest? - var streamingFromDisk: Bool? // When - Alamofire.upload( - multipartFormData: { multipartFormData in - multipartFormData.append(uploadData, withName: "upload_data") - formData = multipartFormData - }, - usingThreshold: 0, - to: urlString, - encodingCompletion: { result in - switch result { - case let .success(upload, uploadStreamingFromDisk, _): - streamingFromDisk = uploadStreamingFromDisk - - upload.response { resp in - request = resp.request - expectation.fulfill() - } - case .failure: - expectation.fulfill() - } - } - ) + let request = AF.upload( + multipartFormData: { multipartFormData in + multipartFormData.append(uploadData, withName: "upload_data") + formData = multipartFormData + }, + usingThreshold: 0, + to: urlString).response { resp in + response = resp + expectation.fulfill() + } waitForExpectations(timeout: timeout, handler: nil) // Then - XCTAssertNotNil(streamingFromDisk, "streaming from disk should not be nil") - - if let streamingFromDisk = streamingFromDisk { - XCTAssertTrue(streamingFromDisk, "streaming from disk should be true") + guard let uploadable = request.uploadable, case .file = uploadable else { + XCTFail("Uploadable is not .file") + return } + XCTAssertTrue(response?.result.isSuccess == true) + if - let request = request, + let request = response?.request, let multipartFormData = formData, let contentType = request.value(forHTTPHeaderField: "Content-Type") { @@ -536,16 +495,16 @@ class UploadMultipartFormDataTestCase: BaseTestCase { #if os(macOS) func testThatUploadingMultipartFormDataOnBackgroundSessionWritesDataToFileToAvoidCrash() { // Given - let manager: SessionManager = { + let manager: Session = { let identifier = "org.alamofire.uploadtests.\(UUID().uuidString)" let configuration = URLSessionConfiguration.background(withIdentifier: identifier) - return SessionManager(configuration: configuration, serverTrustPolicyManager: nil) + return Session(configuration: configuration) }() let urlString = "https://httpbin.org/post" - let french = "français".data(using: .utf8, allowLossyConversion: false)! - let japanese = "日本語".data(using: .utf8, allowLossyConversion: false)! + let french = Data("français".utf8) + let japanese = Data("日本語".utf8) let expectation = self.expectation(description: "multipart form data upload should succeed") @@ -553,33 +512,22 @@ class UploadMultipartFormDataTestCase: BaseTestCase { var response: HTTPURLResponse? var data: Data? var error: Error? - var streamingFromDisk: Bool? // When - manager.upload( + let upload = manager.upload( multipartFormData: { multipartFormData in multipartFormData.append(french, withName: "french") multipartFormData.append(japanese, withName: "japanese") }, - to: urlString, - encodingCompletion: { result in - switch result { - case let .success(upload, uploadStreamingFromDisk, _): - streamingFromDisk = uploadStreamingFromDisk - - upload.response { defaultResponse in - request = defaultResponse.request - response = defaultResponse.response - data = defaultResponse.data - error = defaultResponse.error - - expectation.fulfill() - } - case .failure: - expectation.fulfill() - } + to: urlString) + .response { defaultResponse in + request = defaultResponse.request + response = defaultResponse.response + data = defaultResponse.data + error = defaultResponse.error + + expectation.fulfill() } - ) waitForExpectations(timeout: timeout, handler: nil) @@ -589,10 +537,9 @@ class UploadMultipartFormDataTestCase: BaseTestCase { XCTAssertNotNil(data, "data should not be nil") XCTAssertNil(error, "error should be nil") - if let streamingFromDisk = streamingFromDisk { - XCTAssertTrue(streamingFromDisk, "streaming from disk should be true") - } else { - XCTFail("streaming from disk should not be nil") + guard let uploadable = upload.uploadable, case .file = uploadable else { + XCTFail("Uploadable is not .file") + return } } #endif @@ -602,57 +549,36 @@ class UploadMultipartFormDataTestCase: BaseTestCase { private func executeMultipartFormDataUploadRequestWithProgress(streamFromDisk: Bool) { // Given let urlString = "https://httpbin.org/post" - let loremData1: Data = { - var loremValues: [String] = [] - for _ in 1...1_500 { - loremValues.append("Lorem ipsum dolor sit amet, consectetur adipiscing elit.") - } - - return loremValues.joined(separator: " ").data(using: .utf8, allowLossyConversion: false)! - }() - let loremData2: Data = { - var loremValues: [String] = [] - for _ in 1...1_500 { - loremValues.append("Lorem ipsum dolor sit amet, nam no graeco recusabo appellantur.") - } - - return loremValues.joined(separator: " ").data(using: .utf8, allowLossyConversion: false)! - }() + let loremData1 = Data(String(repeating: "Lorem ipsum dolor sit amet, consectetur adipiscing elit.", + count: 100).utf8) + let loremData2 = Data(String(repeating: "Lorem ipsum dolor sit amet, nam no graeco recusabo appellantur.", + count: 100).utf8) let expectation = self.expectation(description: "multipart form data upload should succeed") var uploadProgressValues: [Double] = [] var downloadProgressValues: [Double] = [] - var response: DefaultDataResponse? + var response: DataResponse? // When - Alamofire.upload( + AF.upload( multipartFormData: { multipartFormData in multipartFormData.append(loremData1, withName: "lorem1") multipartFormData.append(loremData2, withName: "lorem2") }, usingThreshold: streamFromDisk ? 0 : 100_000_000, - to: urlString, - encodingCompletion: { result in - switch result { - case .success(let upload, _, _): - upload - .uploadProgress { progress in - uploadProgressValues.append(progress.fractionCompleted) - } - .downloadProgress { progress in - downloadProgressValues.append(progress.fractionCompleted) - } - .response { resp in - response = resp - expectation.fulfill() - } - case .failure: - expectation.fulfill() - } + to: urlString) + .uploadProgress { progress in + uploadProgressValues.append(progress.fractionCompleted) } - ) + .downloadProgress { progress in + downloadProgressValues.append(progress.fractionCompleted) + } + .response { resp in + response = resp + expectation.fulfill() + } waitForExpectations(timeout: timeout, handler: nil) diff --git a/Tests/ValidationTests.swift b/Tests/ValidationTests.swift index 6451c5c1b..216363f70 100644 --- a/Tests/ValidationTests.swift +++ b/Tests/ValidationTests.swift @@ -38,14 +38,14 @@ class StatusCodeValidationTestCase: BaseTestCase { var downloadError: Error? // When - Alamofire.request(urlString) + AF.request(urlString) .validate(statusCode: 200..<300) .response { resp in requestError = resp.error expectation1.fulfill() } - Alamofire.download(urlString) + AF.download(urlString) .validate(statusCode: 200..<300) .response { resp in downloadError = resp.error @@ -70,14 +70,14 @@ class StatusCodeValidationTestCase: BaseTestCase { var downloadError: Error? // When - Alamofire.request(urlString) + AF.request(urlString) .validate(statusCode: [200]) .response { resp in requestError = resp.error expectation1.fulfill() } - Alamofire.download(urlString) + AF.download(urlString) .validate(statusCode: [200]) .response { resp in downloadError = resp.error @@ -91,7 +91,7 @@ class StatusCodeValidationTestCase: BaseTestCase { XCTAssertNotNil(downloadError) for error in [requestError, downloadError] { - if let error = error as? AFError, let statusCode = error.responseCode { + if let error = error?.asAFError, let statusCode = error.responseCode { XCTAssertTrue(error.isUnacceptableStatusCode) XCTAssertEqual(statusCode, 404) } else { @@ -111,14 +111,14 @@ class StatusCodeValidationTestCase: BaseTestCase { var downloadError: Error? // When - Alamofire.request(urlString) + AF.request(urlString) .validate(statusCode: []) .response { resp in requestError = resp.error expectation1.fulfill() } - Alamofire.download(urlString) + AF.download(urlString) .validate(statusCode: []) .response { resp in downloadError = resp.error @@ -132,7 +132,7 @@ class StatusCodeValidationTestCase: BaseTestCase { XCTAssertNotNil(downloadError) for error in [requestError, downloadError] { - if let error = error as? AFError, let statusCode = error.responseCode { + if let error = error?.asAFError, let statusCode = error.responseCode { XCTAssertTrue(error.isUnacceptableStatusCode) XCTAssertEqual(statusCode, 201) } else { @@ -156,7 +156,7 @@ class ContentTypeValidationTestCase: BaseTestCase { var downloadError: Error? // When - Alamofire.request(urlString) + AF.request(urlString) .validate(contentType: ["application/json"]) .validate(contentType: ["application/json; charset=utf-8"]) .validate(contentType: ["application/json; q=0.8; charset=utf-8"]) @@ -165,7 +165,7 @@ class ContentTypeValidationTestCase: BaseTestCase { expectation1.fulfill() } - Alamofire.download(urlString) + AF.download(urlString) .validate(contentType: ["application/json"]) .validate(contentType: ["application/json; charset=utf-8"]) .validate(contentType: ["application/json; q=0.8; charset=utf-8"]) @@ -192,7 +192,7 @@ class ContentTypeValidationTestCase: BaseTestCase { var downloadError: Error? // When - Alamofire.request(urlString) + AF.request(urlString) .validate(contentType: ["*/*"]) .validate(contentType: ["application/*"]) .validate(contentType: ["*/json"]) @@ -201,7 +201,7 @@ class ContentTypeValidationTestCase: BaseTestCase { expectation1.fulfill() } - Alamofire.download(urlString) + AF.download(urlString) .validate(contentType: ["*/*"]) .validate(contentType: ["application/*"]) .validate(contentType: ["*/json"]) @@ -228,14 +228,14 @@ class ContentTypeValidationTestCase: BaseTestCase { var downloadError: Error? // When - Alamofire.request(urlString) + AF.request(urlString) .validate(contentType: ["application/octet-stream"]) .response { resp in requestError = resp.error expectation1.fulfill() } - Alamofire.download(urlString) + AF.download(urlString) .validate(contentType: ["application/octet-stream"]) .response { resp in downloadError = resp.error @@ -249,7 +249,7 @@ class ContentTypeValidationTestCase: BaseTestCase { XCTAssertNotNil(downloadError) for error in [requestError, downloadError] { - if let error = error as? AFError { + if let error = error?.asAFError { XCTAssertTrue(error.isUnacceptableContentType) XCTAssertEqual(error.responseContentType, "application/xml") XCTAssertEqual(error.acceptableContentTypes?.first, "application/octet-stream") @@ -270,14 +270,14 @@ class ContentTypeValidationTestCase: BaseTestCase { var downloadError: Error? // When - Alamofire.request(urlString) + AF.request(urlString) .validate(contentType: []) .response { resp in requestError = resp.error expectation1.fulfill() } - Alamofire.download(urlString) + AF.download(urlString) .validate(contentType: []) .response { resp in downloadError = resp.error @@ -291,7 +291,7 @@ class ContentTypeValidationTestCase: BaseTestCase { XCTAssertNotNil(downloadError) for error in [requestError, downloadError] { - if let error = error as? AFError { + if let error = error?.asAFError { XCTAssertTrue(error.isUnacceptableContentType) XCTAssertEqual(error.responseContentType, "application/xml") XCTAssertTrue(error.acceptableContentTypes?.isEmpty ?? false) @@ -312,14 +312,14 @@ class ContentTypeValidationTestCase: BaseTestCase { var downloadError: Error? // When - Alamofire.request(urlString) + AF.request(urlString) .validate(contentType: []) .response { resp in requestError = resp.error expectation1.fulfill() } - Alamofire.download(urlString) + AF.download(urlString) .validate(contentType: []) .response { resp in downloadError = resp.error @@ -335,51 +335,34 @@ class ContentTypeValidationTestCase: BaseTestCase { func testThatValidationForRequestWithAcceptableWildcardContentTypeResponseSucceedsWhenResponseIsNil() { // Given - class MockManager: SessionManager { - override func request(_ urlRequest: URLRequestConvertible) -> DataRequest { - do { - let originalRequest = try urlRequest.asURLRequest() - let originalTask = DataRequest.Requestable(urlRequest: originalRequest) - - let task = try originalTask.task(session: session, adapter: adapter, queue: queue) - let request = MockDataRequest(session: session, requestTask: .data(originalTask, task)) + class MockManager: Session { + override func request(_ convertible: URLRequestConvertible) -> DataRequest { + let request = MockDataRequest(convertible: convertible, + underlyingQueue: rootQueue, + serializationQueue: serializationQueue, + eventMonitor: eventMonitor, + delegate: self) - delegate[task] = request + perform(request) - if startRequestsImmediately { request.resume() } - - return request - } catch { - let request = DataRequest(session: session, requestTask: .data(nil, nil), error: error) - if startRequestsImmediately { request.resume() } - return request - } + return request } override func download( - _ urlRequest: URLRequestConvertible, - to destination: DownloadRequest.DownloadFileDestination? = nil) + _ convertible: URLRequestConvertible, + to destination: DownloadRequest.Destination? = nil) -> DownloadRequest { - do { - let originalRequest = try urlRequest.asURLRequest() - let originalTask = DownloadRequest.Downloadable.request(originalRequest) - - let task = try originalTask.task(session: session, adapter: adapter, queue: queue) - let request = MockDownloadRequest(session: session, requestTask: .download(originalTask, task)) - - request.downloadDelegate.destination = destination - - delegate[task] = request + let request = MockDownloadRequest(downloadable: .request(convertible), + underlyingQueue: rootQueue, + serializationQueue: serializationQueue, + eventMonitor: eventMonitor, + delegate: self + ) - if startRequestsImmediately { request.resume() } + perform(request) - return request - } catch { - let download = DownloadRequest(session: session, requestTask: .download(nil, nil), error: error) - if startRequestsImmediately { download.resume() } - return download - } + return request } } @@ -409,10 +392,10 @@ class ContentTypeValidationTestCase: BaseTestCase { override var mimeType: String? { return nil } } - let manager: SessionManager = { + let manager: Session = { let configuration: URLSessionConfiguration = { let configuration = URLSessionConfiguration.ephemeral - configuration.httpAdditionalHeaders = SessionManager.defaultHTTPHeaders + configuration.httpHeaders = HTTPHeaders.default return configuration }() @@ -422,11 +405,11 @@ class ContentTypeValidationTestCase: BaseTestCase { let urlString = "https://httpbin.org/delete" - let expectation1 = self.expectation(description: "request should be stubbed and return 204 status code") - let expectation2 = self.expectation(description: "download should be stubbed and return 204 status code") + let expectation1 = expectation(description: "request should be stubbed and return 204 status code") + let expectation2 = expectation(description: "download should be stubbed and return 204 status code") - var requestResponse: DefaultDataResponse? - var downloadResponse: DefaultDownloadResponse? + var requestResponse: DataResponse? + var downloadResponse: DownloadResponse? // When manager.request(urlString, method: .delete) @@ -454,8 +437,7 @@ class ContentTypeValidationTestCase: BaseTestCase { XCTAssertNil(requestResponse?.response?.mimeType) XCTAssertNotNil(downloadResponse?.response) - XCTAssertNotNil(downloadResponse?.temporaryURL) - XCTAssertNil(downloadResponse?.destinationURL) + XCTAssertNotNil(downloadResponse?.fileURL) XCTAssertNil(downloadResponse?.error) XCTAssertEqual(downloadResponse?.response?.statusCode, 204) @@ -477,7 +459,7 @@ class MultipleValidationTestCase: BaseTestCase { var downloadError: Error? // When - Alamofire.request(urlString) + AF.request(urlString) .validate(statusCode: 200..<300) .validate(contentType: ["application/json"]) .response { resp in @@ -485,7 +467,7 @@ class MultipleValidationTestCase: BaseTestCase { expectation1.fulfill() } - Alamofire.download(urlString) + AF.download(urlString) .validate(statusCode: 200..<300) .validate(contentType: ["application/json"]) .response { resp in @@ -511,7 +493,7 @@ class MultipleValidationTestCase: BaseTestCase { var downloadError: Error? // When - Alamofire.request(urlString) + AF.request(urlString) .validate(statusCode: 400..<600) .validate(contentType: ["application/octet-stream"]) .response { resp in @@ -519,7 +501,7 @@ class MultipleValidationTestCase: BaseTestCase { expectation1.fulfill() } - Alamofire.download(urlString) + AF.download(urlString) .validate(statusCode: 400..<600) .validate(contentType: ["application/octet-stream"]) .response { resp in @@ -534,7 +516,7 @@ class MultipleValidationTestCase: BaseTestCase { XCTAssertNotNil(downloadError) for error in [requestError, downloadError] { - if let error = error as? AFError { + if let error = error?.asAFError { XCTAssertTrue(error.isUnacceptableStatusCode) XCTAssertEqual(error.responseCode, 200) } else { @@ -554,7 +536,7 @@ class MultipleValidationTestCase: BaseTestCase { var downloadError: Error? // When - Alamofire.request(urlString) + AF.request(urlString) .validate(contentType: ["application/octet-stream"]) .validate(statusCode: 400..<600) .response { resp in @@ -562,7 +544,7 @@ class MultipleValidationTestCase: BaseTestCase { expectation1.fulfill() } - Alamofire.download(urlString) + AF.download(urlString) .validate(contentType: ["application/octet-stream"]) .validate(statusCode: 400..<600) .response { resp in @@ -577,7 +559,7 @@ class MultipleValidationTestCase: BaseTestCase { XCTAssertNotNil(downloadError) for error in [requestError, downloadError] { - if let error = error as? AFError { + if let error = error?.asAFError { XCTAssertTrue(error.isUnacceptableContentType) XCTAssertEqual(error.responseContentType, "application/xml") XCTAssertEqual(error.acceptableContentTypes?.first, "application/octet-stream") @@ -604,12 +586,12 @@ class AutomaticValidationTestCase: BaseTestCase { var downloadError: Error? // When - Alamofire.request(urlRequest).validate().response { resp in + AF.request(urlRequest).validate().response { resp in requestError = resp.error expectation1.fulfill() } - Alamofire.download(urlRequest).validate().response { resp in + AF.download(urlRequest).validate().response { resp in downloadError = resp.error expectation2.fulfill() } @@ -632,14 +614,14 @@ class AutomaticValidationTestCase: BaseTestCase { var downloadError: Error? // When - Alamofire.request(urlString) + AF.request(urlString) .validate() .response { resp in requestError = resp.error expectation1.fulfill() } - Alamofire.download(urlString) + AF.download(urlString) .validate() .response { resp in downloadError = resp.error @@ -653,7 +635,7 @@ class AutomaticValidationTestCase: BaseTestCase { XCTAssertNotNil(downloadError) for error in [requestError, downloadError] { - if let error = error as? AFError, let statusCode = error.responseCode { + if let error = error?.asAFError, let statusCode = error.responseCode { XCTAssertTrue(error.isUnacceptableStatusCode) XCTAssertEqual(statusCode, 404) } else { @@ -675,12 +657,12 @@ class AutomaticValidationTestCase: BaseTestCase { var downloadError: Error? // When - Alamofire.request(urlRequest).validate().response { resp in + AF.request(urlRequest).validate().response { resp in requestError = resp.error expectation1.fulfill() } - Alamofire.download(urlRequest).validate().response { resp in + AF.download(urlRequest).validate().response { resp in downloadError = resp.error expectation2.fulfill() } @@ -707,12 +689,12 @@ class AutomaticValidationTestCase: BaseTestCase { var downloadError: Error? // When - Alamofire.request(urlRequest).validate().response { resp in + AF.request(urlRequest).validate().response { resp in requestError = resp.error expectation1.fulfill() } - Alamofire.download(urlRequest).validate().response { resp in + AF.download(urlRequest).validate().response { resp in downloadError = resp.error expectation2.fulfill() } @@ -730,19 +712,19 @@ class AutomaticValidationTestCase: BaseTestCase { var urlRequest = URLRequest(url: url) urlRequest.setValue("application/json", forHTTPHeaderField: "Accept") - let expectation1 = self.expectation(description: "request should succeed and return xml") - let expectation2 = self.expectation(description: "download should succeed and return xml") + let expectation1 = expectation(description: "request should succeed and return xml") + let expectation2 = expectation(description: "download should succeed and return xml") var requestError: Error? var downloadError: Error? // When - Alamofire.request(urlRequest).validate().response { resp in + AF.request(urlRequest).validate().response { resp in requestError = resp.error expectation1.fulfill() } - Alamofire.download(urlRequest).validate().response { resp in + AF.download(urlRequest).validate().response { resp in downloadError = resp.error expectation2.fulfill() } @@ -754,7 +736,7 @@ class AutomaticValidationTestCase: BaseTestCase { XCTAssertNotNil(downloadError) for error in [requestError, downloadError] { - if let error = error as? AFError { + if let error = error?.asAFError { XCTAssertTrue(error.isUnacceptableContentType) XCTAssertEqual(error.responseContentType, "application/xml") XCTAssertEqual(error.acceptableContentTypes?.first, "application/json") @@ -775,7 +757,7 @@ extension DataRequest { func validateDataExists() -> Self { return validate { request, response, data in guard data != nil else { return .failure(ValidationError.missingData) } - return .success + return .success(Void()) } } @@ -786,14 +768,14 @@ extension DataRequest { extension DownloadRequest { func validateDataExists() -> Self { - return validate { request, response, _, _ in - let fileURL = self.downloadDelegate.fileURL + return validate { (request, response, _) in + let fileURL = self.fileURL guard let validFileURL = fileURL else { return .failure(ValidationError.missingFile) } do { let _ = try Data(contentsOf: validFileURL) - return .success + return .success(Void()) } catch { return .failure(ValidationError.fileReadFailed) } @@ -801,7 +783,7 @@ extension DownloadRequest { } func validate(with error: Error) -> Self { - return validate { _, _, _, _ in .failure(error) } + return validate { (_, _, _) in .failure(error) } } } @@ -819,23 +801,23 @@ class CustomValidationTestCase: BaseTestCase { var downloadError: Error? // When - Alamofire.request(urlString) + AF.request(urlString) .validate { request, response, data in guard data != nil else { return .failure(ValidationError.missingData) } - return .success + return .success(Void()) } .response { resp in requestError = resp.error expectation1.fulfill() } - Alamofire.download(urlString) - .validate { request, response, temporaryURL, destinationURL in - guard let fileURL = temporaryURL else { return .failure(ValidationError.missingFile) } + AF.download(urlString) + .validate { (request, response, fileURL) in + guard let fileURL = fileURL else { return .failure(ValidationError.missingFile) } do { - let _ = try Data(contentsOf: fileURL) - return .success + _ = try Data(contentsOf: fileURL) + return .success(Void()) } catch { return .failure(ValidationError.fileReadFailed) } @@ -863,7 +845,7 @@ class CustomValidationTestCase: BaseTestCase { var downloadError: Error? // When - Alamofire.request(urlString) + AF.request(urlString) .validate { _, _, _ in .failure(ValidationError.missingData) } .validate { _, _, _ in .failure(ValidationError.missingFile) } // should be ignored .response { resp in @@ -871,9 +853,9 @@ class CustomValidationTestCase: BaseTestCase { expectation1.fulfill() } - Alamofire.download(urlString) - .validate { _, _, _, _ in .failure(ValidationError.missingFile) } - .validate { _, _, _, _ in .failure(ValidationError.fileReadFailed) } // should be ignored + AF.download(urlString) + .validate { (_, _, _) in .failure(ValidationError.missingFile) } + .validate { (_, _, _) in .failure(ValidationError.fileReadFailed) } // should be ignored .response { resp in downloadError = resp.error expectation2.fulfill() @@ -897,14 +879,14 @@ class CustomValidationTestCase: BaseTestCase { var downloadError: Error? // When - Alamofire.request(urlString) + AF.request(urlString) .validateDataExists() .response { resp in requestError = resp.error expectation1.fulfill() } - Alamofire.download(urlString) + AF.download(urlString) .validateDataExists() .response { resp in downloadError = resp.error @@ -929,7 +911,7 @@ class CustomValidationTestCase: BaseTestCase { var downloadError: Error? // When - Alamofire.request(urlString) + AF.request(urlString) .validate(with: ValidationError.missingData) .validate(with: ValidationError.missingFile) // should be ignored .response { resp in @@ -937,7 +919,7 @@ class CustomValidationTestCase: BaseTestCase { expectation1.fulfill() } - Alamofire.download(urlString) + AF.download(urlString) .validate(with: ValidationError.missingFile) .validate(with: ValidationError.fileReadFailed) // should be ignored .response { resp in diff --git a/docs/Classes.html b/docs/Classes.html index e160f9185..e636a43ee 100644 --- a/docs/Classes.html +++ b/docs/Classes.html @@ -23,7 +23,7 @@ Alamofire Docs - (86% documented) + (77% documented)

@@ -59,18 +59,54 @@

- - + + - - -