[BWA-155] Move ResponseValidationHandler to BitwardenKit (#1486)

This commit is contained in:
Katherine Bertelsen 2025-04-07 16:25:10 -05:00 committed by GitHub
parent 93a98c7f06
commit 4e17cf18c2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
25 changed files with 99 additions and 187 deletions

View File

@ -1,3 +1,4 @@
import BitwardenKit
import Foundation
public extension Error {

View File

@ -1,4 +1,5 @@
import BitwardenKit
import BitwardenKitMocks
import Networking
import TestHelpers
import XCTest

View File

@ -1,29 +0,0 @@
import BitwardenKit
// MARK: - ServerError
/// An enumeration of server errors.
///
enum ServerError: Error, Equatable {
/// A generic error.
///
/// - Parameter errorResponse: The error response returned from the API request.
///
case error(errorResponse: ErrorResponseModel)
/// A validation error.
///
/// - Parameter validationErrorResponse: The validation error response returned from the API request.
///
case validationError(validationErrorResponse: ResponseValidationErrorModel)
/// A computed property that returns an error message based on the case.
var message: String {
switch self {
case let .error(errorResponse):
errorResponse.singleMessage()
case let .validationError(validationErrorResponse):
validationErrorResponse.errorModel.message
}
}
}

View File

@ -1,57 +0,0 @@
import BitwardenKit
import Networking
import TestHelpers
import XCTest
@testable import AuthenticatorShared
class ResponseValidationHandlerTests: BitwardenTestCase {
// MARK: Properties
var subject: ResponseValidationHandler!
// MARK: Setup & Teardown
override func setUp() {
super.setUp()
subject = ResponseValidationHandler()
}
override func tearDown() {
super.tearDown()
subject = nil
}
// MARK: Tests
/// `handle(_:)` doesn't throw an error for successful status codes.
func test_handle_validResponse() async throws {
for statusCode in [200, 250, 299] {
var response = HTTPResponse.success(statusCode: statusCode)
let handledResponse = try await subject.handle(&response)
XCTAssertEqual(handledResponse, response)
}
}
/// `handle(_:)` throws a `ServerError` if the response is able to be parsed as a `ErrorResponseModel`.
func test_handle_throwsServerError() async throws {
var response = HTTPResponse.failure(statusCode: 400, body: APITestData.bitwardenErrorMessage.data)
try await assertAsyncThrows(error: ServerError.error(errorResponse: ErrorResponseModel(response: response))) {
_ = try await subject.handle(&response)
}
}
/// `handle(_:)` throws a `ResponseValidationError` for any non-2XX status codes that aren't
/// able to be parsed as a `ErrorResponseModel`.
func test_handle_throwsResponseValidationError() async {
for statusCode in [400, 499, 500, 599] {
var response = HTTPResponse.failure(statusCode: statusCode)
await assertAsyncThrows(error: ResponseValidationError(response: response)) {
_ = try await subject.handle(&response)
}
}
}
}

View File

@ -1,3 +1,4 @@
import BitwardenKit
import BitwardenSdk
import Foundation

View File

@ -1,6 +1,7 @@
// swiftlint:disable:this file_name
import BitwardenKit
import BitwardenKitMocks
import Foundation
import Networking
import TestHelpers

View File

@ -0,0 +1,48 @@
import Foundation
import Networking
// MARK: - ResponseValidationErrorModel
/// An Response validation error returned from an API request.
///
public struct ResponseValidationErrorModel: Codable, Equatable {
// MARK: Properties
/// A string that represents the error code.
public let error: String
/// A string that provides a description of the error.
public let errorDescription: String?
/// An `ErrorModel` object that provides more details about the error.
public let errorModel: ErrorModel
/// Public instance of synthesized initializer.
public init(error: String, errorDescription: String?, errorModel: ErrorModel) {
self.error = error
self.errorDescription = errorDescription
self.errorModel = errorModel
}
}
public struct ErrorModel: Codable, Equatable {
// MARK: Properties
/// A string that provides a message about the error.
public let message: String
/// A string that represents an object associated with the error.
public let object: String
/// Public instance of synthesized initializer.
public init(message: String, object: String) {
self.message = message
self.object = object
}
}
// MARK: JSONResponse
extension ResponseValidationErrorModel: JSONResponse {
public static let decoder = JSONDecoder.pascalOrSnakeCaseDecoder
}

View File

@ -1,7 +1,8 @@
import BitwardenKitMocks
import TestHelpers
import XCTest
@testable import BitwardenShared
@testable import BitwardenKit
// MARK: - ResponseValidationErrorModelTests

View File

@ -137,7 +137,3 @@ public extension APITestData {
bundle: BitwardenKitMocksBundleFinder.bundle
)
}
class BitwardenKitMocksBundleFinder {
static let bundle = Bundle(for: BitwardenKitMocksBundleFinder.self)
}

View File

@ -1,11 +1,10 @@
import BitwardenKit
import Foundation
// MARK: - ServerError
/// An enumeration of server errors.
///
enum ServerError: Error, Equatable, CustomNSError {
public enum ServerError: Error, Equatable, CustomNSError {
/// A generic error.
///
/// - Parameter errorResponse: The error response returned from the API request.
@ -19,7 +18,7 @@ enum ServerError: Error, Equatable, CustomNSError {
case validationError(validationErrorResponse: ResponseValidationErrorModel)
/// A computed property that returns an error message based on the case.
var message: String {
public var message: String {
switch self {
case let .error(errorResponse):
errorResponse.singleMessage()

View File

@ -0,0 +1,11 @@
import Foundation
import TestHelpers
public extension APITestData {
/// A standard Bitwarden error message of "You do not have permissions to edit this."
static let bitwardenErrorMessage = loadFromJsonBundle(
resource: "BitwardenErrorMessage",
bundle: BitwardenKitMocksBundleFinder.bundle
)
}

View File

@ -0,0 +1,5 @@
import Foundation
class BitwardenKitMocksBundleFinder {
static let bundle = Bundle(for: BitwardenKitMocksBundleFinder.self)
}

View File

@ -1,15 +1,21 @@
import BitwardenKit
import Networking
// MARK: - ResponseValidationError
/// An error indicating that the response was invalid and didn't contain a successful HTTP status code.
///
struct ResponseValidationError: Error, Equatable {
public struct ResponseValidationError: Error, Equatable {
// MARK: Properties
/// The received HTTP response.
let response: HTTPResponse
public let response: HTTPResponse
// MARK: Initializers
/// Public version of synthesized initializer.
public init(response: HTTPResponse) {
self.response = response
}
}
// MARK: - ResponseValidationHandler
@ -17,8 +23,17 @@ struct ResponseValidationError: Error, Equatable {
/// A `ResponseHandler` that validates that HTTP responses contain successful (2XX) HTTP status
/// codes or tries to parse the error otherwise.
///
class ResponseValidationHandler: ResponseHandler {
func handle(_ response: inout HTTPResponse) async throws -> HTTPResponse {
public final class ResponseValidationHandler: ResponseHandler {
/// Public version of synthesized initializer.
public init() {}
/// Handles receiving a `HTTPResponse`. The handler can view or modify the response before
/// returning it to continue to handler chain.
///
/// - Parameter response: The `HTTPResponse` that was received by the `HTTPClient`.
/// - Returns: The original or modified `HTTPResponse`.
///
public func handle(_ response: inout HTTPResponse) async throws -> HTTPResponse {
guard (200 ..< 300).contains(response.statusCode) else {
// If the response can be parsed, throw an error containing the response message.
if let errorResponse = try? ErrorResponseModel(response: response) {

View File

@ -1,9 +1,8 @@
import BitwardenKit
import Networking
import TestHelpers
import XCTest
@testable import BitwardenShared
@testable import BitwardenKit
class ResponseValidationHandlerTests: BitwardenTestCase {
// MARK: Properties

View File

@ -1,35 +0,0 @@
import Foundation
import Networking
// MARK: - ResponseValidationErrorModel
/// An Response validation error returned from an API request.
///
struct ResponseValidationErrorModel: Codable, Equatable {
// MARK: Properties
/// A string that represents the error code.
let error: String
/// A string that provides a description of the error.
let errorDescription: String?
/// An `ErrorModel` object that provides more details about the error.
let errorModel: ErrorModel
}
struct ErrorModel: Codable, Equatable {
// MARK: Properties
/// A string that provides a message about the error.
let message: String
/// A string that represents an object associated with the error.
let object: String
}
// MARK: JSONResponse
extension ResponseValidationErrorModel: JSONResponse {
static let decoder = JSONDecoder.pascalOrSnakeCaseDecoder
}

View File

@ -1,3 +1,4 @@
import BitwardenKit
import Foundation
public extension Error {

View File

@ -1,4 +1,5 @@
import BitwardenKit
import BitwardenKitMocks
import Networking
import TestHelpers
import XCTest

View File

@ -1,8 +0,0 @@
{
"message": "You do not have permissions to edit this.",
"validationErrors": null,
"exceptionMessage": null,
"exceptionStackTrace": null,
"innerExceptionMessage": null,
"object": "error"
}

View File

@ -1,38 +0,0 @@
import BitwardenKit
import Networking
// MARK: - ResponseValidationError
/// An error indicating that the response was invalid and didn't contain a successful HTTP status code.
///
struct ResponseValidationError: Error, Equatable {
// MARK: Properties
/// The received HTTP response.
let response: HTTPResponse
}
// MARK: - ResponseValidationHandler
/// A `ResponseHandler` that validates that HTTP responses contain successful (2XX) HTTP status
/// codes or tries to parse the error otherwise.
///
final class ResponseValidationHandler: ResponseHandler {
func handle(_ response: inout HTTPResponse) async throws -> HTTPResponse {
guard (200 ..< 300).contains(response.statusCode) else {
// If the response can be parsed, throw an error containing the response message.
if let errorResponse = try? ErrorResponseModel(response: response) {
throw ServerError.error(errorResponse: errorResponse)
}
// If the response can be parsed, throw an error containing the response message.
if let validationErrorResponse = try? ResponseValidationErrorModel(response: response) {
throw ServerError.validationError(validationErrorResponse: validationErrorResponse)
}
// Otherwise, throw a generic response validation error.
throw ResponseValidationError(response: response)
}
return response
}
}

View File

@ -1,3 +1,4 @@
import BitwardenKit
import Networking
import XCTest

View File

@ -1,3 +1,4 @@
import BitwardenKit
import BitwardenSdk
import Foundation

View File

@ -1,3 +1,4 @@
import BitwardenKit
import BitwardenSdk
// MARK: - DeleteAccountProcessor

View File

@ -1,3 +1,4 @@
import BitwardenKit
import BitwardenSdk
import Foundation

View File

@ -22,8 +22,3 @@ public struct APITestData {
loadFromBundle(resource: resource, extension: "json", bundle: bundle)
}
}
public extension APITestData {
/// A standard Bitwarden error message of "You do not have permissions to edit this."
static let bitwardenErrorMessage = loadFromJsonBundle(resource: "BitwardenErrorMessage")
}