diff --git a/BitwardenKit/Core/Platform/Services/BiometricsStateService.swift b/BitwardenKit/Core/Platform/Services/BiometricsStateService.swift new file mode 100644 index 0000000000..13e63b26f7 --- /dev/null +++ b/BitwardenKit/Core/Platform/Services/BiometricsStateService.swift @@ -0,0 +1,21 @@ +// MARK: - BiometricsStateService + +/// A protocol for a service that provides state management functionality around biometrics. +/// +public protocol BiometricsStateService: ActiveAccountStateProvider { + /// Get the active user's Biometric Authentication Preference. + /// + /// - Returns: A `Bool` indicating the user's preference for using biometric authentication. + /// If `true`, the device should attempt biometric authentication for authorization events. + /// If `false`, the device should not attempt biometric authentication for authorization events. + /// + func getBiometricAuthenticationEnabled() async throws -> Bool + + /// Sets the user's Biometric Authentication Preference. + /// + /// - Parameter isEnabled: A `Bool` indicating the user's preference for using biometric authentication. + /// If `true`, the device should attempt biometric authentication for authorization events. + /// If `false`, the device should not attempt biometric authentication for authorization events. + /// + func setBiometricAuthenticationEnabled(_ isEnabled: Bool?) async throws +} diff --git a/BitwardenKit/Core/Platform/Services/Mocks/MockBiometricsStateService.swift b/BitwardenKit/Core/Platform/Services/Mocks/MockBiometricsStateService.swift new file mode 100644 index 0000000000..d09cb7b8ce --- /dev/null +++ b/BitwardenKit/Core/Platform/Services/Mocks/MockBiometricsStateService.swift @@ -0,0 +1,29 @@ +import BitwardenKit +import TestHelpers + +public class MockBiometricsStateService: BiometricsStateService { + public var activeAccountIdError: Error? + public var activeAccountIdResult = Result.failure(BitwardenTestError.mock("Mock error not set")) + public var biometricAuthenticationEnabledResult: Result = .success(false) + public var setBiometricAuthenticationEnabledError: Error? + + public init() {} + + public func getActiveAccountId() async throws -> String { + if let activeAccountIdError { + throw activeAccountIdError + } + return try activeAccountIdResult.get() + } + + public func getBiometricAuthenticationEnabled() async throws -> Bool { + try biometricAuthenticationEnabledResult.get() + } + + public func setBiometricAuthenticationEnabled(_ isEnabled: Bool?) async throws { + if let setBiometricAuthenticationEnabledError { + throw setBiometricAuthenticationEnabledError + } + biometricAuthenticationEnabledResult = .success(isEnabled ?? false) + } +} diff --git a/BitwardenShared/Core/Auth/Services/Biometrics/BiometricsRepository.swift b/BitwardenShared/Core/Auth/Services/Biometrics/BiometricsRepository.swift index 91ff7b8f44..11c42376b7 100644 --- a/BitwardenShared/Core/Auth/Services/Biometrics/BiometricsRepository.swift +++ b/BitwardenShared/Core/Auth/Services/Biometrics/BiometricsRepository.swift @@ -1,3 +1,4 @@ +import BitwardenKit import BitwardenSdk import LocalAuthentication @@ -66,7 +67,7 @@ class DefaultBiometricsRepository: BiometricsRepository { var keychainRepository: KeychainRepository /// A service used to update user preferences. - var stateService: StateService + var stateService: BiometricsStateService // MARK: Initialization @@ -80,7 +81,7 @@ class DefaultBiometricsRepository: BiometricsRepository { init( biometricsService: BiometricsService, keychainService: KeychainRepository, - stateService: StateService, + stateService: BiometricsStateService, ) { self.biometricsService = biometricsService keychainRepository = keychainService diff --git a/BitwardenShared/Core/Auth/Services/Biometrics/BiometricsRepositoryTests.swift b/BitwardenShared/Core/Auth/Services/Biometrics/BiometricsRepositoryTests.swift index 69d33c98f7..d0001b2f5e 100644 --- a/BitwardenShared/Core/Auth/Services/Biometrics/BiometricsRepositoryTests.swift +++ b/BitwardenShared/Core/Auth/Services/Biometrics/BiometricsRepositoryTests.swift @@ -1,3 +1,4 @@ +import BitwardenKitMocks import LocalAuthentication import TestHelpers import XCTest @@ -8,17 +9,11 @@ import XCTest // MARK: - BiometricsRepositoryTests final class BiometricsRepositoryTests: BitwardenTestCase { // swiftlint:disable:this type_body_length - // MARK: Types - - enum TestError: Error, Equatable { - case mock(String) - } - // MARK: Properties var biometricsService: MockBiometricsService! var keychainService: MockKeychainRepository! - var stateService: MockStateService! + var stateService: MockBiometricsStateService! var subject: DefaultBiometricsRepository! // MARK: Setup & Teardown @@ -28,7 +23,7 @@ final class BiometricsRepositoryTests: BitwardenTestCase { // swiftlint:disable: biometricsService = MockBiometricsService() keychainService = MockKeychainRepository() - stateService = MockStateService() + stateService = MockBiometricsStateService() subject = DefaultBiometricsRepository( biometricsService: biometricsService, @@ -57,17 +52,17 @@ final class BiometricsRepositoryTests: BitwardenTestCase { // swiftlint:disable: XCTAssertNil(subject.getBiometricAuthenticationType()) } - /// `setBiometricUnlockKey` throws for a no user situation. + /// `setBiometricUnlockKey` throws for a no-user situation. func test_getBiometricUnlockKey_noActiveAccount() async throws { - stateService.activeAccount = nil - await assertAsyncThrows(error: StateServiceError.noActiveAccount) { + stateService.activeAccountIdResult = .failure(BitwardenTestError.example) + await assertAsyncThrows(error: BitwardenTestError.example) { _ = try await subject.getUserAuthKey() } } /// `setBiometricUnlockKey` throws for a keychain error. func test_getBiometricUnlockKey_keychainServiceError() async throws { - stateService.activeAccount = .fixture() + stateService.activeAccountIdResult = .success("1") keychainService.getResult = .failure( KeychainServiceError.keyNotFound(.biometrics(userId: "1")), ) @@ -79,7 +74,7 @@ final class BiometricsRepositoryTests: BitwardenTestCase { // swiftlint:disable: /// `setBiometricUnlockKey` throws an error for an empty key. func test_getBiometricUnlockKey_emptyString() async throws { let expectedKey = "" - stateService.activeAccount = .fixture() + stateService.activeAccountIdResult = .success("1") keychainService.getResult = .success(expectedKey) await assertAsyncThrows(error: BiometricsServiceError.getAuthKeyFailed) { _ = try await subject.getUserAuthKey() @@ -89,7 +84,7 @@ final class BiometricsRepositoryTests: BitwardenTestCase { // swiftlint:disable: /// `setBiometricUnlockKey` returns the correct key for the active user. func test_getBiometricUnlockKey_success() async throws { let expectedKey = "expectedKey" - stateService.activeAccount = .fixture() + stateService.activeAccountIdResult = .success("1") keychainService.getResult = .success(expectedKey) let key = try await subject.getUserAuthKey() XCTAssertEqual(key, expectedKey) @@ -98,7 +93,7 @@ final class BiometricsRepositoryTests: BitwardenTestCase { // swiftlint:disable: /// `setBiometricUnlockKey` throws a cancelled error for `errSecAuthFailed` if the device is /// locked while performing biometrics. func test_getBiometricUnlockKey_authFailed() async throws { - stateService.activeAccount = .fixture() + stateService.activeAccountIdResult = .success("1") keychainService.getResult = .failure( KeychainServiceError.osStatusError(errSecAuthFailed), ) @@ -109,12 +104,9 @@ final class BiometricsRepositoryTests: BitwardenTestCase { // swiftlint:disable: /// `getBiometricUnlockStatus` throws an error if the user has locked biometrics. func test_getBiometricUnlockStatus_lockout() async throws { - let active = Account.fixture() - stateService.activeAccount = active + stateService.activeAccountIdResult = .success("1") biometricsService.biometricAuthStatus = .lockedOut(.faceID) - stateService.biometricsEnabled = [ - active.profile.userId: false, - ] + stateService.biometricAuthenticationEnabledResult = .success(false) await assertAsyncThrows(error: BiometricsServiceError.biometryLocked) { _ = try await subject.getBiometricUnlockStatus() } @@ -122,12 +114,9 @@ final class BiometricsRepositoryTests: BitwardenTestCase { // swiftlint:disable: /// `getBiometricUnlockStatus` tracks the availability of biometrics. func test_getBiometricUnlockStatus_success_denied() async throws { - let active = Account.fixture() - stateService.activeAccount = active + stateService.activeAccountIdResult = .success("1") biometricsService.biometricAuthStatus = .denied(.touchID) - stateService.biometricsEnabled = [ - active.profile.userId: false, - ] + stateService.biometricAuthenticationEnabledResult = .success(false) let status = try await subject.getBiometricUnlockStatus() XCTAssertEqual( status, @@ -137,12 +126,9 @@ final class BiometricsRepositoryTests: BitwardenTestCase { // swiftlint:disable: /// `getBiometricUnlockStatus` tracks if a user has enabled or disabled biometrics. func test_getBiometricUnlockStatus_success_disabled() async throws { - let active = Account.fixture() - stateService.activeAccount = active + stateService.activeAccountIdResult = .success("1") + stateService.biometricAuthenticationEnabledResult = .success(false) biometricsService.biometricAuthStatus = .authorized(.touchID) - stateService.biometricsEnabled = [ - active.profile.userId: false, - ] let status = try await subject.getBiometricUnlockStatus() XCTAssertEqual( status, @@ -152,12 +138,9 @@ final class BiometricsRepositoryTests: BitwardenTestCase { // swiftlint:disable: /// `getBiometricUnlockStatus` tracks all biometrics components. func test_getBiometricUnlockStatus_success() async throws { - let active = Account.fixture() - stateService.activeAccount = active + stateService.activeAccountIdResult = .success("1") + stateService.biometricAuthenticationEnabledResult = .success(true) biometricsService.biometricAuthStatus = .authorized(.faceID) - stateService.biometricsEnabled = [ - active.profile.userId: true, - ] let status = try await subject.getBiometricUnlockStatus() XCTAssertEqual( status, @@ -167,11 +150,8 @@ final class BiometricsRepositoryTests: BitwardenTestCase { // swiftlint:disable: /// `getUserAuthKey` throws on empty keys. func test_getUserAuthKey_emptyString() async throws { - let active = Account.fixture() - stateService.activeAccount = active - stateService.biometricsEnabled = [ - active.profile.userId: true, - ] + stateService.activeAccountIdResult = .success("1") + stateService.biometricAuthenticationEnabledResult = .success(true) keychainService.getResult = .success("") await assertAsyncThrows(error: BiometricsServiceError.getAuthKeyFailed) { _ = try await subject.getUserAuthKey() @@ -180,11 +160,8 @@ final class BiometricsRepositoryTests: BitwardenTestCase { // swiftlint:disable: /// `getUserAuthKey` retrieves the key from keychain. func test_getUserAuthKey_success() async throws { - let active = Account.fixture() - stateService.activeAccount = active - stateService.biometricsEnabled = [ - active.profile.userId: true, - ] + stateService.activeAccountIdResult = .success("1") + stateService.biometricAuthenticationEnabledResult = .success(true) keychainService.getResult = .success("Dramatic Masterpiece") let key = try await subject.getUserAuthKey() XCTAssertEqual( @@ -195,11 +172,8 @@ final class BiometricsRepositoryTests: BitwardenTestCase { // swiftlint:disable: /// `getUserAuthKey` throws a biometry locked error if biometrics are locked out. func test_getUserAuthKey_lockedError() async throws { - let active = Account.fixture() - stateService.activeAccount = active - stateService.biometricsEnabled = [ - active.profile.userId: true, - ] + stateService.activeAccountIdResult = .success("1") + stateService.biometricAuthenticationEnabledResult = .success(true) // -8 is the code for kLAErrorBiometryLockout. keychainService.getResult = .failure(KeychainServiceError.osStatusError(-8)) await assertAsyncThrows(error: BiometricsServiceError.biometryLocked) { @@ -209,11 +183,8 @@ final class BiometricsRepositoryTests: BitwardenTestCase { // swiftlint:disable: /// `getUserAuthKey` throws a not found error if the key can't be found. func test_getUserAuthKey_notFoundError() async throws { - let active = Account.fixture() - stateService.activeAccount = active - stateService.biometricsEnabled = [ - active.profile.userId: true, - ] + stateService.activeAccountIdResult = .success("1") + stateService.biometricAuthenticationEnabledResult = .success(true) keychainService.getResult = .failure(KeychainServiceError.osStatusError(errSecItemNotFound)) await assertAsyncThrows(error: BiometricsServiceError.getAuthKeyFailed) { _ = try await subject.getUserAuthKey() @@ -222,11 +193,8 @@ final class BiometricsRepositoryTests: BitwardenTestCase { // swiftlint:disable: /// `getUserAuthKey` throws a biometry failed error if biometrics are disconnected. func test_getUserAuthKey_biometryFailed() async throws { - let active = Account.fixture() - stateService.activeAccount = active - stateService.biometricsEnabled = [ - active.profile.userId: true, - ] + stateService.activeAccountIdResult = .success("1") + stateService.biometricAuthenticationEnabledResult = .success(true) keychainService.getResult = .failure(KeychainServiceError.osStatusError(kLAErrorBiometryDisconnected)) await assertAsyncThrows(error: BiometricsServiceError.biometryFailed) { _ = try await subject.getUserAuthKey() @@ -235,11 +203,8 @@ final class BiometricsRepositoryTests: BitwardenTestCase { // swiftlint:disable: /// `getUserAuthKey` throws a biometry cancelled error if biometrics were cancelled. func test_getUserAuthKey_cancelled() async throws { - let active = Account.fixture() - stateService.activeAccount = active - stateService.biometricsEnabled = [ - active.profile.userId: true, - ] + stateService.activeAccountIdResult = .success("1") + stateService.biometricAuthenticationEnabledResult = .success(true) // Send the user cancelled code. keychainService.getResult = .failure(KeychainServiceError.osStatusError(errSecUserCanceled)) await assertAsyncThrows(error: BiometricsServiceError.biometryCancelled) { @@ -249,11 +214,8 @@ final class BiometricsRepositoryTests: BitwardenTestCase { // swiftlint:disable: /// `getUserAuthKey` throws an error if one occurs. func test_getUserAuthKey_unknownError() async throws { - let active = Account.fixture() - stateService.activeAccount = active - stateService.biometricsEnabled = [ - active.profile.userId: true, - ] + stateService.activeAccountIdResult = .success("1") + stateService.biometricAuthenticationEnabledResult = .success(true) keychainService.getResult = .failure(BitwardenTestError.example) await assertAsyncThrows(error: BitwardenTestError.example) { _ = try await subject.getUserAuthKey() @@ -262,11 +224,8 @@ final class BiometricsRepositoryTests: BitwardenTestCase { // swiftlint:disable: /// `getUserAuthKey` throws an error if an unknown OS error occurs. func test_getUserAuthKey_unknownOSError() async throws { - let active = Account.fixture() - stateService.activeAccount = active - stateService.biometricsEnabled = [ - active.profile.userId: true, - ] + stateService.activeAccountIdResult = .success("1") + stateService.biometricAuthenticationEnabledResult = .success(true) keychainService.getResult = .failure(KeychainServiceError.osStatusError(errSecParam)) await assertAsyncThrows(error: KeychainServiceError.osStatusError(errSecParam)) { _ = try await subject.getUserAuthKey() @@ -275,20 +234,19 @@ final class BiometricsRepositoryTests: BitwardenTestCase { // swiftlint:disable: /// `setBiometricUnlockKey` throws when there is no active account. func test_setBiometricUnlockKey_nilValue_noActiveAccount() async throws { - stateService.activeAccount = nil - await assertAsyncThrows(error: StateServiceError.noActiveAccount) { + stateService.setBiometricAuthenticationEnabledError = BitwardenTestError.mock("NoActiveAccount") + await assertAsyncThrows(error: BitwardenTestError.mock("NoActiveAccount")) { try await subject.setBiometricUnlockKey(authKey: nil) } } /// `setBiometricUnlockKey` throws when there is a state service error. func test_setBiometricUnlockKey_nilValue_setBiometricAuthenticationEnabledFailed() async throws { - stateService.activeAccount = .fixture() - stateService.setBiometricAuthenticationEnabledResult = .failure( - TestError.mock("setBiometricAuthenticationEnabledFailed"), - ) + stateService.activeAccountIdResult = .success("1") + let error = BitwardenTestError.mock("setBiometricAuthenticationEnabledFailed") + stateService.setBiometricAuthenticationEnabledError = error await assertAsyncThrows( - error: TestError.mock("setBiometricAuthenticationEnabledFailed"), + error: BitwardenTestError.mock("setBiometricAuthenticationEnabledFailed"), ) { try await subject.setBiometricUnlockKey(authKey: nil) } @@ -296,39 +254,37 @@ final class BiometricsRepositoryTests: BitwardenTestCase { // swiftlint:disable: /// `setBiometricUnlockKey` A failure in evaluating the biometrics policy clears any auth key. func test_setBiometricUnlockKey_evaluationFalse() async throws { - stateService.activeAccount = .fixture() - try? await stateService.setBiometricAuthenticationEnabled(true) + stateService.activeAccountIdResult = .success("1") + stateService.biometricAuthenticationEnabledResult = .success(true) keychainService.mockStorage = [ keychainService.formattedKey(for: .biometrics(userId: "1")): "storedKey", ] biometricsService.evaluationResult = false - stateService.setBiometricAuthenticationEnabledResult = .success(()) keychainService.deleteResult = .success(()) try await subject.setBiometricUnlockKey(authKey: nil) waitFor(keychainService.mockStorage.isEmpty) - let result = try XCTUnwrap(stateService.biometricsEnabled["1"]) + let result = try stateService.biometricAuthenticationEnabledResult.get() XCTAssertFalse(result) } /// `setBiometricUnlockKey` can remove a user key from the keychain and track the availability in state. func test_setBiometricUnlockKey_nilValue_success() async throws { - stateService.activeAccount = .fixture() - try? await stateService.setBiometricAuthenticationEnabled(true) + stateService.activeAccountIdResult = .success("1") + stateService.biometricAuthenticationEnabledResult = .success(true) keychainService.mockStorage = [ keychainService.formattedKey(for: .biometrics(userId: "1")): "storedKey", ] - stateService.setBiometricAuthenticationEnabledResult = .success(()) keychainService.deleteResult = .success(()) try await subject.setBiometricUnlockKey(authKey: nil) waitFor(keychainService.mockStorage.isEmpty) - let result = try XCTUnwrap(stateService.biometricsEnabled["1"]) + let result = try stateService.biometricAuthenticationEnabledResult.get() XCTAssertFalse(result) } /// `setBiometricUnlockKey` throws on a keychain error. func test_setBiometricUnlockKey_nilValue_successWithKeychainError() async throws { - stateService.activeAccount = .fixture() - stateService.setBiometricAuthenticationEnabledResult = .success(()) + stateService.activeAccountIdResult = .success("1") + stateService.biometricAuthenticationEnabledResult = .success(true) keychainService.deleteResult = .failure(KeychainServiceError.osStatusError(13)) await assertAsyncDoesNotThrow { try await subject.setBiometricUnlockKey(authKey: nil) @@ -337,20 +293,19 @@ final class BiometricsRepositoryTests: BitwardenTestCase { // swiftlint:disable: /// `setBiometricUnlockKey` throws when there is no active account. func test_setBiometricUnlockKey_withValue_noActiveAccount() async throws { - stateService.activeAccount = nil - await assertAsyncThrows(error: StateServiceError.noActiveAccount) { + stateService.activeAccountIdError = BitwardenTestError.mock("NoActiveAccount") + await assertAsyncThrows(error: BitwardenTestError.mock("NoActiveAccount")) { try await subject.setBiometricUnlockKey(authKey: "authKey") } } /// `setBiometricUnlockKey` throws when there is no active account. func test_setBiometricUnlockKey_withValue_setBiometricAuthenticationEnabledFailed() async throws { - stateService.activeAccount = .fixture() - stateService.setBiometricAuthenticationEnabledResult = .failure( - TestError.mock("setBiometricAuthenticationEnabledFailed"), - ) + stateService.activeAccountIdResult = .success("1") + let error = BitwardenTestError.mock("setBiometricAuthenticationEnabledFailed") + stateService.setBiometricAuthenticationEnabledError = error await assertAsyncThrows( - error: TestError.mock("setBiometricAuthenticationEnabledFailed"), + error: BitwardenTestError.mock("setBiometricAuthenticationEnabledFailed"), ) { try await subject.setBiometricUnlockKey(authKey: "authKey") } @@ -358,8 +313,8 @@ final class BiometricsRepositoryTests: BitwardenTestCase { // swiftlint:disable: /// `setBiometricUnlockKey` throws on a keychain error. func test_setBiometricUnlockKey_withValue_keychainError() async throws { - stateService.activeAccount = .fixture() - stateService.setBiometricAuthenticationEnabledResult = .success(()) + stateService.activeAccountIdResult = .success("1") + stateService.biometricAuthenticationEnabledResult = .success(true) keychainService.setResult = .failure(KeychainServiceError.osStatusError(13)) await assertAsyncThrows( error: BiometricsServiceError.setAuthKeyFailed, @@ -370,8 +325,8 @@ final class BiometricsRepositoryTests: BitwardenTestCase { // swiftlint:disable: /// `setBiometricUnlockKey` can store a user key to the keychain and track the availability in state. func test_setBiometricUnlockKey_withValue_success() async throws { - stateService.activeAccount = .fixture() - stateService.setBiometricAuthenticationEnabledResult = .success(()) + stateService.activeAccountIdResult = .success("1") + stateService.biometricAuthenticationEnabledResult = .success(true) keychainService.setResult = .success(()) try await subject.setBiometricUnlockKey(authKey: "authKey") waitFor(!keychainService.mockStorage.isEmpty) @@ -383,7 +338,7 @@ final class BiometricsRepositoryTests: BitwardenTestCase { // swiftlint:disable: ), )], ) - let result = try XCTUnwrap(stateService.biometricsEnabled["1"]) + let result = try stateService.biometricAuthenticationEnabledResult.get() XCTAssertTrue(result) XCTAssertEqual(keychainService.securityType, .biometryCurrentSet) } diff --git a/BitwardenShared/Core/Platform/Services/StateService.swift b/BitwardenShared/Core/Platform/Services/StateService.swift index 202f998f74..b3f34bd61b 100644 --- a/BitwardenShared/Core/Platform/Services/StateService.swift +++ b/BitwardenShared/Core/Platform/Services/StateService.swift @@ -146,14 +146,6 @@ protocol StateService: AnyObject { /// func getAppTheme() async -> AppTheme - /// Get the active user's Biometric Authentication Preference. - /// - /// - Returns: A `Bool` indicating the user's preference for using biometric authentication. - /// If `true`, the device should attempt biometric authentication for authorization events. - /// If `false`, the device should not attempt biometric authentication for authorization events. - /// - func getBiometricAuthenticationEnabled() async throws -> Bool - /// Gets the clear clipboard value for an account. /// /// - Parameter userId: The user ID associated with the clear clipboard value. Defaults to the active @@ -535,14 +527,6 @@ protocol StateService: AnyObject { /// func setAppTheme(_ appTheme: AppTheme) async - /// Sets the user's Biometric Authentication Preference. - /// - /// - Parameter isEnabled: A `Bool` indicating the user's preference for using biometric authentication. - /// If `true`, the device should attempt biometric authentication for authorization events. - /// If `false`, the device should not attempt biometric authentication for authorization events. - /// - func setBiometricAuthenticationEnabled(_ isEnabled: Bool?) async throws - /// Sets the clear clipboard value for an account. /// /// - Parameters: @@ -2376,7 +2360,7 @@ struct AccountVolatileData { // MARK: Biometrics -extension DefaultStateService { +extension DefaultStateService: BiometricsStateService { func getBiometricAuthenticationEnabled() async throws -> Bool { let userId = try getActiveAccountUserId() return appSettingsStore.isBiometricAuthenticationEnabled(userId: userId) diff --git a/TestHelpers/Support/BitwardenTestError.swift b/TestHelpers/Support/BitwardenTestError.swift index 0257dbe366..27a8995759 100644 --- a/TestHelpers/Support/BitwardenTestError.swift +++ b/TestHelpers/Support/BitwardenTestError.swift @@ -7,13 +7,16 @@ import Foundation /// /// These errors will typically be provided to a mocked type to be thrown at the /// appropriate time. XCAssertThrows -public enum BitwardenTestError: LocalizedError { +public enum BitwardenTestError: Equatable, LocalizedError { case example + case mock(String) public var errorDescription: String? { switch self { case .example: "An example error used to test throwing capabilities." + case .mock(let string): + "A mock error with the string: \(string)." } } }