mirror of
https://github.com/bitwarden/ios.git
synced 2025-12-13 02:58:59 -06:00
777 lines
32 KiB
Swift
777 lines
32 KiB
Swift
import AuthenticationServices
|
|
import Networking
|
|
import TestHelpers
|
|
import XCTest
|
|
|
|
@testable import BitwardenShared
|
|
|
|
// MARK: - CreateAccountProcessorTests
|
|
|
|
// swiftlint:disable:next type_body_length
|
|
class CreateAccountProcessorTests: BitwardenTestCase {
|
|
// MARK: Properties
|
|
|
|
var authRepository: MockAuthRepository!
|
|
var captchaService: MockCaptchaService!
|
|
var client: MockHTTPClient!
|
|
var authClient: MockAuthClient!
|
|
var coordinator: MockCoordinator<AuthRoute, AuthEvent>!
|
|
var errorReporter: MockErrorReporter!
|
|
var subject: CreateAccountProcessor!
|
|
|
|
// MARK: Setup & Teardown
|
|
|
|
override func setUp() {
|
|
super.setUp()
|
|
authRepository = MockAuthRepository()
|
|
captchaService = MockCaptchaService()
|
|
client = MockHTTPClient()
|
|
authClient = MockAuthClient()
|
|
coordinator = MockCoordinator<AuthRoute, AuthEvent>()
|
|
errorReporter = MockErrorReporter()
|
|
subject = CreateAccountProcessor(
|
|
coordinator: coordinator.asAnyCoordinator(),
|
|
services: ServiceContainer.withMocks(
|
|
authRepository: authRepository,
|
|
captchaService: captchaService,
|
|
clientService: MockClientService(auth: authClient),
|
|
errorReporter: errorReporter,
|
|
httpClient: client
|
|
),
|
|
state: CreateAccountState()
|
|
)
|
|
}
|
|
|
|
override func tearDown() {
|
|
super.tearDown()
|
|
authRepository = nil
|
|
captchaService = nil
|
|
authClient = nil
|
|
client = nil
|
|
coordinator = nil
|
|
errorReporter = nil
|
|
subject = nil
|
|
}
|
|
|
|
// MARK: Tests
|
|
|
|
/// `captchaCompleted()` makes the create account request again, this time with a captcha token.
|
|
/// Also tests that the user is then navigated to the login screen.
|
|
@MainActor
|
|
func test_captchaCompleted() throws {
|
|
CreateAccountRequestModel.encoder.outputFormatting = .sortedKeys
|
|
subject.state.isTermsAndPrivacyToggleOn = true
|
|
authClient.hashPasswordResult = .success("hashed password")
|
|
client.result = .httpSuccess(testData: .createAccountRequest)
|
|
subject.state = .fixture()
|
|
subject.captchaCompleted(token: "token")
|
|
|
|
let createAccountRequest = CreateAccountRequestModel(
|
|
captchaResponse: "token",
|
|
email: "email@example.com",
|
|
kdfConfig: KdfConfig(),
|
|
key: "encryptedUserKey",
|
|
keys: KeysRequestModel(
|
|
encryptedPrivateKey: "private",
|
|
publicKey: "public"
|
|
),
|
|
masterPasswordHash: "hashed password",
|
|
masterPasswordHint: ""
|
|
)
|
|
|
|
waitFor(!coordinator.routes.isEmpty)
|
|
|
|
XCTAssertEqual(client.requests.count, 1)
|
|
XCTAssertEqual(client.requests[0].body, try createAccountRequest.encode())
|
|
XCTAssertEqual(authClient.hashPasswordPassword, "password1234")
|
|
XCTAssertEqual(authClient.hashPasswordKdfParams, .pbkdf2(iterations: 600_000))
|
|
XCTAssertEqual(coordinator.routes.last, .login(username: "email@example.com"))
|
|
}
|
|
|
|
/// `captchaErrored(error:)` records an error.
|
|
@MainActor
|
|
func test_captchaErrored() {
|
|
subject.captchaErrored(error: BitwardenTestError.example)
|
|
|
|
waitFor(!coordinator.alertShown.isEmpty)
|
|
XCTAssertEqual(coordinator.alertShown.last, .networkResponseError(BitwardenTestError.example))
|
|
XCTAssertEqual(errorReporter.errors.last as? BitwardenTestError, .example)
|
|
}
|
|
|
|
/// `captchaErrored(error:)` doesn't record an error if the captcha flow was cancelled.
|
|
@MainActor
|
|
func test_captchaErrored_cancelled() {
|
|
let error = NSError(domain: "", code: ASWebAuthenticationSessionError.canceledLogin.rawValue)
|
|
subject.captchaErrored(error: error)
|
|
XCTAssertTrue(errorReporter.errors.isEmpty)
|
|
}
|
|
|
|
/// `perform(_:)` with `.createAccount` will still make the `CreateAccountRequest` when the HIBP
|
|
/// network request fails.
|
|
@MainActor
|
|
func test_perform_checkPasswordAndCreateAccount_failure() async throws {
|
|
subject.state = .fixture(isCheckDataBreachesToggleOn: true)
|
|
|
|
client.results = [.httpFailure(URLError(.timedOut) as Error), .httpSuccess(testData: .createAccountRequest)]
|
|
|
|
await subject.perform(.createAccount)
|
|
|
|
XCTAssertEqual(client.requests.count, 2)
|
|
XCTAssertEqual(client.requests[0].url, URL(string: "https://api.pwnedpasswords.com/range/e6b6a"))
|
|
XCTAssertEqual(client.requests[1].url, URL(string: "https://example.com/identity/accounts/register"))
|
|
XCTAssertEqual(coordinator.routes.last, .login(username: "email@example.com"))
|
|
|
|
XCTAssertFalse(coordinator.isLoadingOverlayShowing)
|
|
XCTAssertEqual(
|
|
coordinator.loadingOverlaysShown,
|
|
[
|
|
LoadingOverlayState(title: Localizations.creatingAccount),
|
|
LoadingOverlayState(title: Localizations.creatingAccount),
|
|
]
|
|
)
|
|
}
|
|
|
|
/// `perform(_:)` with `.createAccount` presents an alert when the password
|
|
/// is weak and exposed. This also tests that the correct alert is presented.
|
|
/// Additionally, this tests that tapping Yes on the alert creates the account.
|
|
@MainActor
|
|
func test_perform_checkPasswordAndCreateAccount_exposedWeak_yesTapped() async throws {
|
|
subject.state = .fixture(isCheckDataBreachesToggleOn: true, passwordStrengthScore: 1)
|
|
|
|
client.results = [.httpSuccess(testData: .hibpLeakedPasswords), .httpSuccess(testData: .createAccountRequest)]
|
|
|
|
await subject.perform(.createAccount)
|
|
|
|
let alert = try XCTUnwrap(coordinator.alertShown.last)
|
|
try await alert.tapAction(title: Localizations.yes)
|
|
|
|
XCTAssertEqual(client.requests.count, 2)
|
|
XCTAssertEqual(client.requests[0].url, URL(string: "https://api.pwnedpasswords.com/range/e6b6a"))
|
|
XCTAssertEqual(client.requests[1].url, URL(string: "https://example.com/identity/accounts/register"))
|
|
XCTAssertEqual(coordinator.alertShown.last, Alert(
|
|
title: Localizations.weakAndExposedMasterPassword,
|
|
message: Localizations.weakPasswordIdentifiedAndFoundInADataBreachAlertDescription,
|
|
alertActions: [
|
|
AlertAction(title: Localizations.no, style: .cancel),
|
|
AlertAction(title: Localizations.yes, style: .default) { _ in },
|
|
]
|
|
))
|
|
|
|
XCTAssertFalse(coordinator.isLoadingOverlayShowing)
|
|
XCTAssertEqual(
|
|
coordinator.loadingOverlaysShown,
|
|
[
|
|
LoadingOverlayState(title: Localizations.creatingAccount),
|
|
LoadingOverlayState(title: Localizations.creatingAccount),
|
|
]
|
|
)
|
|
}
|
|
|
|
/// `perform(_:)` with `.createAccount` presents an alert when the password
|
|
/// is strong and exposed. This also tests that the correct alert is presented.
|
|
/// Additionally, this tests that tapping Yes on the alert creates the account.
|
|
@MainActor
|
|
func test_perform_checkPasswordAndCreateAccount_exposedStrong_yesTapped() async throws {
|
|
subject.state = .fixture(isCheckDataBreachesToggleOn: true, passwordStrengthScore: 3)
|
|
|
|
client.results = [.httpSuccess(testData: .hibpLeakedPasswords), .httpSuccess(testData: .createAccountRequest)]
|
|
|
|
await subject.perform(.createAccount)
|
|
|
|
let alert = try XCTUnwrap(coordinator.alertShown.last)
|
|
try await alert.tapAction(title: Localizations.yes)
|
|
|
|
XCTAssertEqual(client.requests.count, 2)
|
|
XCTAssertEqual(client.requests[0].url, URL(string: "https://api.pwnedpasswords.com/range/e6b6a"))
|
|
XCTAssertEqual(client.requests[1].url, URL(string: "https://example.com/identity/accounts/register"))
|
|
XCTAssertEqual(coordinator.alertShown.last, Alert(
|
|
title: Localizations.exposedMasterPassword,
|
|
message: Localizations.passwordFoundInADataBreachAlertDescription,
|
|
alertActions: [
|
|
AlertAction(title: Localizations.no, style: .cancel),
|
|
AlertAction(title: Localizations.yes, style: .default) { _ in },
|
|
]
|
|
))
|
|
|
|
XCTAssertFalse(coordinator.isLoadingOverlayShowing)
|
|
XCTAssertEqual(
|
|
coordinator.loadingOverlaysShown,
|
|
[
|
|
LoadingOverlayState(title: Localizations.creatingAccount),
|
|
LoadingOverlayState(title: Localizations.creatingAccount),
|
|
]
|
|
)
|
|
}
|
|
|
|
/// `perform(_:)` with `.createAccount` presents an alert when the password
|
|
/// is weak and unchecked against breaches. This also tests that the correct alert is presented.
|
|
/// Additionally, this tests that tapping Yes on the alert creates the account.
|
|
@MainActor
|
|
func test_perform_checkPasswordAndCreateAccount_uncheckedWeak_yesTapped() async throws {
|
|
subject.state = .fixture(
|
|
isCheckDataBreachesToggleOn: false,
|
|
passwordText: "unexposed123",
|
|
passwordStrengthScore: 2,
|
|
retypePasswordText: "unexposed123"
|
|
)
|
|
|
|
client.results = [.httpSuccess(testData: .createAccountRequest)]
|
|
|
|
await subject.perform(.createAccount)
|
|
|
|
let alert = try XCTUnwrap(coordinator.alertShown.last)
|
|
try await alert.tapAction(title: Localizations.yes)
|
|
|
|
XCTAssertEqual(client.requests.count, 1)
|
|
XCTAssertEqual(client.requests[0].url, URL(string: "https://example.com/identity/accounts/register"))
|
|
XCTAssertEqual(coordinator.alertShown.last, Alert(
|
|
title: Localizations.weakMasterPassword,
|
|
message: Localizations.weakPasswordIdentifiedUseAStrongPasswordToProtectYourAccount,
|
|
alertActions: [
|
|
AlertAction(title: Localizations.no, style: .cancel),
|
|
AlertAction(title: Localizations.yes, style: .default) { _ in },
|
|
]
|
|
))
|
|
|
|
XCTAssertFalse(coordinator.isLoadingOverlayShowing)
|
|
XCTAssertEqual(coordinator.loadingOverlaysShown, [LoadingOverlayState(title: Localizations.creatingAccount)])
|
|
}
|
|
|
|
/// `perform(_:)` with `.createAccount` presents an alert when the password
|
|
/// is weak and unexposed. This also tests that the correct alert is presented.
|
|
/// Additionally, this tests that tapping Yes on the alert creates the account.
|
|
@MainActor
|
|
func test_perform_checkPasswordAndCreateAccount_unexposedWeak_yesTapped() async throws {
|
|
subject.state = .fixture(
|
|
isCheckDataBreachesToggleOn: true,
|
|
passwordText: "unexposed123",
|
|
passwordStrengthScore: 2,
|
|
retypePasswordText: "unexposed123"
|
|
)
|
|
|
|
client.results = [.httpSuccess(testData: .hibpLeakedPasswords), .httpSuccess(testData: .createAccountRequest)]
|
|
|
|
await subject.perform(.createAccount)
|
|
|
|
let alert = try XCTUnwrap(coordinator.alertShown.last)
|
|
try await alert.tapAction(title: Localizations.yes)
|
|
|
|
XCTAssertEqual(client.requests.count, 2)
|
|
XCTAssertEqual(client.requests[0].url, URL(string: "https://api.pwnedpasswords.com/range/6bf92"))
|
|
XCTAssertEqual(client.requests[1].url, URL(string: "https://example.com/identity/accounts/register"))
|
|
XCTAssertEqual(coordinator.alertShown.last, Alert(
|
|
title: Localizations.weakMasterPassword,
|
|
message: Localizations.weakPasswordIdentifiedUseAStrongPasswordToProtectYourAccount,
|
|
alertActions: [
|
|
AlertAction(title: Localizations.no, style: .cancel),
|
|
AlertAction(title: Localizations.yes, style: .default) { _ in },
|
|
]
|
|
))
|
|
|
|
XCTAssertFalse(coordinator.isLoadingOverlayShowing)
|
|
XCTAssertEqual(
|
|
coordinator.loadingOverlaysShown,
|
|
[
|
|
LoadingOverlayState(title: Localizations.creatingAccount),
|
|
LoadingOverlayState(title: Localizations.creatingAccount),
|
|
]
|
|
)
|
|
}
|
|
|
|
/// `perform(_:)` with `.createAccount` presents an alert when the email has already been taken.
|
|
@MainActor
|
|
func test_perform_createAccount_accountAlreadyExists() async {
|
|
subject.state = .fixture()
|
|
|
|
let response = HTTPResponse.failure(
|
|
statusCode: 400,
|
|
body: APITestData.createAccountAccountAlreadyExists.data
|
|
)
|
|
|
|
guard let errorResponse = try? ErrorResponseModel(response: response) else { return }
|
|
|
|
client.result = .httpFailure(
|
|
ServerError.error(errorResponse: errorResponse)
|
|
)
|
|
|
|
await subject.perform(.createAccount)
|
|
|
|
XCTAssertEqual(client.requests.count, 1)
|
|
XCTAssertEqual(
|
|
coordinator.alertShown.last,
|
|
.defaultAlert(
|
|
title: Localizations.anErrorHasOccurred,
|
|
message: "Email 'j@a.com' is already taken."
|
|
)
|
|
)
|
|
|
|
XCTAssertFalse(coordinator.isLoadingOverlayShowing)
|
|
XCTAssertEqual(coordinator.loadingOverlaysShown, [LoadingOverlayState(title: Localizations.creatingAccount)])
|
|
}
|
|
|
|
/// `perform(_:)` with `.createAccount` presents an alert when the email exceeds the maximum length.
|
|
@MainActor
|
|
func test_perform_createAccount_emailExceedsMaxLength() async {
|
|
subject.state = .fixture(emailText: """
|
|
eyrztwlvxqdksnmcbjgahfpouyqiwubfdzoxhjsrlnvgeatkcpimy\
|
|
fqaxhztsowbmdkjlrpnuqvycigfexrvlosqtpnheujawzsdmkbfoy\
|
|
cxqpwkzthbnmudxlysgarcejfqvopzrkihwdelbuxyfqnjsgptamcozrvihsl\
|
|
nbujrtdosmvhxwyfapzcklqoxbgdvtfieqyuhwajnrpslmcskgzofdqehxcbv\
|
|
omjltzafwudqypnisgrkeohycbvxjflaumtwzrdqnpsoiezgyhqbmxdlvnzwa\
|
|
htjoekrcispgvyfbuqklszepjwdrantihxfcoygmuslqbajzdfgrkmwbpnouq\
|
|
tlsvixechyfjslrdvngiwzqpcotxubamhyekufjrzdwmxihqkfonslbcjgtpu\
|
|
voyaezrctudwlskjpvmfqhnxbriyg@example.com
|
|
""")
|
|
|
|
let response = HTTPResponse.failure(
|
|
statusCode: 400,
|
|
body: APITestData.createAccountEmailExceedsMaxLength.data
|
|
)
|
|
|
|
guard let errorResponse = try? ErrorResponseModel(response: response) else { return }
|
|
|
|
client.result = .httpFailure(
|
|
ServerError.error(errorResponse: errorResponse)
|
|
)
|
|
|
|
await subject.perform(.createAccount)
|
|
|
|
XCTAssertEqual(client.requests.count, 1)
|
|
XCTAssertEqual(
|
|
coordinator.alertShown.last,
|
|
.defaultAlert(
|
|
title: Localizations.anErrorHasOccurred,
|
|
message: "The field Email must be a string with a maximum length of 256."
|
|
)
|
|
)
|
|
|
|
XCTAssertFalse(coordinator.isLoadingOverlayShowing)
|
|
XCTAssertEqual(coordinator.loadingOverlaysShown, [LoadingOverlayState(title: Localizations.creatingAccount)])
|
|
}
|
|
|
|
/// `perform(_:)` with `.createAccount` presents an alert when the email field is empty.
|
|
@MainActor
|
|
func test_perform_createAccount_emptyEmail() async {
|
|
subject.state = .fixture(emailText: "")
|
|
|
|
client.result = .httpSuccess(testData: .createAccountSuccess)
|
|
|
|
await subject.perform(.createAccount)
|
|
|
|
XCTAssertEqual(client.requests.count, 0)
|
|
XCTAssertEqual(coordinator.alertShown.last, .validationFieldRequired(fieldName: "Email"))
|
|
XCTAssertTrue(coordinator.loadingOverlaysShown.isEmpty)
|
|
}
|
|
|
|
/// `perform(_:)` with `.createAccount` presents an alert when the password field is empty.
|
|
@MainActor
|
|
func test_perform_createAccount_emptyPassword() async {
|
|
subject.state = .fixture(passwordText: "", retypePasswordText: "")
|
|
|
|
client.result = .httpSuccess(testData: .createAccountSuccess)
|
|
|
|
await subject.perform(.createAccount)
|
|
|
|
XCTAssertEqual(client.requests.count, 0)
|
|
XCTAssertEqual(coordinator.alertShown.last, .validationFieldRequired(fieldName: "Master password"))
|
|
XCTAssertTrue(coordinator.loadingOverlaysShown.isEmpty)
|
|
}
|
|
|
|
/// `perform(_:)` with `.createAccount` and a captcha required error occurs navigates to the `.captcha` route.
|
|
@MainActor
|
|
func test_perform_createAccount_captchaError() async {
|
|
subject.state = .fixture()
|
|
|
|
client.result = .httpFailure(CreateAccountRequestError.captchaRequired(hCaptchaSiteCode: "token"))
|
|
|
|
await subject.perform(.createAccount)
|
|
|
|
XCTAssertEqual(client.requests.count, 1)
|
|
XCTAssertEqual(captchaService.callbackUrlSchemeGets, 1)
|
|
XCTAssertEqual(captchaService.generateCaptchaSiteKey, "token")
|
|
XCTAssertEqual(coordinator.routes.last, .captcha(url: .example, callbackUrlScheme: "callback"))
|
|
|
|
XCTAssertFalse(coordinator.isLoadingOverlayShowing)
|
|
XCTAssertEqual(coordinator.loadingOverlaysShown, [LoadingOverlayState(title: Localizations.creatingAccount)])
|
|
}
|
|
|
|
/// `perform(_:)` with `.createAccount` and a captcha flow error records the error.
|
|
@MainActor
|
|
func test_perform_createAccount_captchaFlowError() async {
|
|
captchaService.generateCaptchaUrlResult = .failure(BitwardenTestError.example)
|
|
client.result = .httpFailure(CreateAccountRequestError.captchaRequired(hCaptchaSiteCode: "token"))
|
|
|
|
subject.state.emailText = "email@example.com"
|
|
subject.state.passwordText = "password1234"
|
|
subject.state.retypePasswordText = "password1234"
|
|
subject.state.isTermsAndPrivacyToggleOn = true
|
|
subject.state.isCheckDataBreachesToggleOn = false
|
|
|
|
await subject.perform(.createAccount)
|
|
|
|
XCTAssertEqual(coordinator.alertShown.last, .networkResponseError(BitwardenTestError.example))
|
|
XCTAssertEqual(errorReporter.errors.last as? BitwardenTestError, .example)
|
|
|
|
XCTAssertFalse(coordinator.isLoadingOverlayShowing)
|
|
XCTAssertEqual(coordinator.loadingOverlaysShown, [LoadingOverlayState(title: Localizations.creatingAccount)])
|
|
}
|
|
|
|
/// `perform(_:)` with `.createAccount` presents an alert when the password hint is too long.
|
|
@MainActor
|
|
func test_perform_createAccount_hintTooLong() async {
|
|
subject.state = .fixture(passwordHintText: """
|
|
ajajajajajajajajajajajajajajajajajajajajajajajajajajajajajajajajajajaj
|
|
ajajajajajajajajajajajajajajajajajajajajajajajajajajajajajsjajajajajaj
|
|
""")
|
|
|
|
let response = HTTPResponse.failure(
|
|
statusCode: 400,
|
|
body: APITestData.createAccountHintTooLong.data
|
|
)
|
|
|
|
guard let errorResponse = try? ErrorResponseModel(response: response) else { return }
|
|
|
|
client.result = .httpFailure(
|
|
ServerError.error(errorResponse: errorResponse)
|
|
)
|
|
|
|
await subject.perform(.createAccount)
|
|
|
|
XCTAssertEqual(client.requests.count, 1)
|
|
XCTAssertEqual(
|
|
coordinator.alertShown.last,
|
|
.defaultAlert(
|
|
title: Localizations.anErrorHasOccurred,
|
|
message: "The field MasterPasswordHint must be a string with a maximum length of 50."
|
|
)
|
|
)
|
|
|
|
XCTAssertFalse(coordinator.isLoadingOverlayShowing)
|
|
XCTAssertEqual(coordinator.loadingOverlaysShown, [LoadingOverlayState(title: Localizations.creatingAccount)])
|
|
}
|
|
|
|
/// `perform(_:)` with `.createAccount` presents an alert when the email is in an invalid format.
|
|
@MainActor
|
|
func test_perform_createAccount_invalidEmailFormat() async {
|
|
subject.state = .fixture(emailText: "∫@ø.com")
|
|
|
|
let response = HTTPResponse.failure(
|
|
statusCode: 400,
|
|
body: APITestData.createAccountInvalidEmailFormat.data
|
|
)
|
|
|
|
guard let errorResponse = try? ErrorResponseModel(response: response) else { return }
|
|
|
|
client.result = .httpFailure(
|
|
ServerError.error(errorResponse: errorResponse)
|
|
)
|
|
|
|
await subject.perform(.createAccount)
|
|
|
|
XCTAssertEqual(client.requests.count, 1)
|
|
XCTAssertEqual(
|
|
coordinator.alertShown.last,
|
|
.defaultAlert(
|
|
title: Localizations.anErrorHasOccurred,
|
|
message: "The Email field is not a supported e-mail address format."
|
|
)
|
|
)
|
|
|
|
XCTAssertFalse(coordinator.isLoadingOverlayShowing)
|
|
XCTAssertEqual(coordinator.loadingOverlaysShown, [LoadingOverlayState(title: Localizations.creatingAccount)])
|
|
}
|
|
|
|
/// `perform(_:)` with `.createAccount` presents an alert when there is no internet connection.
|
|
/// When the user taps `Try again`, the create account request is made again.
|
|
@MainActor
|
|
func test_perform_createAccount_noInternetConnection() async throws {
|
|
subject.state = .fixture()
|
|
|
|
let urlError = URLError(.notConnectedToInternet) as Error
|
|
client.results = [.httpFailure(urlError), .httpSuccess(testData: .createAccountRequest)]
|
|
|
|
await subject.perform(.createAccount)
|
|
|
|
let alert = try XCTUnwrap(coordinator.alertShown.last)
|
|
XCTAssertEqual(alert, Alert.networkResponseError(urlError) {
|
|
await self.subject.perform(.createAccount)
|
|
})
|
|
|
|
try await alert.tapAction(title: Localizations.tryAgain)
|
|
|
|
XCTAssertEqual(client.requests.count, 2)
|
|
XCTAssertEqual(client.requests[0].url, URL(string: "https://example.com/identity/accounts/register"))
|
|
XCTAssertEqual(client.requests[1].url, URL(string: "https://example.com/identity/accounts/register"))
|
|
XCTAssertEqual(coordinator.routes.last, .login(username: "email@example.com"))
|
|
|
|
XCTAssertFalse(coordinator.isLoadingOverlayShowing)
|
|
XCTAssertEqual(
|
|
coordinator.loadingOverlaysShown,
|
|
[
|
|
LoadingOverlayState(title: Localizations.creatingAccount),
|
|
LoadingOverlayState(title: Localizations.creatingAccount),
|
|
]
|
|
)
|
|
}
|
|
|
|
/// `perform(_:)` with `.createAccount` presents an alert when password confirmation is incorrect.
|
|
@MainActor
|
|
func test_perform_createAccount_passwordsDontMatch() async {
|
|
subject.state = .fixture(passwordText: "123456789012", retypePasswordText: "123456789000")
|
|
|
|
client.result = .httpSuccess(testData: .createAccountSuccess)
|
|
|
|
await subject.perform(.createAccount)
|
|
|
|
XCTAssertEqual(client.requests.count, 0)
|
|
XCTAssertEqual(coordinator.alertShown.last, .passwordsDontMatch)
|
|
XCTAssertTrue(coordinator.loadingOverlaysShown.isEmpty)
|
|
}
|
|
|
|
/// `perform(_:)` with `.createAccount` presents an alert when the password isn't long enough.
|
|
@MainActor
|
|
func test_perform_createAccount_passwordsTooShort() async {
|
|
subject.state = .fixture(passwordText: "123", retypePasswordText: "123")
|
|
|
|
client.result = .httpSuccess(testData: .createAccountSuccess)
|
|
|
|
await subject.perform(.createAccount)
|
|
|
|
XCTAssertEqual(client.requests.count, 0)
|
|
XCTAssertEqual(coordinator.alertShown.last, .passwordIsTooShort)
|
|
XCTAssertTrue(coordinator.loadingOverlaysShown.isEmpty)
|
|
}
|
|
|
|
/// `perform(_:)` with `.createAccount` presents an alert when the request times out.
|
|
/// When the user taps `Try again`, the create account request is made again.
|
|
@MainActor
|
|
func test_perform_createAccount_timeout() async throws {
|
|
subject.state = .fixture()
|
|
|
|
let urlError = URLError(.timedOut) as Error
|
|
client.results = [.httpFailure(urlError), .httpSuccess(testData: .createAccountRequest)]
|
|
|
|
await subject.perform(.createAccount)
|
|
|
|
let alert = try XCTUnwrap(coordinator.alertShown.last)
|
|
XCTAssertEqual(alert.message, urlError.localizedDescription)
|
|
|
|
try await alert.tapAction(title: Localizations.tryAgain)
|
|
|
|
XCTAssertEqual(client.requests.count, 2)
|
|
XCTAssertEqual(client.requests[0].url, URL(string: "https://example.com/identity/accounts/register"))
|
|
XCTAssertEqual(client.requests[1].url, URL(string: "https://example.com/identity/accounts/register"))
|
|
XCTAssertEqual(coordinator.routes.last, .login(username: "email@example.com"))
|
|
|
|
XCTAssertFalse(coordinator.isLoadingOverlayShowing)
|
|
XCTAssertEqual(
|
|
coordinator.loadingOverlaysShown,
|
|
[
|
|
LoadingOverlayState(title: Localizations.creatingAccount),
|
|
LoadingOverlayState(title: Localizations.creatingAccount),
|
|
]
|
|
)
|
|
}
|
|
|
|
/// `perform(_:)` with `.createAccount` and an invalid email navigates to an invalid email alert.
|
|
@MainActor
|
|
func test_perform_createAccount_withInvalidEmail() async {
|
|
subject.state = .fixture(emailText: "exampleemail.com")
|
|
|
|
client.result = .httpFailure(CreateAccountError.invalidEmail)
|
|
|
|
await subject.perform(.createAccount)
|
|
|
|
XCTAssertEqual(client.requests.count, 0)
|
|
XCTAssertEqual(coordinator.alertShown.last, .invalidEmail)
|
|
XCTAssertTrue(coordinator.loadingOverlaysShown.isEmpty)
|
|
}
|
|
|
|
/// `perform(_:)` with `.createAccount` and a valid email creates the user's account.
|
|
@MainActor
|
|
func test_perform_createAccount_withValidEmail() async {
|
|
subject.state = .fixture()
|
|
|
|
client.result = .httpSuccess(testData: .createAccountSuccess)
|
|
|
|
await subject.perform(.createAccount)
|
|
|
|
XCTAssertEqual(client.requests.count, 1)
|
|
XCTAssertEqual(client.requests[0].url, URL(string: "https://example.com/identity/accounts/register"))
|
|
|
|
XCTAssertFalse(coordinator.isLoadingOverlayShowing)
|
|
XCTAssertEqual(coordinator.loadingOverlaysShown, [LoadingOverlayState(title: Localizations.creatingAccount)])
|
|
}
|
|
|
|
/// `perform(_:)` with `.createAccount` and a valid email surrounded by whitespace trims the whitespace and
|
|
/// creates the user's account
|
|
@MainActor
|
|
func test_perform_createAccount_withValidEmailAndSpace() async {
|
|
subject.state = .fixture(emailText: " email@example.com ")
|
|
|
|
client.result = .httpSuccess(testData: .createAccountSuccess)
|
|
|
|
await subject.perform(.createAccount)
|
|
|
|
XCTAssertEqual(client.requests.count, 1)
|
|
XCTAssertEqual(client.requests[0].url, URL(string: "https://example.com/identity/accounts/register"))
|
|
|
|
XCTAssertFalse(coordinator.isLoadingOverlayShowing)
|
|
XCTAssertEqual(coordinator.loadingOverlaysShown, [LoadingOverlayState(title: Localizations.creatingAccount)])
|
|
}
|
|
|
|
/// `perform(_:)` with `.createAccount` and a valid email with uppercase characters converts the email to lowercase
|
|
/// and creates the user's account.
|
|
@MainActor
|
|
func test_perform_createAccount_withValidEmailUppercased() async {
|
|
subject.state = .fixture(emailText: "EMAIL@EXAMPLE.COM")
|
|
|
|
client.result = .httpSuccess(testData: .createAccountSuccess)
|
|
|
|
await subject.perform(.createAccount)
|
|
|
|
XCTAssertEqual(client.requests.count, 1)
|
|
XCTAssertEqual(client.requests[0].url, URL(string: "https://example.com/identity/accounts/register"))
|
|
|
|
XCTAssertFalse(coordinator.isLoadingOverlayShowing)
|
|
XCTAssertEqual(coordinator.loadingOverlaysShown, [LoadingOverlayState(title: Localizations.creatingAccount)])
|
|
}
|
|
|
|
/// `perform(_:)` with `.createAccount` navigates to an error alert when the terms of service
|
|
/// and privacy policy toggle is off.
|
|
@MainActor
|
|
func test_perform_createAccount_withTermsAndServicesToggle_false() async {
|
|
subject.state = .fixture(isTermsAndPrivacyToggleOn: false)
|
|
|
|
client.result = .httpSuccess(testData: .createAccountSuccess)
|
|
|
|
await subject.perform(.createAccount)
|
|
|
|
XCTAssertEqual(client.requests.count, 0)
|
|
XCTAssertEqual(coordinator.alertShown.last, .acceptPoliciesAlert())
|
|
XCTAssertTrue(coordinator.loadingOverlaysShown.isEmpty)
|
|
}
|
|
|
|
/// `receive(_:)` with `.dismiss` dismisses the view.
|
|
@MainActor
|
|
func test_receive_dismiss() {
|
|
subject.receive(.dismiss)
|
|
XCTAssertEqual(coordinator.routes.last, .dismiss)
|
|
}
|
|
|
|
/// `receive(_:)` with `.emailTextChanged(_:)` updates the state to reflect the change.
|
|
@MainActor
|
|
func test_receive_emailTextChanged() {
|
|
subject.state.emailText = ""
|
|
XCTAssertTrue(subject.state.emailText.isEmpty)
|
|
|
|
subject.receive(.emailTextChanged("updated email"))
|
|
XCTAssertTrue(subject.state.emailText == "updated email")
|
|
}
|
|
|
|
/// `receive(_:)` with `.passwordHintTextChanged(_:)` updates the state to reflect the change.
|
|
@MainActor
|
|
func test_receive_passwordHintTextChanged() {
|
|
subject.state.passwordHintText = ""
|
|
XCTAssertTrue(subject.state.passwordHintText.isEmpty)
|
|
|
|
subject.receive(.passwordHintTextChanged("updated hint"))
|
|
XCTAssertTrue(subject.state.passwordHintText == "updated hint")
|
|
}
|
|
|
|
/// `receive(_:)` with `.passwordTextChanged(_:)` updates the state to reflect the change.
|
|
@MainActor
|
|
func test_receive_passwordTextChanged() {
|
|
subject.state.passwordText = ""
|
|
XCTAssertTrue(subject.state.passwordText.isEmpty)
|
|
|
|
subject.receive(.passwordTextChanged("updated password"))
|
|
XCTAssertTrue(subject.state.passwordText == "updated password")
|
|
}
|
|
|
|
/// `receive(_:)` with `.passwordTextChanged(_:)` updates the password strength score based on
|
|
/// the entered password.
|
|
@MainActor
|
|
func test_receive_passwordTextChanged_updatesPasswordStrength() {
|
|
subject.state.emailText = "user@bitwarden.com"
|
|
subject.receive(.passwordTextChanged(""))
|
|
XCTAssertNil(subject.state.passwordStrengthScore)
|
|
XCTAssertNil(authRepository.passwordStrengthPassword)
|
|
|
|
authRepository.passwordStrengthResult = .success(0)
|
|
subject.receive(.passwordTextChanged("T"))
|
|
waitFor(subject.state.passwordStrengthScore == 0)
|
|
XCTAssertEqual(authRepository.passwordStrengthEmail, "user@bitwarden.com")
|
|
XCTAssertTrue(authRepository.passwordStrengthIsPreAuth)
|
|
XCTAssertEqual(authRepository.passwordStrengthPassword, "T")
|
|
|
|
authRepository.passwordStrengthResult = .success(4)
|
|
subject.receive(.passwordTextChanged("TestPassword1234567890!@#"))
|
|
waitFor(subject.state.passwordStrengthScore == 4)
|
|
XCTAssertEqual(authRepository.passwordStrengthEmail, "user@bitwarden.com")
|
|
XCTAssertTrue(authRepository.passwordStrengthIsPreAuth)
|
|
XCTAssertEqual(authRepository.passwordStrengthPassword, "TestPassword1234567890!@#")
|
|
}
|
|
|
|
/// `receive(_:)` with `.passwordTextChanged(_:)` records an error if the `.passwordStrength()` throws.
|
|
@MainActor
|
|
func test_receive_passwordTextChanged_updatePasswordStrength_fails() {
|
|
authRepository.passwordStrengthResult = .failure(BitwardenTestError.example)
|
|
subject.receive(.passwordTextChanged("T"))
|
|
waitFor(!errorReporter.errors.isEmpty)
|
|
XCTAssertEqual(errorReporter.errors.last as? BitwardenTestError, BitwardenTestError.example)
|
|
}
|
|
|
|
/// `receive(_:)` with `.retypePasswordTextChanged(_:)` updates the state to reflect the change.
|
|
@MainActor
|
|
func test_receive_retypePasswordTextChanged() {
|
|
subject.state.retypePasswordText = ""
|
|
XCTAssertTrue(subject.state.retypePasswordText.isEmpty)
|
|
|
|
subject.receive(.retypePasswordTextChanged("updated re-type"))
|
|
XCTAssertTrue(subject.state.retypePasswordText == "updated re-type")
|
|
}
|
|
|
|
/// `receive(_:)` with `.toggleCheckDataBreaches(_:)` updates the state to reflect the change.
|
|
@MainActor
|
|
func test_receive_toggleCheckDataBreaches() {
|
|
subject.receive(.toggleCheckDataBreaches(false))
|
|
XCTAssertFalse(subject.state.isCheckDataBreachesToggleOn)
|
|
|
|
subject.receive(.toggleCheckDataBreaches(true))
|
|
XCTAssertTrue(subject.state.isCheckDataBreachesToggleOn)
|
|
|
|
subject.receive(.toggleCheckDataBreaches(true))
|
|
XCTAssertTrue(subject.state.isCheckDataBreachesToggleOn)
|
|
}
|
|
|
|
/// `receive(_:)` with `.togglePasswordVisibility(_:)` updates the state to reflect the change.
|
|
@MainActor
|
|
func test_receive_togglePasswordVisibility() {
|
|
subject.state.arePasswordsVisible = false
|
|
|
|
subject.receive(.togglePasswordVisibility(true))
|
|
XCTAssertTrue(subject.state.arePasswordsVisible)
|
|
|
|
subject.receive(.togglePasswordVisibility(true))
|
|
XCTAssertTrue(subject.state.arePasswordsVisible)
|
|
|
|
subject.receive(.togglePasswordVisibility(false))
|
|
XCTAssertFalse(subject.state.arePasswordsVisible)
|
|
}
|
|
|
|
/// `receive(_:)` with `.toggleTermsAndPrivacy(_:)` updates the state to reflect the change.
|
|
@MainActor
|
|
func test_receive_toggleTermsAndPrivacy() {
|
|
subject.receive(.toggleTermsAndPrivacy(false))
|
|
XCTAssertFalse(subject.state.isTermsAndPrivacyToggleOn)
|
|
|
|
subject.receive(.toggleTermsAndPrivacy(true))
|
|
XCTAssertTrue(subject.state.isTermsAndPrivacyToggleOn)
|
|
|
|
subject.receive(.toggleTermsAndPrivacy(true))
|
|
XCTAssertTrue(subject.state.isTermsAndPrivacyToggleOn)
|
|
}
|
|
// swiftlint:disable:next file_length
|
|
}
|