Skip to content

Commit

Permalink
Improves pixel information (#748)
Browse files Browse the repository at this point in the history
Task/Issue URL: https://app.asana.com/0/0/1206919998699658/f
iOS PR: duckduckgo/iOS#2636
macOS PR: duckduckgo/macos-browser#2498
What kind of version bump will this require?: Minor

## Description

Adds some new errors to improve tracking of pixel errors.
  • Loading branch information
diegoreymendez authored Mar 26, 2024
1 parent 24da852 commit c2ae796
Show file tree
Hide file tree
Showing 6 changed files with 251 additions and 20 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ public enum NetworkProtectionError: LocalizedError, CustomNSError {
// Client errors
case failedToFetchServerList(Error?)
case failedToParseServerListResponse(Error)
case failedToFetchLocationList(Error?)
case failedToFetchLocationList(Error)
case failedToParseLocationListResponse(Error)
case failedToEncodeRegisterKeyRequest
case failedToFetchRegisteredServers(Error?)
Expand Down Expand Up @@ -157,7 +157,6 @@ public enum NetworkProtectionError: LocalizedError, CustomNSError {
.vpnAccessRevoked:
return [:]
case .failedToFetchServerList(let error),
.failedToFetchLocationList(let error),
.failedToFetchRegisteredServers(let error),
.failedToRedeemInviteCode(let error):
guard let error else {
Expand All @@ -168,6 +167,7 @@ public enum NetworkProtectionError: LocalizedError, CustomNSError {
NSUnderlyingErrorKey: error
]
case .failedToParseServerListResponse(let error),
.failedToFetchLocationList(let error),
.failedToParseLocationListResponse(let error),
.failedToParseRegisteredServersResponse(let error),
.failedToParseRedeemResponse(let error),
Expand Down
95 changes: 81 additions & 14 deletions Sources/NetworkProtection/Networking/NetworkProtectionClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -32,16 +32,16 @@ protocol NetworkProtectionClient {
}

public enum NetworkProtectionClientError: Error, NetworkProtectionErrorConvertible {
case failedToFetchLocationList(Error?)
case failedToFetchLocationList(Error)
case failedToParseLocationListResponse(Error)
case failedToFetchServerList(Error?)
case failedToFetchServerList(Error)
case failedToParseServerListResponse(Error)
case failedToEncodeRegisterKeyRequest
case failedToFetchRegisteredServers(Error?)
case failedToFetchRegisteredServers(Error)
case failedToParseRegisteredServersResponse(Error)
case failedToEncodeRedeemRequest
case invalidInviteCode
case failedToRedeemInviteCode(Error?)
case failedToRedeemInviteCode(Error)
case failedToRetrieveAuthToken(AuthenticationFailureResponse)
case failedToParseRedeemResponse(Error)
case invalidAuthToken
Expand Down Expand Up @@ -173,6 +173,21 @@ final class NetworkProtectionBackendClient: NetworkProtectionClient {
self.endpointURL = environment.endpointURL
}

public enum GetLocationsError: CustomNSError {
case noResponse
case unexpectedStatus(status: Int)

var errorCode: Int {
switch self {
case .noResponse:
return 0
case .unexpectedStatus(let status):
// Adding a large number so that we can get a somewhat reasonable status code
return 100000 + status
}
}
}

func getLocations(authToken: String) async -> Result<[NetworkProtectionLocation], NetworkProtectionClientError> {
var request = URLRequest(url: locationsURL)
request.setValue("bearer \(authToken)", forHTTPHeaderField: "Authorization")
Expand All @@ -181,12 +196,13 @@ final class NetworkProtectionBackendClient: NetworkProtectionClient {
do {
let (data, response) = try await URLSession.shared.data(for: request)
guard let response = response as? HTTPURLResponse else {
return .failure(.failedToFetchLocationList(nil))
throw GetLocationsError.noResponse
}
switch response.statusCode {
case 200: downloadedData = data
case 401: return .failure(.invalidAuthToken)
default: return .failure(.failedToFetchLocationList(nil))
default:
throw GetLocationsError.unexpectedStatus(status: response.statusCode)
}
} catch {
return .failure(NetworkProtectionClientError.failedToFetchLocationList(error))
Expand All @@ -200,6 +216,21 @@ final class NetworkProtectionBackendClient: NetworkProtectionClient {
}
}

public enum GetServersError: CustomNSError {
case noResponse
case unexpectedStatus(status: Int)

var errorCode: Int {
switch self {
case .noResponse:
return 0
case .unexpectedStatus(let status):
// Adding a large number so that we can get a somewhat reasonable status code
return 100000 + status
}
}
}

func getServers(authToken: String) async -> Result<[NetworkProtectionServer], NetworkProtectionClientError> {
var request = URLRequest(url: serversURL)
request.setValue("bearer \(authToken)", forHTTPHeaderField: "Authorization")
Expand All @@ -208,12 +239,13 @@ final class NetworkProtectionBackendClient: NetworkProtectionClient {
do {
let (data, response) = try await URLSession.shared.data(for: request)
guard let response = response as? HTTPURLResponse else {
return .failure(.failedToFetchServerList(nil))
throw GetServersError.noResponse
}
switch response.statusCode {
case 200: downloadedData = data
case 401: return .failure(.invalidAuthToken)
default: return .failure(.failedToFetchServerList(nil))
default:
throw GetServersError.unexpectedStatus(status: response.statusCode)
}
} catch {
return .failure(NetworkProtectionClientError.failedToFetchServerList(error))
Expand All @@ -227,6 +259,21 @@ final class NetworkProtectionBackendClient: NetworkProtectionClient {
}
}

public enum RegisterError: CustomNSError {
case noResponse
case unexpectedStatus(status: Int)

var errorCode: Int {
switch self {
case .noResponse:
return 0
case .unexpectedStatus(let status):
// Adding a large number so that we can get a somewhat reasonable status code
return 100000 + status
}
}
}

func register(authToken: String,
requestBody: RegisterKeyRequestBody) async -> Result<[NetworkProtectionServer], NetworkProtectionClientError> {
let requestBodyData: Data
Expand All @@ -248,12 +295,17 @@ final class NetworkProtectionBackendClient: NetworkProtectionClient {
do {
let (data, response) = try await URLSession.shared.data(for: request)
guard let response = response as? HTTPURLResponse else {
return .failure(.failedToFetchRegisteredServers(nil))
throw RegisterError.noResponse
}
switch response.statusCode {
case 200: responseData = data
case 401: return .failure(.invalidAuthToken)
default: return (response.statusCode == 403 && isSubscriptionEnabled) ? .failure(.accessDenied) : .failure(.failedToFetchRegisteredServers(nil))
case 200:
responseData = data
case 401:
return .failure(.invalidAuthToken)
case 403 where isSubscriptionEnabled:
return .failure(.accessDenied)
default:
throw RegisterError.unexpectedStatus(status: response.statusCode)
}
} catch {
return .failure(NetworkProtectionClientError.failedToFetchRegisteredServers(error))
Expand All @@ -276,6 +328,21 @@ final class NetworkProtectionBackendClient: NetworkProtectionClient {
}
}

public enum AuthTokenError: CustomNSError {
case noResponse
case unexpectedStatus(status: Int)

var errorCode: Int {
switch self {
case .noResponse:
return 0
case .unexpectedStatus(let status):
// Adding a large number so that we can get a somewhat reasonable status code
return 100000 + status
}
}
}

private func redeem(inviteCode: String) async -> Result<String, NetworkProtectionClientError> {
let requestBody = RedeemInviteCodeRequestBody(code: inviteCode)
return await retrieveAuthToken(requestBody: requestBody, endpoint: redeemURL)
Expand Down Expand Up @@ -308,7 +375,7 @@ final class NetworkProtectionBackendClient: NetworkProtectionClient {
do {
let (data, response) = try await URLSession.shared.data(for: request)
guard let response = response as? HTTPURLResponse else {
return .failure(.failedToRedeemInviteCode(nil))
throw AuthTokenError.noResponse
}
switch response.statusCode {
case 200:
Expand All @@ -321,7 +388,7 @@ final class NetworkProtectionBackendClient: NetworkProtectionClient {
let decodedRedemptionResponse = try decoder.decode(AuthenticationFailureResponse.self, from: data)
return .failure(.failedToRetrieveAuthToken(decodedRedemptionResponse))
} catch {
return .failure(.failedToRedeemInviteCode(nil))
throw AuthTokenError.unexpectedStatus(status: response.statusCode)
}
}
} catch {
Expand Down
29 changes: 27 additions & 2 deletions Sources/NetworkProtection/PacketTunnelProvider.swift
Original file line number Diff line number Diff line change
Expand Up @@ -58,12 +58,15 @@ open class PacketTunnelProvider: NEPacketTunnelProvider {

// MARK: - Error Handling

enum TunnelError: LocalizedError {
enum TunnelError: LocalizedError, CustomNSError {
// Tunnel Setup Errors - 0+
case startingTunnelWithoutAuthToken
case vpnAccessRevoked
case couldNotGenerateTunnelConfiguration(internalError: Error)
case simulateTunnelFailureError

// Subscription Errors - 100+
case vpnAccessRevoked

var errorDescription: String? {
switch self {
case .startingTunnelWithoutAuthToken:
Expand All @@ -76,6 +79,28 @@ open class PacketTunnelProvider: NEPacketTunnelProvider {
return "Simulated a tunnel error as requested"
}
}

var errorCode: Int {
switch self {
// Tunnel Setup Errors - 0+
case .startingTunnelWithoutAuthToken: return 0
case .couldNotGenerateTunnelConfiguration: return 1
case .simulateTunnelFailureError: return 2
// Subscription Errors - 100+
case .vpnAccessRevoked: return 100
}
}

var errorUserInfo: [String: Any] {
switch self {
case .startingTunnelWithoutAuthToken,
.simulateTunnelFailureError,
.vpnAccessRevoked:
return [:]
case .couldNotGenerateTunnelConfiguration(let underlyingError):
return [NSUnderlyingErrorKey: underlyingError]
}
}
}

// MARK: - WireGuard
Expand Down
94 changes: 94 additions & 0 deletions Tests/NetworkProtectionTests/NetworkProtectionErrorTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
//
// NetworkProtectionErrorTests.swift
//
// Copyright © 2024 DuckDuckGo. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//

import Foundation
import XCTest
@testable import NetworkProtection

final class NetworkProtectionErrorTests: XCTestCase {

/// Tests that `TunnelError`implements `CustomNSError`.
///
func testThatNetworkProtectionErrorImplementsCustomNSError() {
let genericError: Error = NetworkProtectionError.noServerRegistrationInfo

XCTAssertNotNil(genericError as? CustomNSError)
}

/// Test that some `NetworkProtectionError` have no underlying errors.
///
func testThatSomeErrorsHaveNoUnderlyingError() {
let errorsWithoutUnderlyingError: [NetworkProtectionError] = [
.noServerRegistrationInfo,
.couldNotSelectClosestServer,
.couldNotGetPeerPublicKey,
.couldNotGetPeerHostName,
.couldNotGetInterfaceAddressRange,
.failedToEncodeRegisterKeyRequest,
.failedToEncodeRedeemRequest,
.invalidInviteCode,
.invalidAuthToken,
.serverListInconsistency,
.noServerListFound,
.wireGuardCannotLocateTunnelFileDescriptor,
.wireGuardDnsResolution,
.noAuthTokenFound,
.vpnAccessRevoked,
.failedToCastKeychainValueToData(field: "test"),
.keychainReadError(field: "test", status: 1),
.keychainWriteError(field: "test", status: 1),
.keychainUpdateError(field: "test", status: 1),
.keychainDeleteError(status: 1),
.wireGuardInvalidState(reason: "test"),
.startWireGuardBackend(1),

]

for error in errorsWithoutUnderlyingError {
let nsError = error as NSError
XCTAssertEqual(nsError.userInfo[NSUnderlyingErrorKey] as? NSError, nil)
}
}

func testThatSomeErrorsHaveUnderlyingErrors() {
let underlyingError = NSError(domain: "test", code: 1)

let errorsWithUnderlyingError: [NetworkProtectionError] = [
.failedToFetchServerList(underlyingError),
.failedToParseServerListResponse(underlyingError),
.failedToFetchLocationList(underlyingError),
.failedToParseLocationListResponse(underlyingError),
.failedToFetchRegisteredServers(underlyingError),
.failedToParseRegisteredServersResponse(underlyingError),
.failedToRedeemInviteCode(underlyingError),
.failedToParseRedeemResponse(underlyingError),
.failedToEncodeServerList(underlyingError),
.failedToDecodeServerList(underlyingError),
.failedToWriteServerList(underlyingError),
.couldNotCreateServerListDirectory(underlyingError),
.failedToReadServerList(underlyingError),
.wireGuardSetNetworkSettings(underlyingError),
.unhandledError(function: #function, line: #line, error: underlyingError),
]

for error in errorsWithUnderlyingError {
let nsError = error as NSError
XCTAssertEqual(nsError.userInfo[NSUnderlyingErrorKey] as? NSError, underlyingError)
}
}
}
45 changes: 45 additions & 0 deletions Tests/NetworkProtectionTests/PacketTunnelProviderTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
//
// PacketTunnelProviderTests.swift
//
// Copyright © 2024 DuckDuckGo. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//

import Foundation
import XCTest
@testable import NetworkProtection

final class PacketTunnelProviderTests: XCTestCase {

typealias TunnelError = PacketTunnelProvider.TunnelError

/// Tests that `TunnelError`implements `CustomNSError`.
///
func testThatTunnelErrorImplementsCustomNSError() {
let genericError: Error = TunnelError.startingTunnelWithoutAuthToken

XCTAssertNotNil(genericError as? CustomNSError)
}

/// Tests that `TunnelError` has the expected underlying error information.
///
func testThatTunnelErrorHasTheExpectedUnderlyingErrorInformation() {
XCTAssertEqual(TunnelError.startingTunnelWithoutAuthToken.errorUserInfo[NSUnderlyingErrorKey] as? NSError, nil)
XCTAssertEqual(TunnelError.simulateTunnelFailureError.errorUserInfo[NSUnderlyingErrorKey] as? NSError, nil)
XCTAssertEqual(TunnelError.vpnAccessRevoked.errorUserInfo[NSUnderlyingErrorKey] as? NSError, nil)

let underlyingError = NSError(domain: "test", code: 1)
XCTAssertEqual(TunnelError.couldNotGenerateTunnelConfiguration(internalError: underlyingError).errorUserInfo[NSUnderlyingErrorKey] as? NSError, underlyingError)
}
}
Loading

0 comments on commit c2ae796

Please sign in to comment.