// // SessionTests.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. // @testable import Alamofire import Foundation import XCTest final class SessionTestCase: BaseTestCase { // MARK: Helper Types private final class HTTPMethodAdapter: RequestInterceptor { let method: HTTPMethod let throwsError: Bool let adaptedCount = Protected(0) init(method: HTTPMethod, throwsError: Bool = false) { self.method = method self.throwsError = throwsError } func adapt(_ urlRequest: URLRequest, using state: RequestAdapterState, completion: @escaping (Result) -> Void) { adaptedCount.write { $0 += 1 } let result: Result = Result { guard !throwsError else { throw AFError.invalidURL(url: "") } var urlRequest = urlRequest urlRequest.httpMethod = method.rawValue return urlRequest } completion(result) } } private final class HeaderAdapter: RequestInterceptor { let headers: HTTPHeaders let throwsError: Bool let adaptedCount = Protected(0) init(headers: HTTPHeaders = ["field": "value"], throwsError: Bool = false) { self.headers = headers self.throwsError = throwsError } func adapt(_ urlRequest: URLRequest, using state: RequestAdapterState, completion: @escaping (Result) -> Void) { adaptedCount.write { $0 += 1 } let result: Result = Result { guard !throwsError else { throw AFError.invalidURL(url: "") } var urlRequest = urlRequest var finalHeaders = urlRequest.headers headers.forEach { finalHeaders.add($0) } urlRequest.headers = finalHeaders return urlRequest } completion(result) } } private final class RequestHandler: RequestInterceptor { struct MutableState { var adaptCalledCount = 0 var adaptedCount = 0 var retryCount = 0 var retryCalledCount = 0 var retryErrors: [any Error] = [] var shouldApplyAuthorizationHeader = false var throwsErrorOnFirstAdapt = false var throwsErrorOnSecondAdapt = false var throwsErrorOnRetry = false var shouldRetry = true var retryDelay: TimeInterval? } private let mutableState: Protected var adaptCalledCount: Int { mutableState.adaptCalledCount } var adaptedCount: Int { mutableState.adaptedCount } var retryCalledCount: Int { mutableState.retryCalledCount } var retryCount: Int { mutableState.retryCount } var retryErrors: [any Error] { mutableState.retryErrors } init(adaptedCount: Int = 0, throwsErrorOnSecondAdapt: Bool = false, throwsErrorOnRetry: Bool = false, shouldApplyAuthorizationHeader: Bool = false, shouldRetry: Bool = true, retryDelay: TimeInterval? = nil) { mutableState = Protected(.init(adaptedCount: adaptedCount, shouldApplyAuthorizationHeader: shouldApplyAuthorizationHeader, throwsErrorOnSecondAdapt: throwsErrorOnSecondAdapt, throwsErrorOnRetry: throwsErrorOnRetry, shouldRetry: shouldRetry, retryDelay: retryDelay)) } func adapt(_ urlRequest: URLRequest, using state: RequestAdapterState, completion: @escaping (Result) -> Void) { let result = mutableState.write { mutableState in mutableState.adaptCalledCount += 1 let result: Result = Result { if mutableState.throwsErrorOnFirstAdapt { mutableState.throwsErrorOnFirstAdapt = false throw AFError.invalidURL(url: "/adapt/error/1") } if mutableState.throwsErrorOnSecondAdapt && mutableState.adaptedCount == 1 { mutableState.throwsErrorOnSecondAdapt = false throw AFError.invalidURL(url: "/adapt/error/2") } var urlRequest = urlRequest mutableState.adaptedCount += 1 if mutableState.shouldApplyAuthorizationHeader && mutableState.adaptedCount > 1 { urlRequest.headers.update(.authorization(username: "user", password: "password")) } return urlRequest } return result } completion(result) } func retry(_ request: Request, for session: Session, dueTo error: any Error, completion: @escaping (RetryResult) -> Void) { let result: RetryResult = mutableState.write { mutableState in mutableState.retryCalledCount += 1 if mutableState.throwsErrorOnRetry { let error = AFError.invalidURL(url: "/invalid/url/\(mutableState.retryCalledCount)") return .doNotRetryWithError(error) } guard mutableState.shouldRetry else { return .doNotRetry } mutableState.retryCount += 1 mutableState.retryErrors.append(error) if mutableState.retryCount < 2 { if let retryDelay = mutableState.retryDelay { return .retryWithDelay(retryDelay) } else { return .retry } } else { return .doNotRetry } } completion(result) } } private final class UploadHandler: RequestInterceptor { struct MutableState { var adaptCalledCount = 0 var adaptedCount = 0 var retryCalledCount = 0 var retryCount = 0 var retryErrors: [any Error] = [] } private let mutableState = Protected(MutableState()) var adaptCalledCount: Int { mutableState.adaptCalledCount } var adaptedCount: Int { mutableState.adaptedCount } var retryCalledCount: Int { mutableState.retryCalledCount } var retryCount: Int { mutableState.retryCount } var retryErrors: [any Error] { mutableState.retryErrors } func adapt(_ urlRequest: URLRequest, using state: RequestAdapterState, completion: @escaping (Result) -> Void) { let result: Result = mutableState.write { mutableState in mutableState.adaptCalledCount += 1 return Result { mutableState.adaptedCount += 1 if mutableState.adaptedCount == 1 { throw AFError.invalidURL(url: "") } return urlRequest } } completion(result) } func retry(_ request: Request, for session: Session, dueTo error: any Error, completion: @escaping (RetryResult) -> Void) { mutableState.write { mutableState in mutableState.retryCalledCount += 1 mutableState.retryCount += 1 mutableState.retryErrors.append(error) } completion(.retry) } } // MARK: Tests - Initialization @MainActor func testInitializerWithDefaultArguments() { // Given, When let session = Session() // Then XCTAssertNotNil(session.session.delegate, "session delegate should not be nil") XCTAssertTrue(session.delegate === session.session.delegate, "manager delegate should equal session delegate") XCTAssertNil(session.serverTrustManager, "session server trust policy manager should be nil") } @MainActor func testInitializerWithSpecifiedArguments() { // Given let configuration = URLSessionConfiguration.default let delegate = SessionDelegate() let serverTrustManager = ServerTrustManager(evaluators: [:]) // When let session = Session(configuration: configuration, delegate: delegate, serverTrustManager: serverTrustManager) // Then XCTAssertNotNil(session.session.delegate, "session delegate should not be nil") XCTAssertTrue(session.delegate === session.session.delegate, "manager delegate should equal session delegate") XCTAssertNotNil(session.serverTrustManager, "session server trust policy manager should not be nil") } @MainActor func testThatSessionInitializerSucceedsWithDefaultArguments() { // Given let delegate = SessionDelegate() let underlyingQueue = DispatchQueue(label: "underlyingQueue") let urlSession: URLSession = { let configuration = URLSessionConfiguration.default let queue = OperationQueue(underlyingQueue: underlyingQueue, name: "delegateQueue") return URLSession(configuration: configuration, delegate: delegate, delegateQueue: queue) }() // When let session = Session(session: urlSession, delegate: delegate, rootQueue: underlyingQueue) // Then XCTAssertTrue(session.delegate === session.session.delegate, "manager delegate should equal session delegate") XCTAssertNil(session.serverTrustManager, "session server trust policy manager should be nil") } @MainActor func testThatSessionInitializerSucceedsWithSpecifiedArguments() { // Given let delegate = SessionDelegate() let underlyingQueue = DispatchQueue(label: "underlyingQueue") let urlSession: URLSession = { let configuration = URLSessionConfiguration.default let queue = OperationQueue(underlyingQueue: underlyingQueue, name: "delegateQueue") return URLSession(configuration: configuration, delegate: delegate, delegateQueue: queue) }() let serverTrustManager = ServerTrustManager(evaluators: [:]) // When let session = Session(session: urlSession, delegate: delegate, rootQueue: underlyingQueue, serverTrustManager: serverTrustManager) // Then XCTAssertTrue(session.delegate === session.session.delegate, "manager delegate should equal session delegate") XCTAssertNotNil(session.serverTrustManager, "session server trust policy manager should not be nil") } // MARK: Tests - Parallel Root Queue @MainActor func testThatSessionWorksCorrectlyWhenPassedAConcurrentRootQueue() { // Given let queue = DispatchQueue(label: "ohNoAParallelQueue", attributes: .concurrent) let session = Session(rootQueue: queue) let didFinish = expectation(description: "request did finish") var receivedResponse: TestResponse? // When session.request(.get).responseDecodable(of: TestResponse.self) { response in receivedResponse = response.value didFinish.fulfill() } waitForExpectations(timeout: timeout) // Then XCTAssertNotNil(receivedResponse, "Should receive TestResponse.") } // MARK: Tests - Default HTTP Headers @MainActor func testDefaultUserAgentHeader() { // Given, When let userAgent = HTTPHeaders.default["User-Agent"] // Then let osNameVersion: String = { let version = ProcessInfo.processInfo.operatingSystemVersion let versionString = "\(version.majorVersion).\(version.minorVersion).\(version.patchVersion)" let osName: String = { #if os(iOS) #if targetEnvironment(macCatalyst) return "macOS(Catalyst)" #else return "iOS" #endif #elseif os(watchOS) return "watchOS" #elseif os(tvOS) return "tvOS" #elseif os(macOS) #if targetEnvironment(macCatalyst) return "macOS(Catalyst)" #else return "macOS" #endif #elseif os(visionOS) return "visionOS" #elseif os(Linux) return "Linux" #elseif os(Windows) return "Windows" #elseif os(Android) return "Android" #elseif os(WASI) return "WASI" #else return "Unknown" #endif }() return "\(osName) \(versionString)" }() let alamofireVersion = "Alamofire/\(AFInfo.version)" XCTAssertTrue(userAgent?.contains(alamofireVersion) == true) XCTAssertTrue(userAgent?.contains(osNameVersion) == true) XCTAssertTrue(userAgent?.contains("xctest/") == true) } // MARK: Tests - Supported Accept-Encodings // Disabled due to HTTPBin flakiness. @MainActor func disabled_testDefaultAcceptEncodingSupportsAppropriateEncodingsOnAppropriateSystems() { // Given 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(.compression(.brotli)).responseDecodable(of: TestResponse.self) { response in brotliResponse = response brotliExpectation.fulfill() } AF.request(.compression(.gzip)).responseDecodable(of: TestResponse.self) { response in gzipResponse = response gzipExpectation.fulfill() } AF.request(.compression(.deflate)).responseDecodable(of: TestResponse.self) { response in deflateResponse = response deflateExpectation.fulfill() } waitForExpectations(timeout: timeout) // 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 @MainActor func testSetStartRequestsImmediatelyToFalseAndResumeRequest() { // Given let session = Session(startRequestsImmediately: false) let url = Endpoint().url let urlRequest = URLRequest(url: url) let expectation = expectation(description: "\(url)") var response: HTTPURLResponse? // When session.request(urlRequest) .response { resp in response = resp.response expectation.fulfill() } .resume() waitForExpectations(timeout: timeout) // Then XCTAssertNotNil(response, "response should not be nil") XCTAssertTrue(response?.statusCode == 200, "response status code should be 200") } @MainActor func testSetStartRequestsImmediatelyToFalseAndCancelledCallsResponseHandlers() { // Given let session = Session(startRequestsImmediately: false) let url = Endpoint().url let urlRequest = URLRequest(url: url) let expectation = expectation(description: "\(url)") var response: DataResponse? // When let request = session.request(urlRequest) .cancel() .response { resp in response = resp expectation.fulfill() } waitForExpectations(timeout: timeout) // Then XCTAssertNotNil(response, "response should not be nil") XCTAssertTrue(request.isCancelled) XCTAssertTrue((request.task == nil) || (request.task?.state == .canceling || request.task?.state == .completed)) XCTAssertEqual(request.error?.isExplicitlyCancelledError, true) } @MainActor func testSetStartRequestsImmediatelyToFalseAndResumeThenCancelRequestHasCorrectOutput() { // Given let session = Session(startRequestsImmediately: false) let url = Endpoint().url let urlRequest = URLRequest(url: url) let expectation = expectation(description: "\(url)") var response: DataResponse? // When let request = session.request(urlRequest) .response { resp in response = resp expectation.fulfill() } .resume() .cancel() waitForExpectations(timeout: timeout) // Then XCTAssertNotNil(response, "response should not be nil") XCTAssertTrue(request.isCancelled) XCTAssertTrue((request.task == nil) || (request.task?.state == .canceling || request.task?.state == .completed)) XCTAssertEqual(request.error?.isExplicitlyCancelledError, true) } @MainActor func testSetStartRequestsImmediatelyToFalseAndCancelThenResumeRequestDoesntCreateTaskAndStaysCancelled() { // Given let session = Session(startRequestsImmediately: false) let url = Endpoint().url let urlRequest = URLRequest(url: url) let expectation = expectation(description: "\(url)") var response: DataResponse? // When let request = session.request(urlRequest) .response { resp in response = resp expectation.fulfill() } .cancel() .resume() waitForExpectations(timeout: timeout) // Then XCTAssertNotNil(response, "response should not be nil") XCTAssertTrue(request.isCancelled) XCTAssertTrue((request.task == nil) || (request.task?.state == .canceling || request.task?.state == .completed)) XCTAssertEqual(request.error?.isExplicitlyCancelledError, true) } // MARK: Tests - Deinitialization @MainActor func testReleasingManagerWithPendingRequestDeinitializesSuccessfully() { // Given let monitor = ClosureEventMonitor() let didCreateRequest = expectation(description: "Request created") monitor.requestDidCreateTask = { _, _ in didCreateRequest.fulfill() } var session: Session? = Session(startRequestsImmediately: false, eventMonitors: [monitor]) weak var weakSession = session // When let request = session?.request(.default) session = nil waitForExpectations(timeout: timeout) // Then if #available(macOS 13, iOS 16, tvOS 16, watchOS 9, *) { // On 2022 OS versions and later, URLSessionTasks are completed even if not resumed before invalidating a session. XCTAssertTrue([.canceling, .completed].contains(request?.task?.state)) } else { XCTAssertEqual(request?.task?.state, .suspended) } XCTAssertNil(session, "session should be nil") XCTAssertNil(weakSession, "weak session should be nil") } func testReleasingManagerWithPendingCanceledRequestDeinitializesSuccessfully() { // Given var session: Session? = Session(startRequestsImmediately: false) // When let request = session?.request(.default) request?.cancel() session = nil let state = request?.state // Then XCTAssertTrue(state == .cancelled, "state should be .cancelled") XCTAssertNil(session, "manager should be nil") } // MARK: Tests - Bad Requests @MainActor func testThatDataRequestWithInvalidURLStringThrowsResponseHandlerError() { // Given let session = Session() let url = "" let expectation = expectation(description: "Request should fail with error") var response: DataResponse? // When session.request(url).response { resp in response = resp expectation.fulfill() } waitForExpectations(timeout: timeout) // Then XCTAssertNil(response?.request) XCTAssertNil(response?.response) XCTAssertNil(response?.data) XCTAssertNotNil(response?.error) XCTAssertEqual(response?.error?.isInvalidURLError, true) XCTAssertEqual(response?.error?.urlConvertible as? String, url) } @MainActor func testThatDownloadRequestWithInvalidURLStringThrowsResponseHandlerError() { // Given let session = Session() let url = "" let expectation = expectation(description: "Download should fail with error") var response: DownloadResponse? // When session.download(url).response { resp in response = resp expectation.fulfill() } waitForExpectations(timeout: timeout) // Then XCTAssertNil(response?.request) XCTAssertNil(response?.response) XCTAssertNil(response?.fileURL) XCTAssertNil(response?.resumeData) XCTAssertNotNil(response?.error) XCTAssertEqual(response?.error?.isInvalidURLError, true) XCTAssertEqual(response?.error?.urlConvertible as? String, url) } @MainActor func testThatUploadDataRequestWithInvalidURLStringThrowsResponseHandlerError() { // Given let session = Session() let url = "" let expectation = expectation(description: "Upload should fail with error") var response: DataResponse? // When session.upload(Data(), to: url).response { resp in response = resp expectation.fulfill() } waitForExpectations(timeout: timeout) // Then XCTAssertNil(response?.request) XCTAssertNil(response?.response) XCTAssertNil(response?.data) XCTAssertNotNil(response?.error) XCTAssertEqual(response?.error?.isInvalidURLError, true) XCTAssertEqual(response?.error?.urlConvertible as? String, url) } @MainActor func testThatUploadFileRequestWithInvalidURLStringThrowsResponseHandlerError() { // Given let session = Session() let url = "" let expectation = expectation(description: "Upload should fail with error") var response: DataResponse? // When session.upload(URL(fileURLWithPath: "/invalid"), to: url).response { resp in response = resp expectation.fulfill() } waitForExpectations(timeout: timeout) // Then XCTAssertNil(response?.request) XCTAssertNil(response?.response) XCTAssertNil(response?.data) XCTAssertNotNil(response?.error) XCTAssertEqual(response?.error?.isInvalidURLError, true) XCTAssertEqual(response?.error?.urlConvertible as? String, url) } @MainActor func testThatUploadStreamRequestWithInvalidURLStringThrowsResponseHandlerError() { // Given let session = Session() let url = "" let expectation = expectation(description: "Upload should fail with error") var response: DataResponse? // When session.upload(InputStream(data: Data()), to: url).response { resp in response = resp expectation.fulfill() } waitForExpectations(timeout: timeout) // Then XCTAssertNil(response?.request) XCTAssertNil(response?.response) XCTAssertNil(response?.data) XCTAssertNotNil(response?.error) XCTAssertEqual(response?.error?.isInvalidURLError, true) XCTAssertEqual(response?.error?.urlConvertible as? String, url) } // MARK: Tests - Request Adapter @MainActor func testThatSessionCallsRequestAdaptersWhenCreatingDataRequest() { // Given let endpoint = Endpoint() let methodAdapter = HTTPMethodAdapter(method: .post) let headerAdapter = HeaderAdapter() let monitor = ClosureEventMonitor() let session = Session(startRequestsImmediately: false, interceptor: methodAdapter, eventMonitors: [monitor]) // When let expectation1 = expectation(description: "Request 1 created") monitor.requestDidCreateTask = { _, _ in expectation1.fulfill() } let request1 = session.request(endpoint) waitForExpectations(timeout: timeout) let expectation2 = expectation(description: "Request 2 created") monitor.requestDidCreateTask = { _, _ in expectation2.fulfill() } let request2 = session.request(endpoint, interceptor: headerAdapter) waitForExpectations(timeout: timeout) // Then XCTAssertEqual(request1.task?.originalRequest?.httpMethod, methodAdapter.method.rawValue) XCTAssertEqual(request2.task?.originalRequest?.httpMethod, methodAdapter.method.rawValue) XCTAssertEqual(request2.task?.originalRequest?.allHTTPHeaderFields?.count, 1) XCTAssertEqual(methodAdapter.adaptedCount.value, 2) XCTAssertEqual(headerAdapter.adaptedCount.value, 1) } @MainActor func testThatSessionCallsRequestAdaptersWhenCreatingDownloadRequest() { // Given let endpoint = Endpoint() let methodAdapter = HTTPMethodAdapter(method: .post) let headerAdapter = HeaderAdapter() let monitor = ClosureEventMonitor() let session = Session(startRequestsImmediately: false, interceptor: methodAdapter, eventMonitors: [monitor]) // When let expectation1 = expectation(description: "Request 1 created") monitor.requestDidCreateTask = { _, _ in expectation1.fulfill() } let request1 = session.download(endpoint) waitForExpectations(timeout: timeout) let expectation2 = expectation(description: "Request 2 created") monitor.requestDidCreateTask = { _, _ in expectation2.fulfill() } let request2 = session.download(endpoint, interceptor: headerAdapter) waitForExpectations(timeout: timeout) // Then XCTAssertEqual(request1.task?.originalRequest?.httpMethod, methodAdapter.method.rawValue) XCTAssertEqual(request2.task?.originalRequest?.httpMethod, methodAdapter.method.rawValue) XCTAssertEqual(request2.task?.originalRequest?.allHTTPHeaderFields?.count, 1) XCTAssertEqual(methodAdapter.adaptedCount.value, 2) XCTAssertEqual(headerAdapter.adaptedCount.value, 1) } @MainActor func testThatSessionCallsRequestAdaptersWhenCreatingUploadRequestWithData() { // Given let data = Data("data".utf8) let endpoint = Endpoint.method(.post) let methodAdapter = HTTPMethodAdapter(method: .get) let headerAdapter = HeaderAdapter() let monitor = ClosureEventMonitor() let session = Session(startRequestsImmediately: false, interceptor: methodAdapter, eventMonitors: [monitor]) // When let expectation1 = expectation(description: "Request 1 created") monitor.requestDidCreateTask = { _, _ in expectation1.fulfill() } let request1 = session.upload(data, to: endpoint) waitForExpectations(timeout: timeout) let expectation2 = expectation(description: "Request 2 created") monitor.requestDidCreateTask = { _, _ in expectation2.fulfill() } let request2 = session.upload(data, to: endpoint, interceptor: headerAdapter) waitForExpectations(timeout: timeout) // Then XCTAssertEqual(request1.task?.originalRequest?.httpMethod, methodAdapter.method.rawValue) XCTAssertEqual(request2.task?.originalRequest?.httpMethod, methodAdapter.method.rawValue) XCTAssertEqual(request2.task?.originalRequest?.allHTTPHeaderFields?.count, 1) XCTAssertEqual(methodAdapter.adaptedCount.value, 2) XCTAssertEqual(headerAdapter.adaptedCount.value, 1) } @MainActor func testThatSessionCallsRequestAdaptersWhenCreatingUploadRequestWithFile() { // Given let fileURL = URL(fileURLWithPath: "/path/to/some/file.txt") let endpoint = Endpoint.method(.post) let methodAdapter = HTTPMethodAdapter(method: .get) let headerAdapter = HeaderAdapter() let monitor = ClosureEventMonitor() let session = Session(startRequestsImmediately: false, interceptor: methodAdapter, eventMonitors: [monitor]) // When let expectation1 = expectation(description: "Request 1 created") monitor.requestDidCreateTask = { _, _ in expectation1.fulfill() } let request1 = session.upload(fileURL, to: endpoint) waitForExpectations(timeout: timeout) let expectation2 = expectation(description: "Request 2 created") monitor.requestDidCreateTask = { _, _ in expectation2.fulfill() } let request2 = session.upload(fileURL, to: endpoint, interceptor: headerAdapter) waitForExpectations(timeout: timeout) // Then XCTAssertEqual(request1.task?.originalRequest?.httpMethod, methodAdapter.method.rawValue) XCTAssertEqual(request2.task?.originalRequest?.httpMethod, methodAdapter.method.rawValue) XCTAssertEqual(request2.task?.originalRequest?.allHTTPHeaderFields?.count, 1) XCTAssertEqual(methodAdapter.adaptedCount.value, 2) XCTAssertEqual(headerAdapter.adaptedCount.value, 1) } @MainActor func testThatSessionCallsRequestAdaptersWhenCreatingUploadRequestWithInputStream() { // Given let inputStream = InputStream(data: Data("data".utf8)) let endpoint = Endpoint.method(.post) let methodAdapter = HTTPMethodAdapter(method: .get) let headerAdapter = HeaderAdapter() let monitor = ClosureEventMonitor() let session = Session(startRequestsImmediately: false, interceptor: methodAdapter, eventMonitors: [monitor]) // When let expectation1 = expectation(description: "Request 1 created") monitor.requestDidCreateTask = { _, _ in expectation1.fulfill() } let request1 = session.upload(inputStream, to: endpoint) waitForExpectations(timeout: timeout) let expectation2 = expectation(description: "Request 2 created") monitor.requestDidCreateTask = { _, _ in expectation2.fulfill() } let request2 = session.upload(inputStream, to: endpoint, interceptor: headerAdapter) waitForExpectations(timeout: timeout) // Then XCTAssertEqual(request1.task?.originalRequest?.httpMethod, methodAdapter.method.rawValue) XCTAssertEqual(request2.task?.originalRequest?.httpMethod, methodAdapter.method.rawValue) XCTAssertEqual(request2.task?.originalRequest?.allHTTPHeaderFields?.count, 1) XCTAssertEqual(methodAdapter.adaptedCount.value, 2) XCTAssertEqual(headerAdapter.adaptedCount.value, 1) } @MainActor func testThatSessionReturnsRequestAdaptationErrorWhenRequestAdapterThrowsError() { // Given let endpoint = Endpoint() let methodAdapter = HTTPMethodAdapter(method: .post, throwsError: true) let headerAdapter = HeaderAdapter(throwsError: true) let monitor = ClosureEventMonitor() let session = Session(startRequestsImmediately: false, interceptor: methodAdapter, eventMonitors: [monitor]) // When let expectation1 = expectation(description: "Request 1 created") monitor.requestDidFailToAdaptURLRequestWithError = { _, _, _ in expectation1.fulfill() } let request1 = session.request(endpoint) waitForExpectations(timeout: timeout) let expectation2 = expectation(description: "Request 2 created") monitor.requestDidFailToAdaptURLRequestWithError = { _, _, _ in expectation2.fulfill() } let request2 = session.request(endpoint, interceptor: headerAdapter) waitForExpectations(timeout: timeout) let requests = [request1, request2] // Then for request in requests { XCTAssertEqual(request.error?.isRequestAdaptationError, true) XCTAssertEqual(request.error?.underlyingError?.asAFError?.urlConvertible as? String, "") } } // MARK: Tests - Request Retrier @MainActor func testThatSessionCallsRequestRetrierWhenRequestInitiallyEncountersAdaptError() { // Given let handler = RequestHandler(adaptedCount: 1, throwsErrorOnSecondAdapt: true, shouldApplyAuthorizationHeader: true) let session = Session() let expectation = expectation(description: "request should eventually fail") var response: DataResponse? // When session.request(.basicAuth(), interceptor: handler) .validate() .responseDecodable(of: TestResponse.self) { jsonResponse in response = jsonResponse expectation.fulfill() } waitForExpectations(timeout: timeout) // Then XCTAssertEqual(handler.adaptCalledCount, 2) XCTAssertEqual(handler.adaptedCount, 2) XCTAssertEqual(handler.retryCalledCount, 1) XCTAssertEqual(handler.retryCount, 1) XCTAssertEqual(response?.result.isSuccess, true) assert(on: session.rootQueue) { XCTAssertTrue(session.requestTaskMap.isEmpty) XCTAssertTrue(session.activeRequests.isEmpty) } } @MainActor func testThatSessionCallsRequestRetrierWhenDownloadInitiallyEncountersAdaptError() { // Given let handler = RequestHandler(adaptedCount: 1, throwsErrorOnSecondAdapt: true, shouldApplyAuthorizationHeader: true) let session = Session() let expectation = expectation(description: "request should eventually fail") var response: DownloadResponse? let destination: DownloadRequest.Destination = { _, _ in let fileURL = self.testDirectoryURL.appendingPathComponent("test-output.json") return (fileURL, [.removePreviousFile]) } // When session.download(.basicAuth(), interceptor: handler, to: destination) .validate() .responseDecodable(of: TestResponse.self) { jsonResponse in response = jsonResponse expectation.fulfill() } waitForExpectations(timeout: timeout) // Then XCTAssertEqual(handler.adaptCalledCount, 2) XCTAssertEqual(handler.adaptedCount, 2) XCTAssertEqual(handler.retryCalledCount, 1) XCTAssertEqual(handler.retryCount, 1) XCTAssertEqual(response?.result.isSuccess, true) assert(on: session.rootQueue) { XCTAssertTrue(session.requestTaskMap.isEmpty) XCTAssertTrue(session.activeRequests.isEmpty) } } @MainActor func testThatSessionCallsRequestRetrierWhenUploadInitiallyEncountersAdaptError() { // Given let handler = UploadHandler() let session = Session(interceptor: handler) let expectation = expectation(description: "request should eventually fail") var response: DataResponse? let uploadData = Data("upload data".utf8) // When session.upload(uploadData, to: .method(.post)) .validate() .responseDecodable(of: TestResponse.self) { jsonResponse in response = jsonResponse expectation.fulfill() } waitForExpectations(timeout: timeout) // Then XCTAssertEqual(handler.adaptCalledCount, 2) XCTAssertEqual(handler.adaptedCount, 2) XCTAssertEqual(handler.retryCalledCount, 1) XCTAssertEqual(handler.retryCount, 1) XCTAssertEqual(response?.result.isSuccess, true) assert(on: session.rootQueue) { XCTAssertTrue(session.requestTaskMap.isEmpty) XCTAssertTrue(session.activeRequests.isEmpty) } } @MainActor func testThatSessionCallsRequestRetrierWhenRequestEncountersError() { // Given let handler = RequestHandler() let session = Session() let expectation = expectation(description: "request should eventually fail") var response: DataResponse? // When let request = session.request(.basicAuth(), interceptor: handler) .validate() .responseDecodable(of: TestResponse.self) { jsonResponse in response = jsonResponse expectation.fulfill() } waitForExpectations(timeout: timeout) // Then XCTAssertEqual(handler.adaptCalledCount, 2) XCTAssertEqual(handler.adaptedCount, 2) XCTAssertEqual(handler.retryCalledCount, 3) XCTAssertEqual(handler.retryCount, 3) XCTAssertEqual(request.retryCount, 1) XCTAssertEqual(response?.result.isSuccess, false) assert(on: session.rootQueue) { XCTAssertTrue(session.requestTaskMap.isEmpty) XCTAssertTrue(session.activeRequests.isEmpty) } } @MainActor func testThatSessionCallsRequestRetrierThenSessionRetrierWhenRequestEncountersError() { // Given let sessionHandler = RequestHandler() let requestHandler = RequestHandler() let session = Session(interceptor: sessionHandler) let expectation = expectation(description: "request should eventually fail") var response: DataResponse? // When let request = session.request(.basicAuth(), interceptor: requestHandler) .validate() .responseDecodable(of: TestResponse.self) { jsonResponse in response = jsonResponse expectation.fulfill() } waitForExpectations(timeout: timeout) // Then XCTAssertEqual(sessionHandler.adaptCalledCount, 3) XCTAssertEqual(sessionHandler.adaptedCount, 3) XCTAssertEqual(sessionHandler.retryCalledCount, 3) XCTAssertEqual(sessionHandler.retryCount, 3) XCTAssertEqual(requestHandler.adaptCalledCount, 3) XCTAssertEqual(requestHandler.adaptedCount, 3) XCTAssertEqual(requestHandler.retryCalledCount, 4) XCTAssertEqual(requestHandler.retryCount, 4) XCTAssertEqual(request.retryCount, 2) XCTAssertEqual(response?.result.isSuccess, false) assert(on: session.rootQueue) { XCTAssertTrue(session.requestTaskMap.isEmpty) XCTAssertTrue(session.activeRequests.isEmpty) } } @MainActor func testThatSessionCallsAdapterWhenRequestIsRetried() { // Given let handler = RequestHandler(shouldApplyAuthorizationHeader: true) let session = Session(interceptor: handler) let expectation = expectation(description: "request should eventually succeed") var response: DataResponse? // When let request = session.request(.basicAuth()) .validate() .responseDecodable(of: TestResponse.self) { jsonResponse in response = jsonResponse expectation.fulfill() } waitForExpectations(timeout: timeout) // Then XCTAssertEqual(handler.adaptCalledCount, 2) XCTAssertEqual(handler.adaptedCount, 2) XCTAssertEqual(handler.retryCalledCount, 1) XCTAssertEqual(handler.retryCount, 1) XCTAssertEqual(request.retryCount, 1) XCTAssertEqual(response?.result.isSuccess, true) assert(on: session.rootQueue) { XCTAssertTrue(session.requestTaskMap.isEmpty) XCTAssertTrue(session.activeRequests.isEmpty) } } @MainActor func testThatSessionReturnsRequestAdaptationErrorWhenRequestIsRetried() { // Given let handler = RequestHandler(throwsErrorOnSecondAdapt: true) let session = Session(interceptor: handler) let expectation = expectation(description: "request should eventually fail") var response: DataResponse? // When let request = session.request(.basicAuth()) .validate() .responseDecodable(of: TestResponse.self) { jsonResponse in response = jsonResponse expectation.fulfill() } waitForExpectations(timeout: timeout) // Then XCTAssertEqual(handler.adaptCalledCount, 2) XCTAssertEqual(handler.adaptedCount, 1) XCTAssertEqual(handler.retryCalledCount, 3) XCTAssertEqual(handler.retryCount, 3) XCTAssertEqual(request.retryCount, 1) XCTAssertEqual(response?.result.isSuccess, false) XCTAssertEqual(request.error?.isRequestAdaptationError, true) XCTAssertEqual(request.error?.underlyingError?.asAFError?.urlConvertible as? String, "/adapt/error/2") assert(on: session.rootQueue) { XCTAssertTrue(session.requestTaskMap.isEmpty) XCTAssertTrue(session.activeRequests.isEmpty) } } @MainActor func testThatSessionRetriesRequestWithDelayWhenRetryResultContainsDelay() { // Given let handler = RequestHandler(throwsErrorOnSecondAdapt: true, retryDelay: 0.01) let session = Session(interceptor: handler) let expectation = expectation(description: "request should eventually fail") var response: DataResponse? // When let request = session.request(.basicAuth()) .validate() .responseDecodable(of: TestResponse.self) { jsonResponse in response = jsonResponse expectation.fulfill() } waitForExpectations(timeout: timeout) // Then XCTAssertEqual(handler.adaptCalledCount, 2) XCTAssertEqual(handler.adaptedCount, 1) XCTAssertEqual(handler.retryCalledCount, 3) XCTAssertEqual(handler.retryCount, 3) XCTAssertEqual(request.retryCount, 1) XCTAssertEqual(response?.result.isSuccess, false) XCTAssertEqual(request.error?.isRequestAdaptationError, true) XCTAssertEqual(request.error?.underlyingError?.asAFError?.urlConvertible as? String, "/adapt/error/2") assert(on: session.rootQueue) { XCTAssertTrue(session.requestTaskMap.isEmpty) XCTAssertTrue(session.activeRequests.isEmpty) } } @MainActor func testThatSessionReturnsRequestRetryErrorWhenRequestRetrierThrowsError() { // Given let handler = RequestHandler(throwsErrorOnRetry: true) let session = Session(interceptor: handler) let expectation = expectation(description: "request should eventually fail") var response: DataResponse? // When let request = session.request(.basicAuth()) .validate() .responseDecodable(of: TestResponse.self) { jsonResponse in response = jsonResponse expectation.fulfill() } waitForExpectations(timeout: timeout) // Then XCTAssertEqual(handler.adaptCalledCount, 1) XCTAssertEqual(handler.adaptedCount, 1) XCTAssertEqual(handler.retryCalledCount, 2) XCTAssertEqual(handler.retryCount, 0) XCTAssertEqual(request.retryCount, 0) XCTAssertEqual(response?.result.isSuccess, false) assert(on: session.rootQueue) { XCTAssertTrue(session.requestTaskMap.isEmpty) XCTAssertTrue(session.activeRequests.isEmpty) } if let error = response?.result.failure { XCTAssertTrue(error.isRequestRetryError) XCTAssertEqual(error.underlyingError?.asAFError?.urlConvertible as? String, "/invalid/url/2") } else { XCTFail("error should not be nil") } } // MARK: Tests - Response Serializer Retry @MainActor func testThatSessionCallsRequestRetrierWhenResponseSerializerThrowsError() { // Given let handler = RequestHandler(shouldRetry: false) let session = Session() let expectation = expectation(description: "request should eventually fail") var response: DataResponse? // When let request = session.request(.image(.jpeg), interceptor: handler) .validate() .responseDecodable(of: TestResponse.self) { jsonResponse in response = jsonResponse expectation.fulfill() } waitForExpectations(timeout: timeout) // Then XCTAssertEqual(handler.adaptCalledCount, 1) XCTAssertEqual(handler.adaptedCount, 1) XCTAssertEqual(handler.retryCalledCount, 1) XCTAssertEqual(handler.retryCount, 0) XCTAssertEqual(request.retryCount, 0) XCTAssertEqual(response?.result.isSuccess, false) XCTAssertEqual(response?.error?.isResponseSerializationError, true) XCTAssertNotNil(response?.error?.underlyingError as? DecodingError) assert(on: session.rootQueue) { XCTAssertTrue(session.requestTaskMap.isEmpty) XCTAssertTrue(session.activeRequests.isEmpty) } } @MainActor func testThatSessionCallsRequestRetrierForAllResponseSerializersThatThrowError() throws { // Given let handler = RequestHandler(throwsErrorOnRetry: true) let session = Session() let json1Expectation = expectation(description: "request should eventually fail") var json1Response: DataResponse? let json2Expectation = expectation(description: "request should eventually fail") var json2Response: DataResponse? // When let request = session.request(.image(.jpeg), interceptor: handler) .validate() .responseDecodable(of: TestResponse.self) { response in json1Response = response json1Expectation.fulfill() } .responseDecodable(of: TestResponse.self) { response in json2Response = response json2Expectation.fulfill() } waitForExpectations(timeout: timeout) // Then XCTAssertEqual(handler.adaptCalledCount, 1) XCTAssertEqual(handler.adaptedCount, 1) XCTAssertEqual(handler.retryCalledCount, 2) XCTAssertEqual(handler.retryCount, 0) XCTAssertEqual(request.retryCount, 0) XCTAssertEqual(json1Response?.result.isSuccess, false) XCTAssertEqual(json2Response?.result.isSuccess, false) assert(on: session.rootQueue) { XCTAssertTrue(session.requestTaskMap.isEmpty) XCTAssertTrue(session.activeRequests.isEmpty) } let errors = [json1Response, json2Response].compactMap { $0?.error } XCTAssertEqual(errors.count, 2) for (index, error) in errors.enumerated() { XCTAssertTrue(error.isRequestRetryError) if case let .requestRetryFailed(retryError, originalError) = error { XCTAssertEqual(retryError.asAFError?.urlConvertible as? String, "/invalid/url/\(index + 1)") XCTAssertNotNil(originalError.asAFError?.underlyingError as? DecodingError) } else { XCTFail("Error failure reason should be response serialization failure") } } } @MainActor func testThatSessionRetriesRequestImmediatelyWhenResponseSerializerRequestsRetry() throws { // Given let handler = RequestHandler() let session = Session() let json1Expectation = expectation(description: "request should eventually fail") var json1Response: DataResponse? let json2Expectation = expectation(description: "request should eventually fail") var json2Response: DataResponse? // When let request = session.request(.image(.jpeg), interceptor: handler) .validate() .responseDecodable(of: TestResponse.self) { response in json1Response = response json1Expectation.fulfill() } .responseDecodable(of: TestResponse.self) { response in json2Response = response json2Expectation.fulfill() } waitForExpectations(timeout: timeout) // Then XCTAssertEqual(handler.adaptCalledCount, 2) XCTAssertEqual(handler.adaptedCount, 2) XCTAssertEqual(handler.retryCalledCount, 3) XCTAssertEqual(handler.retryCount, 3) XCTAssertEqual(request.retryCount, 1) XCTAssertEqual(json1Response?.result.isSuccess, false) XCTAssertEqual(json2Response?.result.isSuccess, false) assert(on: session.rootQueue) { XCTAssertTrue(session.requestTaskMap.isEmpty) XCTAssertTrue(session.activeRequests.isEmpty) } let errors = [json1Response, json2Response].compactMap { $0?.error } XCTAssertEqual(errors.count, 2) for error in errors { XCTAssertTrue(error.isResponseSerializationError) XCTAssertNotNil(error.underlyingError as? DecodingError) } } @MainActor func testThatSessionCallsResponseSerializerCompletionsWhenAdapterThrowsErrorDuringRetry() { // Four retries should occur given this scenario: // 1) Retrier is called from first response serializer failure (trips retry) // 2) Retrier is called by Session for adapt error thrown // 3) Retrier is called again from first response serializer failure // 4) Retrier is called from second response serializer failure // Given let handler = RequestHandler(throwsErrorOnSecondAdapt: true) let session = Session() let json1Expectation = expectation(description: "request should eventually fail") var json1Response: DataResponse? let json2Expectation = expectation(description: "request should eventually fail") var json2Response: DataResponse? // When let request = session.request(.image(.jpeg), interceptor: handler) .validate() .responseDecodable(of: TestResponse.self) { response in json1Response = response json1Expectation.fulfill() } .responseDecodable(of: TestResponse.self) { response in json2Response = response json2Expectation.fulfill() } waitForExpectations(timeout: timeout) // Then XCTAssertEqual(handler.adaptCalledCount, 2) XCTAssertEqual(handler.adaptedCount, 1) XCTAssertEqual(handler.retryCalledCount, 4) XCTAssertEqual(handler.retryCount, 4) XCTAssertEqual(request.retryCount, 1) XCTAssertEqual(json1Response?.result.isSuccess, false) XCTAssertEqual(json2Response?.result.isSuccess, false) assert(on: session.rootQueue) { XCTAssertTrue(session.requestTaskMap.isEmpty) XCTAssertTrue(session.activeRequests.isEmpty) } let errors = [json1Response, json2Response].compactMap { $0?.error } XCTAssertEqual(errors.count, 2) for error in errors { XCTAssertTrue(error.isRequestAdaptationError) XCTAssertEqual(error.underlyingError?.asAFError?.urlConvertible as? String, "/adapt/error/2") } } @MainActor func testThatSessionCallsResponseSerializerCompletionsWhenAdapterThrowsErrorDuringRetryForDownloads() { // Four retries should occur given this scenario: // 1) Retrier is called from first response serializer failure (trips retry) // 2) Retrier is called by Session for adapt error thrown // 3) Retrier is called again from first response serializer failure // 4) Retrier is called from second response serializer failure // Given let handler = RequestHandler(throwsErrorOnSecondAdapt: true) let session = Session() let json1Expectation = expectation(description: "request should eventually fail") var json1Response: DownloadResponse? let json2Expectation = expectation(description: "request should eventually fail") var json2Response: DownloadResponse? // When let request = session.download(.image(.jpeg), interceptor: handler) .validate() .responseDecodable(of: TestResponse.self) { response in json1Response = response json1Expectation.fulfill() } .responseDecodable(of: TestResponse.self) { response in json2Response = response json2Expectation.fulfill() } waitForExpectations(timeout: timeout) // Then XCTAssertEqual(handler.adaptCalledCount, 2) XCTAssertEqual(handler.adaptedCount, 1) XCTAssertEqual(handler.retryCalledCount, 4) XCTAssertEqual(handler.retryCount, 4) XCTAssertEqual(request.retryCount, 1) XCTAssertEqual(json1Response?.result.isSuccess, false) XCTAssertEqual(json2Response?.result.isSuccess, false) assert(on: session.rootQueue) { XCTAssertTrue(session.requestTaskMap.isEmpty) XCTAssertTrue(session.activeRequests.isEmpty) } let errors = [json1Response, json2Response].compactMap { $0?.error } XCTAssertEqual(errors.count, 2) for error in errors { XCTAssertTrue(error.isRequestAdaptationError) XCTAssertEqual(error.underlyingError?.asAFError?.urlConvertible as? String, "/adapt/error/2") } } // MARK: Tests - Session Invalidation @MainActor func testThatSessionIsInvalidatedAndAllRequestsCompleteWhenSessionIsDeinitialized() { // Given let invalidationExpectation = expectation(description: "sessionDidBecomeInvalidWithError should be called") let events = ClosureEventMonitor() events.sessionDidBecomeInvalidWithError = { _, _ in invalidationExpectation.fulfill() } var session: Session? = Session(startRequestsImmediately: false, eventMonitors: [events]) var error: AFError? let requestExpectation = expectation(description: "request should complete") // When session?.request(.default).response { response in error = response.error requestExpectation.fulfill() } session = nil waitForExpectations(timeout: timeout) // Then XCTAssertEqual(error?.isSessionDeinitializedError, true) } // MARK: Tests - Request Cancellation @MainActor func testThatSessionOnlyCallsResponseSerializerCompletionWhenCancellingInsideCompletion() { // Given let handler = RequestHandler() let session = Session() let expectation = expectation(description: "request should complete") var response: DataResponse? var completionCallCount = 0 // When let request = session.request(.default, interceptor: handler) request.validate() request.responseDecodable(of: TestResponse.self) { resp in request.cancel() response = resp completionCallCount += 1 DispatchQueue.main.after(0.01) { expectation.fulfill() } } waitForExpectations(timeout: timeout) // Then XCTAssertEqual(handler.adaptCalledCount, 1) XCTAssertEqual(handler.adaptedCount, 1) XCTAssertEqual(handler.retryCalledCount, 0) XCTAssertEqual(handler.retryCount, 0) XCTAssertEqual(request.retryCount, 0) XCTAssertEqual(response?.result.isSuccess, true) XCTAssertEqual(completionCallCount, 1) assert(on: session.rootQueue) { XCTAssertTrue(session.requestTaskMap.isEmpty) XCTAssertTrue(session.activeRequests.isEmpty) } } // MARK: Tests - Request State @MainActor func testThatSessionSetsRequestStateWhenStartRequestsImmediatelyIsTrue() { // Given let session = Session() let expectation = expectation(description: "request should complete") var response: DataResponse? // When let request = session.request(.default).responseDecodable(of: TestResponse.self) { resp in response = resp expectation.fulfill() } waitForExpectations(timeout: timeout) // Then XCTAssertEqual(request.state, .finished) XCTAssertEqual(response?.result.isSuccess, true) } // MARK: Invalid Requests @MainActor func testThatGETRequestsWithBodyDataAreConsideredInvalid() { // Given let session = Session() var request = Endpoint().urlRequest request.httpBody = Data("invalid".utf8) let expect = expectation(description: "request should complete") var response: DataResponse? // When session.request(request).responseDecodable(of: TestResponse.self) { resp in response = resp expect.fulfill() } waitForExpectations(timeout: timeout) // Then XCTAssertEqual(response?.result.isFailure, true) XCTAssertEqual(response?.error?.isBodyDataInGETRequest, true) } @MainActor func testThatAdaptedGETRequestsWithBodyDataAreConsideredInvalid() { // Given struct InvalidAdapter: RequestInterceptor { func adapt(_ urlRequest: URLRequest, for session: Session, completion: @escaping (Result) -> Void) { var request = urlRequest request.httpBody = Data("invalid".utf8) completion(.success(request)) } } let session = Session(interceptor: InvalidAdapter()) let expect = expectation(description: "request should complete") var response: DataResponse? // When session.request(.default).responseDecodable(of: TestResponse.self) { resp in response = resp expect.fulfill() } waitForExpectations(timeout: timeout) // Then XCTAssertEqual(response?.result.isFailure, true) XCTAssertEqual(response?.error?.isRequestAdaptationError, true) XCTAssertEqual(response?.error?.underlyingError?.asAFError?.isBodyDataInGETRequest, true) } } // MARK: - final class SessionMassActionTestCase: BaseTestCase { func testThatRequestsCanHaveMassActionsPerformed() { // Given let count = 10 let createdTasks = expectation(description: "all tasks created") createdTasks.expectedFulfillmentCount = count let massActions = expectation(description: "cancel all requests should be called") let monitor = ClosureEventMonitor() monitor.requestDidCreateTask = { _, _ in createdTasks.fulfill() } let session = Session(eventMonitors: [monitor]) let request = Endpoint.delay(1) var requests: [DataRequest] = [] // When requests = (0..] = [] // When requests = (0..] = [] // When for _ in 0..) -> Void) { if hasRetried { var request = urlRequest request.url = Endpoint.delay(1).url completion(.success(request)) } else { completion(.success(urlRequest)) } } func retry(_ request: Request, for session: Session, dueTo error: any Error, completion: @escaping (RetryResult) -> Void) { completion(hasRetried ? .doNotRetry : .retry) hasRetried = true } } let queue = DispatchQueue(label: "com.alamofire.testQueue") let monitor = ClosureEventMonitor(queue: queue) let session = Session(rootQueue: queue, interceptor: OnceRetrier(), eventMonitors: [monitor]) let request = Endpoint.status(401) let completion = expectation(description: "all requests should finish") let cancellation = expectation(description: "cancel all requests should be called") let createTask = expectation(description: "should create task twice") createTask.expectedFulfillmentCount = 2 var tasksCreated = 0 monitor.requestDidCreateTask = { [unowned session] _, _ in tasksCreated += 1 createTask.fulfill() // Cancel after the second task is created to ensure proper lifetime events. if tasksCreated == 2 { session.cancelAllRequests { cancellation.fulfill() } } } var received: DataResponse? // When session.request(request).validate().response { response in received = response completion.fulfill() } waitForExpectations(timeout: timeout) // Then XCTAssertEqual(received?.error?.isExplicitlyCancelledError, true) assert(on: session.rootQueue) { XCTAssertTrue(session.requestTaskMap.isEmpty, "requestTaskMap should be empty but has \(session.requestTaskMap.count) items") XCTAssertTrue(session.activeRequests.isEmpty, "activeRequests should be empty but has \(session.activeRequests.count) items") } } } // MARK: - final class SessionConfigurationHeadersTestCase: BaseTestCase { enum ConfigurationType { case `default`, ephemeral } @MainActor func testThatDefaultConfigurationHeadersAreSentWithRequest() { // Given, When, Then executeAuthorizationHeaderTest(for: .default) } @MainActor func testThatEphemeralConfigurationHeadersAreSentWithRequest() { // Given, When, Then executeAuthorizationHeaderTest(for: .ephemeral) } @MainActor private func executeAuthorizationHeaderTest(for type: ConfigurationType) { // Given let session: Session = { let configuration: URLSessionConfiguration = { let configuration: URLSessionConfiguration = switch type { case .default: .default case .ephemeral: .ephemeral } var headers = HTTPHeaders.default headers["Authorization"] = "Bearer 123456" configuration.headers = headers return configuration }() return Session(configuration: configuration) }() let expectation = expectation(description: "request should complete successfully") var response: DataResponse? // When session.request(.default) .responseDecodable(of: TestResponse.self) { closureResponse in response = closureResponse expectation.fulfill() } waitForExpectations(timeout: timeout) // Then XCTAssertNotNil(response?.request, "request should not be nil") XCTAssertNotNil(response?.response, "response should not be nil") XCTAssertNotNil(response?.data, "data should not be nil") XCTAssertEqual(response?.result.isSuccess, true) XCTAssertEqual(response?.value?.headers["Authorization"], "Bearer 123456", "Authorization header should match") } }