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 8a3994016d
commit 17dfe1359a
17 changed files with 263 additions and 139 deletions
-4
View File
@@ -829,7 +829,6 @@
B6DA66EFC13A90846B625836 /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = 91DE43B8815918E590912DDA /* InfoPlist.strings */; }; B6DA66EFC13A90846B625836 /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = 91DE43B8815918E590912DDA /* InfoPlist.strings */; };
B6DF6B6FA8734B70F9BF261E /* BlurHashDecode.swift in Sources */ = {isa = PBXBuildFile; fileRef = E5272BC4A60B6AD7553BACA1 /* BlurHashDecode.swift */; }; B6DF6B6FA8734B70F9BF261E /* BlurHashDecode.swift in Sources */ = {isa = PBXBuildFile; fileRef = E5272BC4A60B6AD7553BACA1 /* BlurHashDecode.swift */; };
B6EC2148FA5443C9289BEEBA /* MediaProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = F17EFA1D3D09FC2F9C5E1CB2 /* MediaProvider.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 */; }; B773ACD8881DB18E876D950C /* WaveformSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94028A227645FA880B966211 /* WaveformSource.swift */; };
B7888FC1E1DEF816D175C8D6 /* SecureBackupKeyBackupScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD72A9B720D75DBE60AC299F /* SecureBackupKeyBackupScreenModels.swift */; }; B7888FC1E1DEF816D175C8D6 /* SecureBackupKeyBackupScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD72A9B720D75DBE60AC299F /* SecureBackupKeyBackupScreenModels.swift */; };
B796A25F282C0A340D1B9C12 /* ImageRoomTimelineItemContent.swift in Sources */ = {isa = PBXBuildFile; fileRef = B2B5EDCD05D50BA9B815C66C /* ImageRoomTimelineItemContent.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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; D8FC33C3F6BF597E095CE9FA /* HomeScreenInviteCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeScreenInviteCell.swift; sourceTree = "<group>"; };
@@ -2815,7 +2813,6 @@
2D0D49B0533C4C2EB889BF3A /* ServerSelectionScreen */ = { 2D0D49B0533C4C2EB889BF3A /* ServerSelectionScreen */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
D8E057FB1F07A5C201C89061 /* MockServerSelectionScreenState.swift */,
BB8BC4C791D0E88CFCF4E5DF /* ServerSelectionScreenCoordinator.swift */, BB8BC4C791D0E88CFCF4E5DF /* ServerSelectionScreenCoordinator.swift */,
9501D11B4258DFA33BA3B40F /* ServerSelectionScreenModels.swift */, 9501D11B4258DFA33BA3B40F /* ServerSelectionScreenModels.swift */,
E3059CFA00C67D8787273B20 /* ServerSelectionScreenViewModel.swift */, E3059CFA00C67D8787273B20 /* ServerSelectionScreenViewModel.swift */,
@@ -6593,7 +6590,6 @@
C97325EFDCCEE457432A9E82 /* MessageText.swift in Sources */, C97325EFDCCEE457432A9E82 /* MessageText.swift in Sources */,
B659E3A49889E749E3239EA7 /* MockMediaProvider.swift in Sources */, B659E3A49889E749E3239EA7 /* MockMediaProvider.swift in Sources */,
09C83DDDB07C28364F325209 /* MockRoomTimelineController.swift in Sources */, 09C83DDDB07C28364F325209 /* MockRoomTimelineController.swift in Sources */,
B721125D17A0BA86794F29FB /* MockServerSelectionScreenState.swift in Sources */,
AF2ABA2794E376B64104C964 /* MockSoftLogoutScreenState.swift in Sources */, AF2ABA2794E376B64104C964 /* MockSoftLogoutScreenState.swift in Sources */,
F9842667B68DC6FA1F9ECCBB /* NSItemProvider.swift in Sources */, F9842667B68DC6FA1F9ECCBB /* NSItemProvider.swift in Sources */,
EA01A06EEDFEF4AE7652E5F3 /* NSRegularExpresion.swift in Sources */, EA01A06EEDFEF4AE7652E5F3 /* NSRegularExpresion.swift in Sources */,
@@ -72,6 +72,8 @@ enum ServerConfirmationScreenAlert: Hashable {
case invalidWellKnown(String) case invalidWellKnown(String)
/// An alert that allows the user to learn about sliding sync. /// An alert that allows the user to learn about sliding sync.
case slidingSync 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. /// An alert that informs the user that registration isn't supported.
case registration case registration
/// An unknown error has occurred. /// An unknown error has occurred.
@@ -82,6 +82,8 @@ class ServerConfirmationScreenViewModel: ServerConfirmationScreenViewModelType,
displayError(.invalidWellKnown(error)) displayError(.invalidWellKnown(error))
case .slidingSyncNotAvailable: case .slidingSyncNotAvailable:
displayError(.slidingSync) displayError(.slidingSync)
case .loginNotSupported:
displayError(.login)
case .registrationNotSupported: case .registrationNotSupported:
displayError(.registration) displayError(.registration)
default: default:
@@ -117,9 +119,13 @@ class ServerConfirmationScreenViewModel: ServerConfirmationScreenViewModelType,
message: L10n.screenChangeServerErrorNoSlidingSyncMessage, message: L10n.screenChangeServerErrorNoSlidingSyncMessage,
primaryButton: .init(title: L10n.actionLearnMore, role: .cancel, action: openURL), primaryButton: .init(title: L10n.actionLearnMore, role: .cancel, action: openURL),
secondaryButton: .init(title: L10n.actionCancel, action: nil)) secondaryButton: .init(title: L10n.actionCancel, action: nil))
case .login:
state.bindings.alertInfo = AlertInfo(id: .login,
title: L10n.commonServerNotSupported,
message: L10n.screenLoginErrorUnsupportedAuthentication)
case .registration: case .registration:
state.bindings.alertInfo = AlertInfo(id: .registration, state.bindings.alertInfo = AlertInfo(id: .registration,
title: L10n.errorUnknown, title: L10n.commonServerNotSupported,
message: L10n.errorAccountCreationNotPossible) message: L10n.errorAccountCreationNotPossible)
case .unknownError: case .unknownError:
state.bindings.alertInfo = AlertInfo(id: .unknownError) state.bindings.alertInfo = AlertInfo(id: .unknownError)
@@ -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
}
}
}
@@ -37,8 +37,10 @@ final class ServerSelectionScreenCoordinator: CoordinatorProtocol {
init(parameters: ServerSelectionScreenCoordinatorParameters) { init(parameters: ServerSelectionScreenCoordinatorParameters) {
self.parameters = parameters self.parameters = parameters
viewModel = ServerSelectionScreenViewModel(homeserverAddress: parameters.authenticationService.homeserver.value.address, viewModel = ServerSelectionScreenViewModel(authenticationService: parameters.authenticationService,
slidingSyncLearnMoreURL: parameters.slidingSyncLearnMoreURL) authenticationFlow: parameters.authenticationFlow,
slidingSyncLearnMoreURL: parameters.slidingSyncLearnMoreURL,
userIndicatorController: parameters.userIndicatorController)
userIndicatorController = parameters.userIndicatorController userIndicatorController = parameters.userIndicatorController
} }
@@ -50,8 +52,8 @@ final class ServerSelectionScreenCoordinator: CoordinatorProtocol {
guard let self else { return } guard let self else { return }
switch action { switch action {
case .confirm(let homeserverAddress): case .updated:
self.useHomeserver(homeserverAddress) actionsSubject.send(.updated)
case .dismiss: case .dismiss:
actionsSubject.send(.dismiss) actionsSubject.send(.dismiss)
} }
@@ -60,56 +62,10 @@ final class ServerSelectionScreenCoordinator: CoordinatorProtocol {
} }
func stop() { func stop() {
stopLoading() parameters.userIndicatorController.retractAllIndicators()
} }
func toPresentable() -> AnyView { func toPresentable() -> AnyView {
AnyView(ServerSelectionScreen(context: viewModel.context)) 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))
}
}
} }
@@ -8,8 +8,8 @@
import Foundation import Foundation
enum ServerSelectionScreenViewModelAction { enum ServerSelectionScreenViewModelAction {
/// The user would like to use the homeserver at the given address. /// The homeserver selection has been updated.
case confirm(homeserverAddress: String) case updated
/// Dismiss the view without using the entered address. /// Dismiss the view without using the entered address.
case dismiss case dismiss
} }
@@ -74,6 +74,8 @@ enum ServerSelectionScreenErrorType: Hashable {
case invalidWellKnownAlert(String) case invalidWellKnownAlert(String)
/// An alert that allows the user to learn about sliding sync. /// An alert that allows the user to learn about sliding sync.
case slidingSyncAlert 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. /// An alert that informs the user that registration isn't supported.
case registrationAlert case registrationAlert
} }
@@ -11,7 +11,10 @@ import SwiftUI
typealias ServerSelectionScreenViewModelType = StateStoreViewModel<ServerSelectionScreenViewState, ServerSelectionScreenViewAction> typealias ServerSelectionScreenViewModelType = StateStoreViewModel<ServerSelectionScreenViewState, ServerSelectionScreenViewAction>
class ServerSelectionScreenViewModel: ServerSelectionScreenViewModelType, ServerSelectionScreenViewModelProtocol { class ServerSelectionScreenViewModel: ServerSelectionScreenViewModelType, ServerSelectionScreenViewModelProtocol {
private let authenticationService: AuthenticationServiceProtocol
private let authenticationFlow: AuthenticationFlow
private let slidingSyncLearnMoreURL: URL private let slidingSyncLearnMoreURL: URL
private let userIndicatorController: UserIndicatorControllerProtocol
private var actionsSubject: PassthroughSubject<ServerSelectionScreenViewModelAction, Never> = .init() private var actionsSubject: PassthroughSubject<ServerSelectionScreenViewModelAction, Never> = .init()
@@ -19,18 +22,23 @@ class ServerSelectionScreenViewModel: ServerSelectionScreenViewModelType, Server
actionsSubject.eraseToAnyPublisher() 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 self.slidingSyncLearnMoreURL = slidingSyncLearnMoreURL
let bindings = ServerSelectionScreenBindings(homeserverAddress: homeserverAddress) self.userIndicatorController = userIndicatorController
super.init(initialViewState: ServerSelectionScreenViewState(slidingSyncLearnMoreURL: slidingSyncLearnMoreURL, let bindings = ServerSelectionScreenBindings(homeserverAddress: authenticationService.homeserver.value.address)
bindings: bindings)) super.init(initialViewState: ServerSelectionScreenViewState(slidingSyncLearnMoreURL: slidingSyncLearnMoreURL, bindings: bindings))
} }
override func process(viewAction: ServerSelectionScreenViewAction) { override func process(viewAction: ServerSelectionScreenViewAction) {
switch viewAction { switch viewAction {
case .confirm: case .confirm:
actionsSubject.send(.confirm(homeserverAddress: state.bindings.homeserverAddress)) configureHomeserver()
case .dismiss: case .dismiss:
actionsSubject.send(.dismiss) actionsSubject.send(.dismiss)
case .clearFooterError: case .clearFooterError:
@@ -38,31 +46,72 @@ class ServerSelectionScreenViewModel: ServerSelectionScreenViewModelType, Server
} }
} }
func displayError(_ type: ServerSelectionScreenErrorType) { // MARK: - Private
switch type {
case .footerMessage(let message): /// Updates the login flow using the supplied homeserver address, or shows an error when this isn't possible.
withElementAnimation { private func configureHomeserver() {
state.footerErrorMessage = message 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), state.bindings.alertInfo = AlertInfo(id: .invalidWellKnownAlert(error),
title: L10n.commonServerNotSupported, title: L10n.commonServerNotSupported,
message: L10n.screenChangeServerErrorInvalidWellKnown(error)) message: L10n.screenChangeServerErrorInvalidWellKnown(error))
case .slidingSyncAlert: case .slidingSyncNotAvailable:
let openURL = { UIApplication.shared.open(self.slidingSyncLearnMoreURL) } let openURL = { UIApplication.shared.open(self.slidingSyncLearnMoreURL) }
state.bindings.alertInfo = AlertInfo(id: .slidingSyncAlert, state.bindings.alertInfo = AlertInfo(id: .slidingSyncAlert,
title: L10n.commonServerNotSupported, title: L10n.commonServerNotSupported,
message: L10n.screenChangeServerErrorNoSlidingSyncMessage, message: L10n.screenChangeServerErrorNoSlidingSyncMessage,
primaryButton: .init(title: L10n.actionLearnMore, role: .cancel, action: openURL), primaryButton: .init(title: L10n.actionLearnMore, role: .cancel, action: openURL),
secondaryButton: .init(title: L10n.actionCancel, action: nil)) 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, state.bindings.alertInfo = AlertInfo(id: .registrationAlert,
title: L10n.errorUnknown, title: L10n.commonServerNotSupported,
message: L10n.errorAccountCreationNotPossible) 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. /// Clear any errors shown in the text field footer.
private func clearFooterError() { private func clearFooterError() {
@@ -11,7 +11,4 @@ import Combine
protocol ServerSelectionScreenViewModelProtocol { protocol ServerSelectionScreenViewModelProtocol {
var actions: AnyPublisher<ServerSelectionScreenViewModelAction, Never> { get } var actions: AnyPublisher<ServerSelectionScreenViewModelAction, Never> { get }
var context: ServerSelectionScreenViewModelType.Context { get } var context: ServerSelectionScreenViewModelType.Context { get }
/// Displays an error to the user.
func displayError(_ type: ServerSelectionScreenErrorType)
} }
@@ -91,11 +91,38 @@ struct ServerSelectionScreen: View {
// MARK: - Previews // MARK: - Previews
struct ServerSelection_Previews: PreviewProvider, TestablePreview { 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 { static var previews: some View {
ForEach(MockServerSelectionScreenState.allCases, id: \.self) { state in NavigationStack {
NavigationStack { ServerSelectionScreen(context: matrixViewModel.context)
ServerSelectionScreen(context: state.viewModel.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
} }
} }
@@ -65,6 +65,9 @@ class AuthenticationService: AuthenticationServiceProtocol {
case .failure: nil case .failure: nil
} }
if flow == .login, homeserver.loginMode == .unsupported {
return .failure(.loginNotSupported)
}
if flow == .register, !homeserver.supportsRegistration { if flow == .register, !homeserver.supportsRegistration {
return .failure(.registrationNotSupported) return .failure(.registrationNotSupported)
} }
@@ -24,6 +24,7 @@ enum AuthenticationServiceError: Error, Equatable {
case invalidHomeserverAddress case invalidHomeserverAddress
case invalidWellKnown(String) case invalidWellKnown(String)
case slidingSyncNotAvailable case slidingSyncNotAvailable
case loginNotSupported
case registrationNotSupported case registrationNotSupported
case accountDeactivated case accountDeactivated
case failedLoggingIn case failedLoggingIn
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:667d6afdec1cad4db4439278981efc6cafb8c4bb52a28ed951120da64156481c oid sha256:b24a1036fe61c64214c929d53d409723bf388191205d4392759009680b451ad3
size 106217 size 121094
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:028f43f14f8ad6f5a7eef82c6bcb01779ddec20b6cda44c405ecd874bc3ed35d oid sha256:8048d1dd4b99792cd4c68567499cfe3aba40a4e3b0697eda3d1b67a115d684f6
size 115393 size 145736
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:04d2b178fa9a746c0ddef74e83b100011267ba40e98dbcd5ed8d75ec599b89f6 oid sha256:d6b890b433313f7398c38f940ba1db616b89c563c5ba35e6378b0a4863b9c6b5
size 60409 size 75379
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:7d05997731f755f45834075ac7dd62cc8b18b94b65354ccd4fb6c198ed91bac1 oid sha256:1fe38dada4e2201f011443eda61791124e33e5423a049ea4f9b7c942c3405b1b
size 73627 size 103829
@@ -89,7 +89,7 @@ class ServerConfirmationScreenViewModelTests: XCTestCase {
func testRegistrationNotSupportedAlert() async throws { 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. // 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) XCTAssertEqual(service.homeserver.value.loginMode, .unknown)
XCTAssertFalse(service.homeserver.value.supportsRegistration) XCTAssertFalse(service.homeserver.value.supportsRegistration)
XCTAssertEqual(clientBuilderFactory.makeBuilderSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount, 0) XCTAssertEqual(clientBuilderFactory.makeBuilderSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount, 0)
@@ -105,10 +105,34 @@ class ServerConfirmationScreenViewModelTests: XCTestCase {
XCTAssertEqual(context.alertInfo?.id, .registration) 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 // MARK: - Helpers
private func setupViewModel(authenticationFlow: AuthenticationFlow, elementWellKnown: Bool = true) { private func setupViewModel(authenticationFlow: AuthenticationFlow, supportsRegistrationHelper: Bool = true, supportsPasswordLogin: Bool = true) {
let client = ClientSDKMock(configuration: elementWellKnown ? .init() : .init(elementWellKnown: "")) // 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], let configuration = AuthenticationClientBuilderMock.Configuration(homeserverClients: ["matrix.org": client],
qrCodeClient: client) qrCodeClient: client)
@@ -11,38 +11,131 @@ import XCTest
@MainActor @MainActor
class ServerSelectionViewModelTests: XCTestCase { class ServerSelectionViewModelTests: XCTestCase {
var viewModel: ServerSelectionScreenViewModelProtocol! var clientBuilderFactory: AuthenticationClientBuilderFactoryMock!
var context: ServerSelectionScreenViewModelType.Context! var service: AuthenticationServiceProtocol!
@MainActor override func setUp() { var viewModel: ServerSelectionScreenViewModelProtocol!
viewModel = ServerSelectionScreenViewModel(homeserverAddress: "", var context: ServerSelectionScreenViewModelType.Context { viewModel.context }
slidingSyncLearnMoreURL: ServiceLocator.shared.settings.slidingSyncLearnMoreURL)
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. // 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.") 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), XCTAssertEqual(String(context.viewState.footerMessage.characters), L10n.screenChangeServerFormNotice(L10n.actionLearnMore),
"The standard footer message should be shown.") "The standard footer message should be shown.")
// When an error occurs. // When attempting to discover an invalid server
let message = "Unable to contact server." var deferred = deferFulfillment(context.$viewState) { $0.isShowingFooterError }
viewModel.displayError(.footerMessage(message)) context.homeserverAddress = "idontexist"
context.send(viewAction: .confirm)
try await deferred.fulfill()
// Then the footer should now be showing an error. // Then the footer should now be showing an error.
XCTAssertEqual(context.viewState.footerErrorMessage, message, "The error message should be stored.") XCTAssertTrue(context.viewState.isShowingFooterError, "The error message should be stored.")
XCTAssertEqual(String(context.viewState.footerMessage.characters), message, "The error message should be shown.") 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. // And when clearing the error.
deferred = deferFulfillment(context.$viewState) { !$0.isShowingFooterError }
context.homeserverAddress = ""
context.send(viewAction: .clearFooterError) context.send(viewAction: .clearFooterError)
try await deferred.fulfill()
// Wait for the action to spawn a Task.
await Task.yield()
// Then the error message should now be removed. // Then the error message should now be removed.
XCTAssertNil(context.viewState.footerErrorMessage, "The error message should have been cleared.") XCTAssertNil(context.viewState.footerErrorMessage, "The error message should have been cleared.")
XCTAssertEqual(String(context.viewState.footerMessage.characters), L10n.screenChangeServerFormNotice(L10n.actionLearnMore), XCTAssertEqual(String(context.viewState.footerMessage.characters), L10n.screenChangeServerFormNotice(L10n.actionLearnMore),
"The standard footer message should be shown again.") "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())
}
} }