Selecting a server that doesn't support login now fails instead of letting you continue to a failure later. (#3342)

* Fail configuration of the authentication service if the homeserver doesn't support login.

* Move the ServerSelectionCoordinator logic into the ViewModel.

- Handle the new login alert.
- Add more tests
This commit is contained in:
Doug
2024-09-27 13:58:05 +01:00
committed by GitHub
parent 2c10dfdb7f
commit aec4e3e8ca
17 changed files with 263 additions and 139 deletions

View File

@@ -829,7 +829,6 @@
B6DA66EFC13A90846B625836 /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = 91DE43B8815918E590912DDA /* InfoPlist.strings */; };
B6DF6B6FA8734B70F9BF261E /* BlurHashDecode.swift in Sources */ = {isa = PBXBuildFile; fileRef = E5272BC4A60B6AD7553BACA1 /* BlurHashDecode.swift */; };
B6EC2148FA5443C9289BEEBA /* MediaProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = F17EFA1D3D09FC2F9C5E1CB2 /* MediaProvider.swift */; };
B721125D17A0BA86794F29FB /* MockServerSelectionScreenState.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8E057FB1F07A5C201C89061 /* MockServerSelectionScreenState.swift */; };
B773ACD8881DB18E876D950C /* WaveformSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94028A227645FA880B966211 /* WaveformSource.swift */; };
B7888FC1E1DEF816D175C8D6 /* SecureBackupKeyBackupScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD72A9B720D75DBE60AC299F /* SecureBackupKeyBackupScreenModels.swift */; };
B796A25F282C0A340D1B9C12 /* ImageRoomTimelineItemContent.swift in Sources */ = {isa = PBXBuildFile; fileRef = B2B5EDCD05D50BA9B815C66C /* ImageRoomTimelineItemContent.swift */; };
@@ -2141,7 +2140,6 @@
D79BB714D28C9F588DD69353 /* SecureBackupScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecureBackupScreenViewModelProtocol.swift; sourceTree = "<group>"; };
D7BB243B26D54EF1A0C422C0 /* NotificationContentBuilder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationContentBuilder.swift; sourceTree = "<group>"; };
D7BEB970F500BFB248443FA1 /* BloomView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BloomView.swift; sourceTree = "<group>"; };
D8E057FB1F07A5C201C89061 /* MockServerSelectionScreenState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockServerSelectionScreenState.swift; sourceTree = "<group>"; };
D8E60332509665C00179ACF6 /* MessageForwardingScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageForwardingScreenViewModel.swift; sourceTree = "<group>"; };
D8F5F9E02B1AB5350B1815E7 /* TimelineStartRoomTimelineItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineStartRoomTimelineItem.swift; sourceTree = "<group>"; };
D8FC33C3F6BF597E095CE9FA /* HomeScreenInviteCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeScreenInviteCell.swift; sourceTree = "<group>"; };
@@ -2815,7 +2813,6 @@
2D0D49B0533C4C2EB889BF3A /* ServerSelectionScreen */ = {
isa = PBXGroup;
children = (
D8E057FB1F07A5C201C89061 /* MockServerSelectionScreenState.swift */,
BB8BC4C791D0E88CFCF4E5DF /* ServerSelectionScreenCoordinator.swift */,
9501D11B4258DFA33BA3B40F /* ServerSelectionScreenModels.swift */,
E3059CFA00C67D8787273B20 /* ServerSelectionScreenViewModel.swift */,
@@ -6593,7 +6590,6 @@
C97325EFDCCEE457432A9E82 /* MessageText.swift in Sources */,
B659E3A49889E749E3239EA7 /* MockMediaProvider.swift in Sources */,
09C83DDDB07C28364F325209 /* MockRoomTimelineController.swift in Sources */,
B721125D17A0BA86794F29FB /* MockServerSelectionScreenState.swift in Sources */,
AF2ABA2794E376B64104C964 /* MockSoftLogoutScreenState.swift in Sources */,
F9842667B68DC6FA1F9ECCBB /* NSItemProvider.swift in Sources */,
EA01A06EEDFEF4AE7652E5F3 /* NSRegularExpresion.swift in Sources */,

View File

@@ -72,6 +72,8 @@ enum ServerConfirmationScreenAlert: Hashable {
case invalidWellKnown(String)
/// An alert that allows the user to learn about sliding sync.
case slidingSync
/// An alert that informs the user that login isn't supported.
case login
/// An alert that informs the user that registration isn't supported.
case registration
/// An unknown error has occurred.

View File

@@ -82,6 +82,8 @@ class ServerConfirmationScreenViewModel: ServerConfirmationScreenViewModelType,
displayError(.invalidWellKnown(error))
case .slidingSyncNotAvailable:
displayError(.slidingSync)
case .loginNotSupported:
displayError(.login)
case .registrationNotSupported:
displayError(.registration)
default:
@@ -117,9 +119,13 @@ class ServerConfirmationScreenViewModel: ServerConfirmationScreenViewModelType,
message: L10n.screenChangeServerErrorNoSlidingSyncMessage,
primaryButton: .init(title: L10n.actionLearnMore, role: .cancel, action: openURL),
secondaryButton: .init(title: L10n.actionCancel, action: nil))
case .login:
state.bindings.alertInfo = AlertInfo(id: .login,
title: L10n.commonServerNotSupported,
message: L10n.screenLoginErrorUnsupportedAuthentication)
case .registration:
state.bindings.alertInfo = AlertInfo(id: .registration,
title: L10n.errorUnknown,
title: L10n.commonServerNotSupported,
message: L10n.errorAccountCreationNotPossible)
case .unknownError:
state.bindings.alertInfo = AlertInfo(id: .unknownError)

View File

@@ -1,32 +0,0 @@
//
// Copyright 2022-2024 New Vector Ltd.
//
// SPDX-License-Identifier: AGPL-3.0-only
// Please see LICENSE in the repository root for full details.
//
import SwiftUI
enum MockServerSelectionScreenState: CaseIterable {
case matrix
case emptyAddress
case invalidAddress
/// Generate the view struct for the screen state.
@MainActor var viewModel: ServerSelectionScreenViewModel {
switch self {
case .matrix:
return ServerSelectionScreenViewModel(homeserverAddress: "https://matrix.org",
slidingSyncLearnMoreURL: ServiceLocator.shared.settings.slidingSyncLearnMoreURL)
case .emptyAddress:
return ServerSelectionScreenViewModel(homeserverAddress: "",
slidingSyncLearnMoreURL: ServiceLocator.shared.settings.slidingSyncLearnMoreURL)
case .invalidAddress:
let viewModel = ServerSelectionScreenViewModel(homeserverAddress: "thisisbad",
slidingSyncLearnMoreURL: ServiceLocator.shared.settings.slidingSyncLearnMoreURL)
viewModel.displayError(.footerMessage(L10n.errorUnknown))
return viewModel
}
}
}

View File

@@ -37,8 +37,10 @@ final class ServerSelectionScreenCoordinator: CoordinatorProtocol {
init(parameters: ServerSelectionScreenCoordinatorParameters) {
self.parameters = parameters
viewModel = ServerSelectionScreenViewModel(homeserverAddress: parameters.authenticationService.homeserver.value.address,
slidingSyncLearnMoreURL: parameters.slidingSyncLearnMoreURL)
viewModel = ServerSelectionScreenViewModel(authenticationService: parameters.authenticationService,
authenticationFlow: parameters.authenticationFlow,
slidingSyncLearnMoreURL: parameters.slidingSyncLearnMoreURL,
userIndicatorController: parameters.userIndicatorController)
userIndicatorController = parameters.userIndicatorController
}
@@ -50,8 +52,8 @@ final class ServerSelectionScreenCoordinator: CoordinatorProtocol {
guard let self else { return }
switch action {
case .confirm(let homeserverAddress):
self.useHomeserver(homeserverAddress)
case .updated:
actionsSubject.send(.updated)
case .dismiss:
actionsSubject.send(.dismiss)
}
@@ -60,56 +62,10 @@ final class ServerSelectionScreenCoordinator: CoordinatorProtocol {
}
func stop() {
stopLoading()
parameters.userIndicatorController.retractAllIndicators()
}
func toPresentable() -> AnyView {
AnyView(ServerSelectionScreen(context: viewModel.context))
}
// MARK: - Private
private func startLoading(label: String = L10n.commonLoading) {
userIndicatorController.submitIndicator(UserIndicator(type: .modal,
title: label,
persistent: true))
}
private func stopLoading() {
userIndicatorController.retractAllIndicators()
}
/// Updates the login flow using the supplied homeserver address, or shows an error when this isn't possible.
private func useHomeserver(_ homeserverAddress: String) {
startLoading()
Task {
switch await authenticationService.configure(for: homeserverAddress, flow: parameters.authenticationFlow) {
case .success:
MXLog.info("Selected homeserver: \(homeserverAddress)")
actionsSubject.send(.updated)
stopLoading()
case .failure(let error):
MXLog.info("Invalid homeserver: \(homeserverAddress)")
stopLoading()
handleError(error)
}
}
}
/// Processes an error to either update the flow or display it to the user.
private func handleError(_ error: AuthenticationServiceError) {
switch error {
case .invalidServer, .invalidHomeserverAddress:
viewModel.displayError(.footerMessage(L10n.screenChangeServerErrorInvalidHomeserver))
case .invalidWellKnown(let error):
viewModel.displayError(.invalidWellKnownAlert(error))
case .slidingSyncNotAvailable:
viewModel.displayError(.slidingSyncAlert)
case .registrationNotSupported:
viewModel.displayError(.registrationAlert) // TODO: [DOUG] Test me!
default:
viewModel.displayError(.footerMessage(L10n.errorUnknown))
}
}
}

View File

@@ -8,8 +8,8 @@
import Foundation
enum ServerSelectionScreenViewModelAction {
/// The user would like to use the homeserver at the given address.
case confirm(homeserverAddress: String)
/// The homeserver selection has been updated.
case updated
/// Dismiss the view without using the entered address.
case dismiss
}
@@ -74,6 +74,8 @@ enum ServerSelectionScreenErrorType: Hashable {
case invalidWellKnownAlert(String)
/// An alert that allows the user to learn about sliding sync.
case slidingSyncAlert
/// An alert that informs the user that login isn't supported.
case loginAlert
/// An alert that informs the user that registration isn't supported.
case registrationAlert
}

View File

@@ -11,7 +11,10 @@ import SwiftUI
typealias ServerSelectionScreenViewModelType = StateStoreViewModel<ServerSelectionScreenViewState, ServerSelectionScreenViewAction>
class ServerSelectionScreenViewModel: ServerSelectionScreenViewModelType, ServerSelectionScreenViewModelProtocol {
private let authenticationService: AuthenticationServiceProtocol
private let authenticationFlow: AuthenticationFlow
private let slidingSyncLearnMoreURL: URL
private let userIndicatorController: UserIndicatorControllerProtocol
private var actionsSubject: PassthroughSubject<ServerSelectionScreenViewModelAction, Never> = .init()
@@ -19,18 +22,23 @@ class ServerSelectionScreenViewModel: ServerSelectionScreenViewModelType, Server
actionsSubject.eraseToAnyPublisher()
}
init(homeserverAddress: String, slidingSyncLearnMoreURL: URL) {
init(authenticationService: AuthenticationServiceProtocol,
authenticationFlow: AuthenticationFlow,
slidingSyncLearnMoreURL: URL,
userIndicatorController: UserIndicatorControllerProtocol) {
self.authenticationService = authenticationService
self.authenticationFlow = authenticationFlow
self.slidingSyncLearnMoreURL = slidingSyncLearnMoreURL
let bindings = ServerSelectionScreenBindings(homeserverAddress: homeserverAddress)
self.userIndicatorController = userIndicatorController
super.init(initialViewState: ServerSelectionScreenViewState(slidingSyncLearnMoreURL: slidingSyncLearnMoreURL,
bindings: bindings))
let bindings = ServerSelectionScreenBindings(homeserverAddress: authenticationService.homeserver.value.address)
super.init(initialViewState: ServerSelectionScreenViewState(slidingSyncLearnMoreURL: slidingSyncLearnMoreURL, bindings: bindings))
}
override func process(viewAction: ServerSelectionScreenViewAction) {
switch viewAction {
case .confirm:
actionsSubject.send(.confirm(homeserverAddress: state.bindings.homeserverAddress))
configureHomeserver()
case .dismiss:
actionsSubject.send(.dismiss)
case .clearFooterError:
@@ -38,31 +46,72 @@ class ServerSelectionScreenViewModel: ServerSelectionScreenViewModelType, Server
}
}
func displayError(_ type: ServerSelectionScreenErrorType) {
switch type {
case .footerMessage(let message):
withElementAnimation {
state.footerErrorMessage = message
// MARK: - Private
/// Updates the login flow using the supplied homeserver address, or shows an error when this isn't possible.
private func configureHomeserver() {
let homeserverAddress = state.bindings.homeserverAddress
startLoading()
Task {
switch await authenticationService.configure(for: homeserverAddress, flow: authenticationFlow) {
case .success:
MXLog.info("Selected homeserver: \(homeserverAddress)")
actionsSubject.send(.updated)
stopLoading()
case .failure(let error):
MXLog.info("Invalid homeserver: \(homeserverAddress)")
stopLoading()
handleError(error)
}
case .invalidWellKnownAlert(let error):
}
}
private func startLoading(label: String = L10n.commonLoading) {
userIndicatorController.submitIndicator(UserIndicator(type: .modal,
title: label,
persistent: true))
}
private func stopLoading() {
userIndicatorController.retractAllIndicators()
}
/// Processes an error to either update the flow or display it to the user.
private func handleError(_ error: AuthenticationServiceError) {
switch error {
case .invalidServer, .invalidHomeserverAddress:
showFooterMessage(L10n.screenChangeServerErrorInvalidHomeserver)
case .invalidWellKnown(let error):
state.bindings.alertInfo = AlertInfo(id: .invalidWellKnownAlert(error),
title: L10n.commonServerNotSupported,
message: L10n.screenChangeServerErrorInvalidWellKnown(error))
case .slidingSyncAlert:
case .slidingSyncNotAvailable:
let openURL = { UIApplication.shared.open(self.slidingSyncLearnMoreURL) }
state.bindings.alertInfo = AlertInfo(id: .slidingSyncAlert,
title: L10n.commonServerNotSupported,
message: L10n.screenChangeServerErrorNoSlidingSyncMessage,
primaryButton: .init(title: L10n.actionLearnMore, role: .cancel, action: openURL),
secondaryButton: .init(title: L10n.actionCancel, action: nil))
case .registrationAlert:
case .loginNotSupported:
state.bindings.alertInfo = AlertInfo(id: .loginAlert,
title: L10n.commonServerNotSupported,
message: L10n.screenLoginErrorUnsupportedAuthentication)
case .registrationNotSupported:
state.bindings.alertInfo = AlertInfo(id: .registrationAlert,
title: L10n.errorUnknown,
title: L10n.commonServerNotSupported,
message: L10n.errorAccountCreationNotPossible)
default:
showFooterMessage(L10n.errorUnknown)
}
}
// MARK: - Private
/// Set a new error message to be shown in the text field footer.
private func showFooterMessage(_ message: String) {
withElementAnimation {
state.footerErrorMessage = message
}
}
/// Clear any errors shown in the text field footer.
private func clearFooterError() {

View File

@@ -11,7 +11,4 @@ import Combine
protocol ServerSelectionScreenViewModelProtocol {
var actions: AnyPublisher<ServerSelectionScreenViewModelAction, Never> { get }
var context: ServerSelectionScreenViewModelType.Context { get }
/// Displays an error to the user.
func displayError(_ type: ServerSelectionScreenErrorType)
}

View File

@@ -91,11 +91,38 @@ struct ServerSelectionScreen: View {
// MARK: - Previews
struct ServerSelection_Previews: PreviewProvider, TestablePreview {
static let matrixViewModel = makeViewModel(for: "https://matrix.org")
static let emptyViewModel = makeViewModel(for: "")
static let invalidViewModel = makeViewModel(for: "thisisbad")
static var previews: some View {
ForEach(MockServerSelectionScreenState.allCases, id: \.self) { state in
NavigationStack {
ServerSelectionScreen(context: state.viewModel.context)
}
NavigationStack {
ServerSelectionScreen(context: matrixViewModel.context)
}
NavigationStack {
ServerSelectionScreen(context: emptyViewModel.context)
}
NavigationStack {
ServerSelectionScreen(context: invalidViewModel.context)
}
.snapshotPreferences(delay: 0.25)
}
static func makeViewModel(for homeserverAddress: String) -> ServerSelectionScreenViewModel {
let authenticationService = AuthenticationService.mock
let slidingSyncLearnMoreURL = ServiceLocator.shared.settings.slidingSyncLearnMoreURL
let viewModel = ServerSelectionScreenViewModel(authenticationService: authenticationService,
authenticationFlow: .login,
slidingSyncLearnMoreURL: slidingSyncLearnMoreURL,
userIndicatorController: UserIndicatorControllerMock())
viewModel.context.homeserverAddress = homeserverAddress
if homeserverAddress == "thisisbad" {
viewModel.context.send(viewAction: .confirm)
}
return viewModel
}
}

View File

@@ -65,6 +65,9 @@ class AuthenticationService: AuthenticationServiceProtocol {
case .failure: nil
}
if flow == .login, homeserver.loginMode == .unsupported {
return .failure(.loginNotSupported)
}
if flow == .register, !homeserver.supportsRegistration {
return .failure(.registrationNotSupported)
}

View File

@@ -24,6 +24,7 @@ enum AuthenticationServiceError: Error, Equatable {
case invalidHomeserverAddress
case invalidWellKnown(String)
case slidingSyncNotAvailable
case loginNotSupported
case registrationNotSupported
case accountDeactivated
case failedLoggingIn

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:667d6afdec1cad4db4439278981efc6cafb8c4bb52a28ed951120da64156481c
size 106217
oid sha256:b24a1036fe61c64214c929d53d409723bf388191205d4392759009680b451ad3
size 121094

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:028f43f14f8ad6f5a7eef82c6bcb01779ddec20b6cda44c405ecd874bc3ed35d
size 115393
oid sha256:8048d1dd4b99792cd4c68567499cfe3aba40a4e3b0697eda3d1b67a115d684f6
size 145736

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:04d2b178fa9a746c0ddef74e83b100011267ba40e98dbcd5ed8d75ec599b89f6
size 60409
oid sha256:d6b890b433313f7398c38f940ba1db616b89c563c5ba35e6378b0a4863b9c6b5
size 75379

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:7d05997731f755f45834075ac7dd62cc8b18b94b65354ccd4fb6c198ed91bac1
size 73627
oid sha256:1fe38dada4e2201f011443eda61791124e33e5423a049ea4f9b7c942c3405b1b
size 103829

View File

@@ -89,7 +89,7 @@ class ServerConfirmationScreenViewModelTests: XCTestCase {
func testRegistrationNotSupportedAlert() async throws {
// Given a view model for registration using a service that hasn't been configured and the default server doesn't support registration.
setupViewModel(authenticationFlow: .register, elementWellKnown: false)
setupViewModel(authenticationFlow: .register, supportsRegistrationHelper: false)
XCTAssertEqual(service.homeserver.value.loginMode, .unknown)
XCTAssertFalse(service.homeserver.value.supportsRegistration)
XCTAssertEqual(clientBuilderFactory.makeBuilderSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount, 0)
@@ -105,10 +105,34 @@ class ServerConfirmationScreenViewModelTests: XCTestCase {
XCTAssertEqual(context.alertInfo?.id, .registration)
}
func testLoginNotSupportedAlert() async throws {
// Given a view model for login using a service that hasn't been configured and the default server doesn't support login.
setupViewModel(authenticationFlow: .login, supportsRegistrationHelper: false, supportsPasswordLogin: false)
XCTAssertEqual(service.homeserver.value.loginMode, .unknown)
XCTAssertFalse(service.homeserver.value.supportsRegistration)
XCTAssertEqual(clientBuilderFactory.makeBuilderSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount, 0)
XCTAssertNil(context.alertInfo)
// When continuing from the confirmation screen.
let deferred = deferFulfillment(context.$viewState) { $0.bindings.alertInfo != nil }
context.send(viewAction: .confirm)
try await deferred.fulfill()
// Then the configuration should fail with an alert about not supporting login.
XCTAssertEqual(clientBuilderFactory.makeBuilderSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount, 1)
XCTAssertEqual(context.alertInfo?.id, .login)
}
// MARK: - Helpers
private func setupViewModel(authenticationFlow: AuthenticationFlow, elementWellKnown: Bool = true) {
let client = ClientSDKMock(configuration: elementWellKnown ? .init() : .init(elementWellKnown: ""))
private func setupViewModel(authenticationFlow: AuthenticationFlow, supportsRegistrationHelper: Bool = true, supportsPasswordLogin: Bool = true) {
// Manually create a configuration as the default homeserver address setting is immutable.
let clientConfiguration: ClientSDKMock.Configuration = if supportsRegistrationHelper {
.init(supportsPasswordLogin: supportsPasswordLogin)
} else {
.init(supportsPasswordLogin: supportsPasswordLogin, elementWellKnown: "")
}
let client = ClientSDKMock(configuration: clientConfiguration)
let configuration = AuthenticationClientBuilderMock.Configuration(homeserverClients: ["matrix.org": client],
qrCodeClient: client)

View File

@@ -11,38 +11,131 @@ import XCTest
@MainActor
class ServerSelectionViewModelTests: XCTestCase {
var viewModel: ServerSelectionScreenViewModelProtocol!
var context: ServerSelectionScreenViewModelType.Context!
var clientBuilderFactory: AuthenticationClientBuilderFactoryMock!
var service: AuthenticationServiceProtocol!
@MainActor override func setUp() {
viewModel = ServerSelectionScreenViewModel(homeserverAddress: "",
slidingSyncLearnMoreURL: ServiceLocator.shared.settings.slidingSyncLearnMoreURL)
context = viewModel.context
var viewModel: ServerSelectionScreenViewModelProtocol!
var context: ServerSelectionScreenViewModelType.Context { viewModel.context }
func testSelectForLogin() async throws {
// Given a view model for login.
setupViewModel(authenticationFlow: .login)
XCTAssertEqual(service.homeserver.value.loginMode, .unknown)
XCTAssertFalse(service.homeserver.value.supportsRegistration)
XCTAssertEqual(clientBuilderFactory.makeBuilderSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount, 0)
// When selecting matrix.org.
context.homeserverAddress = "matrix.org"
let deferred = deferFulfillment(viewModel.actions) { $0 == .updated }
context.send(viewAction: .confirm)
try await deferred.fulfill()
// Then selection should succeed.
XCTAssertEqual(clientBuilderFactory.makeBuilderSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount, 1)
XCTAssertEqual(service.homeserver.value, .mockMatrixDotOrg)
}
func testErrorMessage() async throws {
func testLoginNotSupportedAlert() async throws {
// Given a view model for login.
setupViewModel(authenticationFlow: .login)
XCTAssertEqual(service.homeserver.value.loginMode, .unknown)
XCTAssertFalse(service.homeserver.value.supportsRegistration)
XCTAssertEqual(clientBuilderFactory.makeBuilderSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount, 0)
XCTAssertNil(context.alertInfo)
// When selecting a server that doesn't support login.
context.homeserverAddress = "server.net"
let deferred = deferFulfillment(context.$viewState) { $0.bindings.alertInfo != nil }
context.send(viewAction: .confirm)
try await deferred.fulfill()
// Then selection should fail with an alert about not supporting registration.
XCTAssertEqual(clientBuilderFactory.makeBuilderSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount, 1)
XCTAssertEqual(context.alertInfo?.id, .loginAlert)
}
func testSelectForRegistration() async throws {
// Given a view model for registration.
setupViewModel(authenticationFlow: .register)
XCTAssertEqual(service.homeserver.value.loginMode, .unknown)
XCTAssertFalse(service.homeserver.value.supportsRegistration)
XCTAssertEqual(clientBuilderFactory.makeBuilderSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount, 0)
// When selecting matrix.org.
context.homeserverAddress = "matrix.org"
let deferred = deferFulfillment(viewModel.actions) { $0 == .updated }
context.send(viewAction: .confirm)
try await deferred.fulfill()
// Then selection should succeed.
XCTAssertEqual(clientBuilderFactory.makeBuilderSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount, 1)
XCTAssertEqual(service.homeserver.value, .mockMatrixDotOrg)
}
func testRegistrationNotSupportedAlert() async throws {
// Given a view model for registration.
setupViewModel(authenticationFlow: .register)
XCTAssertEqual(service.homeserver.value.loginMode, .unknown)
XCTAssertFalse(service.homeserver.value.supportsRegistration)
XCTAssertEqual(clientBuilderFactory.makeBuilderSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount, 0)
XCTAssertNil(context.alertInfo)
// When selecting a server that doesn't support registration.
context.homeserverAddress = "example.com"
let deferred = deferFulfillment(context.$viewState) { $0.bindings.alertInfo != nil }
context.send(viewAction: .confirm)
try await deferred.fulfill()
// Then selection should fail with an alert about not supporting registration.
XCTAssertEqual(clientBuilderFactory.makeBuilderSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount, 1)
XCTAssertEqual(context.alertInfo?.id, .registrationAlert)
}
func testInvalidServer() async throws {
// Given a new instance of the view model.
setupViewModel(authenticationFlow: .login)
XCTAssertFalse(context.viewState.isShowingFooterError, "There should not be an error message for a new view model.")
XCTAssertNil(context.viewState.footerErrorMessage, "There should not be an error message for a new view model.")
XCTAssertEqual(String(context.viewState.footerMessage.characters), L10n.screenChangeServerFormNotice(L10n.actionLearnMore),
"The standard footer message should be shown.")
// When an error occurs.
let message = "Unable to contact server."
viewModel.displayError(.footerMessage(message))
// When attempting to discover an invalid server
var deferred = deferFulfillment(context.$viewState) { $0.isShowingFooterError }
context.homeserverAddress = "idontexist"
context.send(viewAction: .confirm)
try await deferred.fulfill()
// Then the footer should now be showing an error.
XCTAssertEqual(context.viewState.footerErrorMessage, message, "The error message should be stored.")
XCTAssertEqual(String(context.viewState.footerMessage.characters), message, "The error message should be shown.")
XCTAssertTrue(context.viewState.isShowingFooterError, "The error message should be stored.")
XCTAssertNotNil(context.viewState.footerErrorMessage, "The error message should be stored.")
XCTAssertNotEqual(String(context.viewState.footerMessage.characters), L10n.screenChangeServerFormNotice(L10n.actionLearnMore),
"The error message should be shown.")
// And when clearing the error.
deferred = deferFulfillment(context.$viewState) { !$0.isShowingFooterError }
context.homeserverAddress = ""
context.send(viewAction: .clearFooterError)
// Wait for the action to spawn a Task.
await Task.yield()
try await deferred.fulfill()
// Then the error message should now be removed.
XCTAssertNil(context.viewState.footerErrorMessage, "The error message should have been cleared.")
XCTAssertEqual(String(context.viewState.footerMessage.characters), L10n.screenChangeServerFormNotice(L10n.actionLearnMore),
"The standard footer message should be shown again.")
}
// MARK: - Helpers
private func setupViewModel(authenticationFlow: AuthenticationFlow) {
clientBuilderFactory = AuthenticationClientBuilderFactoryMock(configuration: .init())
service = AuthenticationService(userSessionStore: UserSessionStoreMock(configuration: .init()),
encryptionKeyProvider: EncryptionKeyProvider(),
clientBuilderFactory: clientBuilderFactory,
appSettings: ServiceLocator.shared.settings,
appHooks: AppHooks())
viewModel = ServerSelectionScreenViewModel(authenticationService: service,
authenticationFlow: authenticationFlow,
slidingSyncLearnMoreURL: ServiceLocator.shared.settings.slidingSyncLearnMoreURL,
userIndicatorController: UserIndicatorControllerMock())
}
}