diff --git a/ios/MullvadVPN.xcodeproj/project.pbxproj b/ios/MullvadVPN.xcodeproj/project.pbxproj index ffb67a06f161..15eaaed3532d 100644 --- a/ios/MullvadVPN.xcodeproj/project.pbxproj +++ b/ios/MullvadVPN.xcodeproj/project.pbxproj @@ -419,6 +419,8 @@ 7A09C98129D99215000C2CAC /* String+FuzzyMatch.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A09C98029D99215000C2CAC /* String+FuzzyMatch.swift */; }; 7A0C0F632A979C4A0058EFCE /* Coordinator+Router.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A0C0F622A979C4A0058EFCE /* Coordinator+Router.swift */; }; 7A11DD0B2A9495D400098CD8 /* AppRoutes.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5802EBC42A8E44AC00E5CE4C /* AppRoutes.swift */; }; + 7A12D0762B062D5C00E9602D /* URLSessionProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A12D0752B062D5C00E9602D /* URLSessionProtocol.swift */; }; + 7A12D0772B062D6500E9602D /* URLSessionProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A12D0752B062D5C00E9602D /* URLSessionProtocol.swift */; }; 7A1A26432A2612AE00B978AA /* PaymentAlertPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A1A26422A2612AE00B978AA /* PaymentAlertPresenter.swift */; }; 7A1A26452A29CEF700B978AA /* RelayFilterViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A1A26442A29CEF700B978AA /* RelayFilterViewController.swift */; }; 7A1A26472A29CF0800B978AA /* RelayFilterDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A1A26462A29CF0800B978AA /* RelayFilterDataSource.swift */; }; @@ -654,7 +656,7 @@ F09D04B52AE93CB6003D4F89 /* OutgoingConnectionProxy+Stub.swift in Sources */ = {isa = PBXBuildFile; fileRef = F09D04B42AE93CB6003D4F89 /* OutgoingConnectionProxy+Stub.swift */; }; F09D04B72AE941DA003D4F89 /* OutgoingConnectionProxyTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F09D04B62AE941DA003D4F89 /* OutgoingConnectionProxyTests.swift */; }; F09D04B92AE95111003D4F89 /* OutgoingConnectionProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = F09D04AF2AE7F83D003D4F89 /* OutgoingConnectionProxy.swift */; }; - F09D04BB2AE95396003D4F89 /* MockURLProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = F09D04BA2AE95396003D4F89 /* MockURLProtocol.swift */; }; + F09D04BB2AE95396003D4F89 /* URLSessionStub.swift in Sources */ = {isa = PBXBuildFile; fileRef = F09D04BA2AE95396003D4F89 /* URLSessionStub.swift */; }; F09D04BD2AEBB7C5003D4F89 /* OutgoingConnectionService.swift in Sources */ = {isa = PBXBuildFile; fileRef = F09D04BC2AEBB7C5003D4F89 /* OutgoingConnectionService.swift */; }; F09D04C02AF39D63003D4F89 /* OutgoingConnectionServiceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F09D04BF2AF39D63003D4F89 /* OutgoingConnectionServiceTests.swift */; }; F09D04C12AF39EA2003D4F89 /* OutgoingConnectionService.swift in Sources */ = {isa = PBXBuildFile; fileRef = F09D04BC2AEBB7C5003D4F89 /* OutgoingConnectionService.swift */; }; @@ -1527,6 +1529,7 @@ 7A02D4EA2A9CEC7A00C19E31 /* MullvadVPNScreenshots.xctestplan */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = MullvadVPNScreenshots.xctestplan; sourceTree = ""; }; 7A09C98029D99215000C2CAC /* String+FuzzyMatch.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+FuzzyMatch.swift"; sourceTree = ""; }; 7A0C0F622A979C4A0058EFCE /* Coordinator+Router.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Coordinator+Router.swift"; sourceTree = ""; }; + 7A12D0752B062D5C00E9602D /* URLSessionProtocol.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = URLSessionProtocol.swift; sourceTree = ""; }; 7A1A26422A2612AE00B978AA /* PaymentAlertPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaymentAlertPresenter.swift; sourceTree = ""; }; 7A1A26442A29CEF700B978AA /* RelayFilterViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelayFilterViewController.swift; sourceTree = ""; }; 7A1A26462A29CF0800B978AA /* RelayFilterDataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelayFilterDataSource.swift; sourceTree = ""; }; @@ -1650,7 +1653,7 @@ F09D04AF2AE7F83D003D4F89 /* OutgoingConnectionProxy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OutgoingConnectionProxy.swift; sourceTree = ""; }; F09D04B42AE93CB6003D4F89 /* OutgoingConnectionProxy+Stub.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "OutgoingConnectionProxy+Stub.swift"; sourceTree = ""; }; F09D04B62AE941DA003D4F89 /* OutgoingConnectionProxyTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OutgoingConnectionProxyTests.swift; sourceTree = ""; }; - F09D04BA2AE95396003D4F89 /* MockURLProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockURLProtocol.swift; sourceTree = ""; }; + F09D04BA2AE95396003D4F89 /* URLSessionStub.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLSessionStub.swift; sourceTree = ""; }; F09D04BC2AEBB7C5003D4F89 /* OutgoingConnectionService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OutgoingConnectionService.swift; sourceTree = ""; }; F09D04BF2AF39D63003D4F89 /* OutgoingConnectionServiceTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OutgoingConnectionServiceTests.swift; sourceTree = ""; }; F0B0E6962AFE6E7E001DC66B /* XCTest+Async.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "XCTest+Async.swift"; sourceTree = ""; }; @@ -2361,8 +2364,9 @@ 5864AF0629C78816005B0CD9 /* Protocols */ = { isa = PBXGroup; children = ( - 58E11187292FA11F009FCA84 /* SettingsMigrationUIHandler.swift */, 5864AF0129C7879B005B0CD9 /* CellFactoryProtocol.swift */, + 58E11187292FA11F009FCA84 /* SettingsMigrationUIHandler.swift */, + 7A12D0752B062D5C00E9602D /* URLSessionProtocol.swift */, ); path = Protocols; sourceTree = ""; @@ -2513,7 +2517,7 @@ F07BF2572A26112D00042943 /* InputTextFormatterTests.swift */, A9B6AC172ADE8F4300F7802A /* MigrationManagerTests.swift */, 58C3FA652A38549D006A450A /* MockFileCache.swift */, - F09D04BA2AE95396003D4F89 /* MockURLProtocol.swift */, + F09D04BA2AE95396003D4F89 /* URLSessionStub.swift */, F09D04B42AE93CB6003D4F89 /* OutgoingConnectionProxy+Stub.swift */, F09D04B62AE941DA003D4F89 /* OutgoingConnectionProxyTests.swift */, F09D04BF2AF39D63003D4F89 /* OutgoingConnectionServiceTests.swift */, @@ -4134,6 +4138,7 @@ A9A5F9EC2ACB05160083449F /* CodingErrors+CustomErrorDescription.swift in Sources */, A9A5F9ED2ACB05160083449F /* NSRegularExpression+IPAddress.swift in Sources */, A9A5F9EE2ACB05160083449F /* RESTCreateApplePaymentResponse+Localization.swift in Sources */, + 7A12D0772B062D6500E9602D /* URLSessionProtocol.swift in Sources */, A9A5F9EF2ACB05160083449F /* String+AccountFormatting.swift in Sources */, A9A5F9F02ACB05160083449F /* String+FuzzyMatch.swift in Sources */, F09D04C12AF39EA2003D4F89 /* OutgoingConnectionService.swift in Sources */, @@ -4190,7 +4195,7 @@ A9A5FA1A2ACB05160083449F /* StopTunnelOperation.swift in Sources */, A9A5FA1B2ACB05160083449F /* Tunnel.swift in Sources */, A9A5FA1C2ACB05160083449F /* Tunnel+Messaging.swift in Sources */, - F09D04BB2AE95396003D4F89 /* MockURLProtocol.swift in Sources */, + F09D04BB2AE95396003D4F89 /* URLSessionStub.swift in Sources */, A9A5FA1D2ACB05160083449F /* TunnelBlockObserver.swift in Sources */, A9A5FA1E2ACB05160083449F /* TunnelConfiguration.swift in Sources */, A9A5FA1F2ACB05160083449F /* TunnelInteractor.swift in Sources */, @@ -4510,6 +4515,7 @@ 5878A27329091D6D0096FC88 /* TunnelBlockObserver.swift in Sources */, A9E034642ABB302000E59A5A /* UIEdgeInsets+Extensions.swift in Sources */, 58E0A98827C8F46300FE6BDD /* Tunnel.swift in Sources */, + 7A12D0762B062D5C00E9602D /* URLSessionProtocol.swift in Sources */, 58ACF64F26567A7100ACE4B7 /* CustomSwitchContainer.swift in Sources */, 58EE2E3A272FF814003BFF93 /* SettingsDataSource.swift in Sources */, F0E8E4C12A602CCB00ED26A3 /* AccountDeletionContentView.swift in Sources */, diff --git a/ios/MullvadVPN/GeneralAPIs/OutgoingConnectionProxy.swift b/ios/MullvadVPN/GeneralAPIs/OutgoingConnectionProxy.swift index a078efce1a26..233bd4d763ce 100644 --- a/ios/MullvadVPN/GeneralAPIs/OutgoingConnectionProxy.swift +++ b/ios/MullvadVPN/GeneralAPIs/OutgoingConnectionProxy.swift @@ -17,7 +17,7 @@ protocol OutgoingConnectionHandling { } final class OutgoingConnectionProxy: OutgoingConnectionHandling { - private enum ExitIPVersion: String { + enum ExitIPVersion: String { case v4 = "ipv4", v6 = "ipv6" var host: String { @@ -25,9 +25,9 @@ final class OutgoingConnectionProxy: OutgoingConnectionHandling { } } - let urlSession: URLSession + let urlSession: URLSessionProtocol - init(urlSession: URLSession) { + init(urlSession: URLSessionProtocol) { self.urlSession = urlSession } @@ -74,7 +74,7 @@ final class OutgoingConnectionProxy: OutgoingConnectionHandling { cachePolicy: .useProtocolCachePolicy, timeoutInterval: REST.defaultAPINetworkTimeout.timeInterval ) - let (data, response) = try await urlSession.data(for: request) + let (data, response) = try await data(for: request) guard let httpResponse = response as? HTTPURLResponse else { throw REST.Error.network(URLError(.badServerResponse)) } @@ -92,3 +92,9 @@ final class OutgoingConnectionProxy: OutgoingConnectionHandling { return connectionData } } + +extension OutgoingConnectionProxy: URLSessionProtocol { + func data(for request: URLRequest) async throws -> (Data, URLResponse) { + return try await urlSession.data(for: request) + } +} diff --git a/ios/MullvadVPN/Protocols/URLSessionProtocol.swift b/ios/MullvadVPN/Protocols/URLSessionProtocol.swift new file mode 100644 index 000000000000..ec2f2982e494 --- /dev/null +++ b/ios/MullvadVPN/Protocols/URLSessionProtocol.swift @@ -0,0 +1,15 @@ +// +// URLSessionProtocol.swift +// MullvadVPN +// +// Created by Jon Petersson on 2023-11-16. +// Copyright © 2023 Mullvad VPN AB. All rights reserved. +// + +import Foundation + +protocol URLSessionProtocol { + func data(for request: URLRequest) async throws -> (Data, URLResponse) +} + +extension URLSession: URLSessionProtocol {} diff --git a/ios/MullvadVPNTests/MockURLProtocol.swift b/ios/MullvadVPNTests/MockURLProtocol.swift deleted file mode 100644 index 061a132528f2..000000000000 --- a/ios/MullvadVPNTests/MockURLProtocol.swift +++ /dev/null @@ -1,55 +0,0 @@ -// -// MockURLProtocol.swift -// MullvadVPNTests -// -// Created by Mojgan on 2023-10-25. -// Copyright © 2023 Mullvad VPN AB. All rights reserved. -// - -import Foundation - -class MockURLProtocol: URLProtocol { - static var error: Error? - static var requestHandler: ((URLRequest) throws -> (HTTPURLResponse, Data))? - - override class func canInit(with request: URLRequest) -> Bool { - return true - } - - override class func canonicalRequest(for request: URLRequest) -> URLRequest { - return request - } - - override func startLoading() { - if let error = MockURLProtocol.error { - client?.urlProtocol(self, didFailWithError: error) - return - } - - guard let handler = MockURLProtocol.requestHandler else { - assertionFailure("Received unexpected request with no handler set") - return - } - - do { - let (response, data) = try handler(request) - client?.urlProtocol(self, didReceive: response, cacheStoragePolicy: .notAllowed) - client?.urlProtocol(self, didLoad: data) - client?.urlProtocolDidFinishLoading(self) - } catch { - client?.urlProtocol(self, didFailWithError: error) - } - } - - override func stopLoading() { - // stop loading here - } -} - -extension URLSession { - static let mock = { - let configuration = URLSessionConfiguration.ephemeral - configuration.protocolClasses = [MockURLProtocol.self] - return URLSession(configuration: configuration) - }() -} diff --git a/ios/MullvadVPNTests/OutgoingConnectionProxyTests.swift b/ios/MullvadVPNTests/OutgoingConnectionProxyTests.swift index afa8ada0fa79..2ffdbdf92dd4 100644 --- a/ios/MullvadVPNTests/OutgoingConnectionProxyTests.swift +++ b/ios/MullvadVPNTests/OutgoingConnectionProxyTests.swift @@ -9,51 +9,27 @@ import MullvadREST import XCTest final class OutgoingConnectionProxyTests: XCTestCase { - private var outgoingConnectionProxy: OutgoingConnectionProxy! private var mockIPV6ConnectionData: Data! private var mockIPV4ConnectionData: Data! private let encoder = JSONEncoder() override func setUpWithError() throws { - outgoingConnectionProxy = OutgoingConnectionProxy(urlSession: .mock) mockIPV4ConnectionData = try encoder.encode(IPV4ConnectionData.mock) mockIPV6ConnectionData = try encoder.encode(IPV6ConnectionData.mock) } override func tearDownWithError() throws { - outgoingConnectionProxy = nil mockIPV4ConnectionData.removeAll() mockIPV6ConnectionData.removeAll() } - func testNoInternetConnection() async throws { - let noIPv4Expectation = expectation(description: "Did not receive IPv4") - let error = URLError(URLError.notConnectedToInternet) - - MockURLProtocol.error = error - MockURLProtocol.requestHandler = nil - - await XCTAssertThrowsErrorAsync(try await outgoingConnectionProxy.getIPV4(retryStrategy: .noRetry)) { error in - noIPv4Expectation.fulfill() - XCTAssertEqual((error as? URLError)?.code, .notConnectedToInternet) - } - await fulfillment(of: [noIPv4Expectation], timeout: 1) - } - func testSuccessGettingIPV4() async throws { let iPv4Expectation = expectation(description: "Did receive IPv4") - MockURLProtocol.error = nil - MockURLProtocol.requestHandler = { _ in - let response = HTTPURLResponse( - url: URL(string: "https://ipv4.am.i.mullvad.net/json")!, - statusCode: 200, - httpVersion: nil, - headerFields: ["Content-Type": "application/json"] - )! - return (response, self.mockIPV4ConnectionData) - } + let outgoingConnectionProxy = OutgoingConnectionProxy(urlSession: URLSessionStub( + response: (mockIPV4ConnectionData, createHTTPURLResponse(ip: .v4, statusCode: 200)) + )) let result = try await outgoingConnectionProxy.getIPV4(retryStrategy: .noRetry) @@ -66,16 +42,9 @@ final class OutgoingConnectionProxyTests: XCTestCase { func testFailureGettingIPV4() async throws { let noIPv4Expectation = expectation(description: "Did not receive IPv4") - MockURLProtocol.error = nil - MockURLProtocol.requestHandler = { _ in - let response = HTTPURLResponse( - url: URL(string: "https://ipv4.am.i.mullvad.net/json")!, - statusCode: 503, - httpVersion: nil, - headerFields: ["Content-Type": "application/json"] - )! - return (response, Data()) - } + let outgoingConnectionProxy = OutgoingConnectionProxy(urlSession: URLSessionStub( + response: (Data(), createHTTPURLResponse(ip: .v4, statusCode: 503)) + )) await XCTAssertThrowsErrorAsync(try await outgoingConnectionProxy.getIPV4(retryStrategy: .noRetry)) { _ in noIPv4Expectation.fulfill() @@ -86,16 +55,9 @@ final class OutgoingConnectionProxyTests: XCTestCase { func testSuccessGettingIPV6() async throws { let ipv6Expectation = expectation(description: "Did receive IPv6") - MockURLProtocol.error = nil - MockURLProtocol.requestHandler = { _ in - let response = HTTPURLResponse( - url: URL(string: "https://ipv6.am.i.mullvad.net/json")!, - statusCode: 200, - httpVersion: nil, - headerFields: ["Content-Type": "application/json"] - )! - return (response, self.mockIPV6ConnectionData) - } + let outgoingConnectionProxy = OutgoingConnectionProxy(urlSession: URLSessionStub( + response: (mockIPV6ConnectionData, createHTTPURLResponse(ip: .v6, statusCode: 200)) + )) let result = try await outgoingConnectionProxy.getIPV6(retryStrategy: .noRetry) @@ -108,16 +70,9 @@ final class OutgoingConnectionProxyTests: XCTestCase { func testFailureGettingIPV6() async throws { let noIPv6Expectation = expectation(description: "Did not receive IPv6") - MockURLProtocol.error = nil - MockURLProtocol.requestHandler = { _ in - let response = HTTPURLResponse( - url: URL(string: "https://ipv6.am.i.mullvad.net/json")!, - statusCode: 404, - httpVersion: nil, - headerFields: ["Content-Type": "application/json"] - )! - return (response, Data()) - } + let outgoingConnectionProxy = OutgoingConnectionProxy(urlSession: URLSessionStub( + response: (mockIPV6ConnectionData, createHTTPURLResponse(ip: .v6, statusCode: 404)) + )) await XCTAssertThrowsErrorAsync(try await outgoingConnectionProxy.getIPV6(retryStrategy: .noRetry)) { _ in noIPv6Expectation.fulfill() @@ -125,3 +80,14 @@ final class OutgoingConnectionProxyTests: XCTestCase { await fulfillment(of: [noIPv6Expectation], timeout: 1) } } + +extension OutgoingConnectionProxyTests { + private func createHTTPURLResponse(ip: OutgoingConnectionProxy.ExitIPVersion, statusCode: Int) -> HTTPURLResponse { + return HTTPURLResponse( + url: URL(string: "https://\(ip.host)/json")!, + statusCode: statusCode, + httpVersion: nil, + headerFields: ["Content-Type": "application/json"] + )! + } +} diff --git a/ios/MullvadVPNTests/URLSessionStub.swift b/ios/MullvadVPNTests/URLSessionStub.swift new file mode 100644 index 000000000000..ecde3456fdac --- /dev/null +++ b/ios/MullvadVPNTests/URLSessionStub.swift @@ -0,0 +1,21 @@ +// +// URLSessionStub.swift +// MullvadVPNTests +// +// Created by Mojgan on 2023-10-25. +// Copyright © 2023 Mullvad VPN AB. All rights reserved. +// + +import Foundation + +class URLSessionStub: URLSessionProtocol { + var response: (Data, URLResponse) + + init(response: (Data, URLResponse)) { + self.response = response + } + + func data(for request: URLRequest) async throws -> (Data, URLResponse) { + return response + } +}