From bd7144093c8b2267f2799eb61fd7c5201dfd20c5 Mon Sep 17 00:00:00 2001 From: Doug Date: Fri, 20 Sep 2024 11:27:16 +0100 Subject: [PATCH 1/5] Don't query the homeserver until confirming it (or selecting a different one). --- .../Sources/Application/AppCoordinator.swift | 2 +- .../AuthenticationFlowCoordinator.swift | 65 ++++--------- .../LoginScreen/LoginHomeserver.swift | 5 + .../LoginScreen/LoginScreenCoordinator.swift | 2 +- .../ServerConfirmationScreenCoordinator.swift | 6 +- .../ServerConfirmationScreenModels.swift | 34 ++++--- .../ServerConfirmationScreenViewModel.swift | 95 +++++++++++++++++-- .../View/ServerConfirmationScreen.swift | 15 ++- .../MockServerSelectionScreenState.swift | 14 +-- .../ServerSelectionScreenCoordinator.swift | 11 ++- .../ServerSelectionScreenModels.swift | 12 +-- .../ServerSelectionScreenViewModel.swift | 11 ++- .../View/ServerSelectionScreen.swift | 11 +-- .../AuthenticationService.swift | 19 +++- .../AuthenticationServiceProtocol.swift | 8 +- .../MockAuthenticationService.swift | 12 ++- .../UITests/UITestsAppCoordinator.swift | 5 +- .../ServerSelectionViewModelTests.swift | 3 +- 18 files changed, 208 insertions(+), 122 deletions(-) diff --git a/ElementX/Sources/Application/AppCoordinator.swift b/ElementX/Sources/Application/AppCoordinator.swift index c979c0035b..18f366bea9 100644 --- a/ElementX/Sources/Application/AppCoordinator.swift +++ b/ElementX/Sources/Application/AppCoordinator.swift @@ -485,7 +485,7 @@ class AppCoordinator: AppCoordinatorProtocol, AuthenticationFlowCoordinatorDeleg encryptionKeyProvider: EncryptionKeyProvider(), appSettings: appSettings, appHooks: appHooks) - _ = await authenticationService.configure(for: userSession.clientProxy.homeserver) + _ = await authenticationService.configure(for: userSession.clientProxy.homeserver, flow: .login) let parameters = SoftLogoutScreenCoordinatorParameters(authenticationService: authenticationService, credentials: credentials, diff --git a/ElementX/Sources/FlowCoordinators/AuthenticationFlowCoordinator.swift b/ElementX/Sources/FlowCoordinators/AuthenticationFlowCoordinator.swift index 996ce5b743..f2cf5c9f47 100644 --- a/ElementX/Sources/FlowCoordinators/AuthenticationFlowCoordinator.swift +++ b/ElementX/Sources/FlowCoordinators/AuthenticationFlowCoordinator.swift @@ -86,11 +86,11 @@ class AuthenticationFlowCoordinator: FlowCoordinatorProtocol { switch action { case .loginManually: - Task { await self.startAuthentication(flow: .login) } + showServerConfirmationScreen(authenticationFlow: .login) case .loginWithQR: startQRCodeLogin() case .register: - Task { await self.startAuthentication(flow: .register) } + showServerConfirmationScreen(authenticationFlow: .register) case .reportProblem: showReportProblemScreen() } @@ -113,7 +113,7 @@ class AuthenticationFlowCoordinator: FlowCoordinatorProtocol { switch action { case .signInManually: navigationStackCoordinator.setSheetCoordinator(nil) - Task { await self.startAuthentication(flow: .login) } + showServerConfirmationScreen(authenticationFlow: .login) case .cancel: navigationStackCoordinator.setSheetCoordinator(nil) case .done(let userSession): @@ -137,25 +137,14 @@ class AuthenticationFlowCoordinator: FlowCoordinatorProtocol { bugReportFlowCoordinator?.start() } - private func startAuthentication(flow: AuthenticationFlow) async { - startLoading() - - switch await authenticationService.configure(for: appSettings.defaultHomeserverAddress) { - case .success: - stopLoading() - showServerConfirmationScreen(authenticationFlow: flow) - case .failure: - stopLoading() - showServerSelectionScreen(authenticationFlow: flow, isModallyPresented: false) - } - } - - private func showServerSelectionScreen(authenticationFlow: AuthenticationFlow, isModallyPresented: Bool) { + // TODO: Move this method after showServerConfirmationScreen + private func showServerSelectionScreen(authenticationFlow: AuthenticationFlow) { let navigationCoordinator = NavigationStackCoordinator() let parameters = ServerSelectionScreenCoordinatorParameters(authenticationService: authenticationService, - userIndicatorController: userIndicatorController, - isModallyPresented: isModallyPresented) + authenticationFlow: authenticationFlow, + slidingSyncLearnMoreURL: appSettings.slidingSyncLearnMoreURL, + userIndicatorController: userIndicatorController) let coordinator = ServerSelectionScreenCoordinator(parameters: parameters) coordinator.actions @@ -164,42 +153,26 @@ class AuthenticationFlowCoordinator: FlowCoordinatorProtocol { switch action { case .updated: - if isModallyPresented { - navigationStackCoordinator.setSheetCoordinator(nil) - } else { - // We are here because the default server failed to respond. - if authenticationService.homeserver.value.loginMode == .password { - if authenticationFlow == .login { - // Add the password login screen directly to the flow, its fine. - showLoginScreen() - } else { - // Add the web registration screen directly to the flow, its fine. - showWebRegistration() - } - } else { - // OIDC is presented from the confirmation screen so replace the - // server selection screen which was inserted to handle the failure. - navigationStackCoordinator.pop() - showServerConfirmationScreen(authenticationFlow: authenticationFlow) - } - } + navigationStackCoordinator.setSheetCoordinator(nil) case .dismiss: navigationStackCoordinator.setSheetCoordinator(nil) } } .store(in: &cancellables) - if isModallyPresented { - navigationCoordinator.setRootCoordinator(coordinator) - navigationStackCoordinator.setSheetCoordinator(navigationCoordinator) - } else { - navigationStackCoordinator.push(coordinator) - } + navigationCoordinator.setRootCoordinator(coordinator) + navigationStackCoordinator.setSheetCoordinator(navigationCoordinator) } private func showServerConfirmationScreen(authenticationFlow: AuthenticationFlow) { + // Reset the service back to the default homeserver before continuing. This ensures + // we check that registration is supported if it was previously configured for login. + authenticationService.reset() + let parameters = ServerConfirmationScreenCoordinatorParameters(authenticationService: authenticationService, - authenticationFlow: authenticationFlow) + authenticationFlow: authenticationFlow, + slidingSyncLearnMoreURL: appSettings.slidingSyncLearnMoreURL, + userIndicatorController: userIndicatorController) let coordinator = ServerConfirmationScreenCoordinator(parameters: parameters) coordinator.actions.sink { [weak self] action in @@ -215,7 +188,7 @@ class AuthenticationFlowCoordinator: FlowCoordinatorProtocol { showLoginScreen() } case .changeServer: - showServerSelectionScreen(authenticationFlow: authenticationFlow, isModallyPresented: true) + showServerSelectionScreen(authenticationFlow: authenticationFlow) } } .store(in: &cancellables) diff --git a/ElementX/Sources/Screens/Authentication/LoginScreen/LoginHomeserver.swift b/ElementX/Sources/Screens/Authentication/LoginScreen/LoginHomeserver.swift index 5bfb33da79..9f3e1fc763 100644 --- a/ElementX/Sources/Screens/Authentication/LoginScreen/LoginHomeserver.swift +++ b/ElementX/Sources/Screens/Authentication/LoginScreen/LoginHomeserver.swift @@ -25,6 +25,11 @@ struct LoginHomeserver: Equatable { self.registrationHelperURL = registrationHelperURL } + /// Whether or not the app is able to register on this homeserver. + var supportsRegistration: Bool { + loginMode == .oidc || (address == "matrix.org" && registrationHelperURL != nil) + } + /// Sanitizes a user entered homeserver address with the following rules /// - Trim any whitespace. /// - Lowercase the address. diff --git a/ElementX/Sources/Screens/Authentication/LoginScreen/LoginScreenCoordinator.swift b/ElementX/Sources/Screens/Authentication/LoginScreen/LoginScreenCoordinator.swift index 9c6d3e2be4..fbbb7409ba 100644 --- a/ElementX/Sources/Screens/Authentication/LoginScreen/LoginScreenCoordinator.swift +++ b/ElementX/Sources/Screens/Authentication/LoginScreen/LoginScreenCoordinator.swift @@ -145,7 +145,7 @@ final class LoginScreenCoordinator: CoordinatorProtocol { startLoading(isInteractionBlocking: false) Task { - switch await authenticationService.configure(for: homeserverDomain) { + switch await authenticationService.configure(for: homeserverDomain, flow: .login) { case .success: stopLoading() if authenticationService.homeserver.value.loginMode == .oidc { diff --git a/ElementX/Sources/Screens/Authentication/ServerConfirmationScreen/ServerConfirmationScreenCoordinator.swift b/ElementX/Sources/Screens/Authentication/ServerConfirmationScreen/ServerConfirmationScreenCoordinator.swift index 37bd5f4845..6d13d835f7 100644 --- a/ElementX/Sources/Screens/Authentication/ServerConfirmationScreen/ServerConfirmationScreenCoordinator.swift +++ b/ElementX/Sources/Screens/Authentication/ServerConfirmationScreen/ServerConfirmationScreenCoordinator.swift @@ -11,6 +11,8 @@ import SwiftUI struct ServerConfirmationScreenCoordinatorParameters { let authenticationService: AuthenticationServiceProtocol let authenticationFlow: AuthenticationFlow + let slidingSyncLearnMoreURL: URL + let userIndicatorController: UserIndicatorControllerProtocol } enum ServerConfirmationScreenCoordinatorAction { @@ -29,7 +31,9 @@ final class ServerConfirmationScreenCoordinator: CoordinatorProtocol { init(parameters: ServerConfirmationScreenCoordinatorParameters) { viewModel = ServerConfirmationScreenViewModel(authenticationService: parameters.authenticationService, - authenticationFlow: parameters.authenticationFlow) + authenticationFlow: parameters.authenticationFlow, + slidingSyncLearnMoreURL: parameters.slidingSyncLearnMoreURL, + userIndicatorController: parameters.userIndicatorController) } func start() { diff --git a/ElementX/Sources/Screens/Authentication/ServerConfirmationScreen/ServerConfirmationScreenModels.swift b/ElementX/Sources/Screens/Authentication/ServerConfirmationScreen/ServerConfirmationScreenModels.swift index d456b9ebaa..7a89f5c0b2 100644 --- a/ElementX/Sources/Screens/Authentication/ServerConfirmationScreen/ServerConfirmationScreenModels.swift +++ b/ElementX/Sources/Screens/Authentication/ServerConfirmationScreen/ServerConfirmationScreenModels.swift @@ -24,6 +24,8 @@ struct ServerConfirmationScreenViewState: BindableState { /// The presentation anchor used for OIDC authentication. var window: UIWindow? + var bindings = ServerConfirmationScreenBindings() + /// The screen's title. var title: String { switch authenticationFlow { @@ -46,23 +48,16 @@ struct ServerConfirmationScreenViewState: BindableState { "" } case .register: - if canContinue { - L10n.screenServerConfirmationMessageRegister - } else { - L10n.errorAccountCreationNotPossible - } - } - } - - /// Whether or not it is valid to continue the flow. - var canContinue: Bool { - switch authenticationFlow { - case .login: true - case .register: homeserverSupportsRegistration + L10n.screenServerConfirmationMessageRegister } } } +struct ServerConfirmationScreenBindings { + /// Information describing the currently displayed alert. + var alertInfo: AlertInfo? +} + enum ServerConfirmationScreenViewAction { /// Updates the window used as the OIDC presentation anchor. case updateWindow(UIWindow) @@ -71,3 +66,16 @@ enum ServerConfirmationScreenViewAction { /// The user would like to change to a different homeserver. case changeServer } + +enum ServerConfirmationScreenAlert: Hashable { + /// An alert that informs the user that a server could not be found. + case homeserverNotFound + /// An alert that informs the user about a bad well-known file. + case invalidWellKnown(String) + /// An alert that allows the user to learn about sliding sync. + case slidingSync + /// An alert that informs the user that registration isn't supported. + case registration + /// An unknown error has occurred. + case unknownError +} diff --git a/ElementX/Sources/Screens/Authentication/ServerConfirmationScreen/ServerConfirmationScreenViewModel.swift b/ElementX/Sources/Screens/Authentication/ServerConfirmationScreen/ServerConfirmationScreenViewModel.swift index 717c8f76fc..860939c7f6 100644 --- a/ElementX/Sources/Screens/Authentication/ServerConfirmationScreen/ServerConfirmationScreenViewModel.swift +++ b/ElementX/Sources/Screens/Authentication/ServerConfirmationScreen/ServerConfirmationScreenViewModel.swift @@ -11,15 +11,27 @@ import SwiftUI typealias ServerConfirmationScreenViewModelType = StateStoreViewModel class ServerConfirmationScreenViewModel: ServerConfirmationScreenViewModelType, ServerConfirmationScreenViewModelProtocol { + let authenticationService: AuthenticationServiceProtocol + let authenticationFlow: AuthenticationFlow + let slidingSyncLearnMoreURL: URL + let userIndicatorController: UserIndicatorControllerProtocol + private var actionsSubject: PassthroughSubject = .init() var actions: AnyPublisher { actionsSubject.eraseToAnyPublisher() } - init(authenticationService: AuthenticationServiceProtocol, authenticationFlow: AuthenticationFlow) { - let homeserver = authenticationService.homeserver.value + init(authenticationService: AuthenticationServiceProtocol, + authenticationFlow: AuthenticationFlow, + slidingSyncLearnMoreURL: URL, + userIndicatorController: UserIndicatorControllerProtocol) { + self.authenticationService = authenticationService + self.authenticationFlow = authenticationFlow + self.slidingSyncLearnMoreURL = slidingSyncLearnMoreURL + self.userIndicatorController = userIndicatorController + let homeserver = authenticationService.homeserver.value super.init(initialViewState: ServerConfirmationScreenViewState(homeserverAddress: homeserver.address, authenticationFlow: authenticationFlow, homeserverSupportsRegistration: homeserver.supportsRegistration)) @@ -34,23 +46,86 @@ class ServerConfirmationScreenViewModel: ServerConfirmationScreenViewModelType, .store(in: &cancellables) } - // MARK: - Public - override func process(viewAction: ServerConfirmationScreenViewAction) { switch viewAction { case .updateWindow(let window): guard state.window != window else { return } Task { state.window = window } case .confirm: - actionsSubject.send(.confirm) + Task { await configureAndContinue() } case .changeServer: actionsSubject.send(.changeServer) } } -} - -extension LoginHomeserver { - var supportsRegistration: Bool { - loginMode == .oidc || (address == "matrix.org" && registrationHelperURL != nil) + + // MARK: - Private + + private func configureAndContinue() async { + let homeserver = authenticationService.homeserver.value + + // If the login mode is unknown, the service hasn't be configured and we need to do it now. + // Otherwise we can continue the flow as server selection has been performed and succeeded. + guard homeserver.loginMode == .unknown || authenticationService.flow != authenticationFlow else { + // TODO: [DOUG] Test this. + actionsSubject.send(.confirm) + return + } + + startLoading() + defer { stopLoading() } + + switch await authenticationService.configure(for: homeserver.address, flow: authenticationFlow) { + case .success: + actionsSubject.send(.confirm) + case .failure(let error): + switch error { + case .invalidServer, .invalidHomeserverAddress: + displayError(.homeserverNotFound) + case .invalidWellKnown(let error): + displayError(.invalidWellKnown(error)) + case .slidingSyncNotAvailable: + displayError(.slidingSync) + case .registrationNotSupported: + displayError(.registration) // TODO: [DOUG] Test me! + default: + displayError(.unknownError) + } + } + } + + private func startLoading(label: String = L10n.commonLoading) { + userIndicatorController.submitIndicator(UserIndicator(type: .modal, + title: label, + persistent: true)) + } + + private func stopLoading() { + userIndicatorController.retractAllIndicators() + } + + private func displayError(_ type: ServerConfirmationScreenAlert) { + switch type { + case .homeserverNotFound: + state.bindings.alertInfo = AlertInfo(id: .homeserverNotFound, + title: L10n.errorUnknown, + message: L10n.screenChangeServerErrorInvalidHomeserver) + case .invalidWellKnown(let error): + state.bindings.alertInfo = AlertInfo(id: .invalidWellKnown(error), + title: L10n.commonServerNotSupported, + message: L10n.screenChangeServerErrorInvalidWellKnown(error)) + case .slidingSync: + let openURL = { UIApplication.shared.open(self.slidingSyncLearnMoreURL) } + state.bindings.alertInfo = AlertInfo(id: .slidingSync, + title: L10n.commonServerNotSupported, + message: L10n.screenChangeServerErrorNoSlidingSyncMessage, + primaryButton: .init(title: L10n.actionLearnMore, role: .cancel, action: openURL), + secondaryButton: .init(title: L10n.actionCancel, action: nil)) + case .registration: + state.bindings.alertInfo = AlertInfo(id: .registration, + title: L10n.errorUnknown, + message: L10n.errorAccountCreationNotPossible) + case .unknownError: + state.bindings.alertInfo = AlertInfo(id: .unknownError) + } } } diff --git a/ElementX/Sources/Screens/Authentication/ServerConfirmationScreen/View/ServerConfirmationScreen.swift b/ElementX/Sources/Screens/Authentication/ServerConfirmationScreen/View/ServerConfirmationScreen.swift index 78fbed4289..1ea19d1422 100644 --- a/ElementX/Sources/Screens/Authentication/ServerConfirmationScreen/View/ServerConfirmationScreen.swift +++ b/ElementX/Sources/Screens/Authentication/ServerConfirmationScreen/View/ServerConfirmationScreen.swift @@ -19,6 +19,7 @@ struct ServerConfirmationScreen: View { } .background() .backgroundStyle(.compound.bgCanvasDefault) + .alert(item: $context.alertInfo) .introspect(.window, on: .supportedVersions) { window in context.send(viewAction: .updateWindow(window)) } @@ -53,7 +54,6 @@ struct ServerConfirmationScreen: View { } .buttonStyle(.compound(.primary)) .accessibilityIdentifier(A11yIdentifiers.serverConfirmationScreen.continue) - .disabled(!context.viewState.canContinue) Button { context.send(viewAction: .changeServer) } label: { Text(L10n.screenServerConfirmationChangeServer) @@ -68,10 +68,8 @@ struct ServerConfirmationScreen: View { // MARK: - Previews struct ServerConfirmationScreen_Previews: PreviewProvider, TestablePreview { - static let loginViewModel = ServerConfirmationScreenViewModel(authenticationService: MockAuthenticationService(), - authenticationFlow: .login) - static let registerViewModel = ServerConfirmationScreenViewModel(authenticationService: MockAuthenticationService(), - authenticationFlow: .register) + static let loginViewModel = makeViewModel(flow: .login) + static let registerViewModel = makeViewModel(flow: .register) static var previews: some View { NavigationStack { @@ -86,4 +84,11 @@ struct ServerConfirmationScreen_Previews: PreviewProvider, TestablePreview { } .previewDisplayName("Register") } + + static func makeViewModel(flow: AuthenticationFlow) -> ServerConfirmationScreenViewModel { + ServerConfirmationScreenViewModel(authenticationService: MockAuthenticationService(), + authenticationFlow: flow, + slidingSyncLearnMoreURL: ServiceLocator.shared.settings.slidingSyncLearnMoreURL, + userIndicatorController: UserIndicatorControllerMock()) + } } diff --git a/ElementX/Sources/Screens/Authentication/ServerSelectionScreen/MockServerSelectionScreenState.swift b/ElementX/Sources/Screens/Authentication/ServerSelectionScreen/MockServerSelectionScreenState.swift index d97a7d37ea..c8393a4bdb 100644 --- a/ElementX/Sources/Screens/Authentication/ServerSelectionScreen/MockServerSelectionScreenState.swift +++ b/ElementX/Sources/Screens/Authentication/ServerSelectionScreen/MockServerSelectionScreenState.swift @@ -11,30 +11,22 @@ enum MockServerSelectionScreenState: CaseIterable { case matrix case emptyAddress case invalidAddress - case nonModal /// 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, - isModallyPresented: true) + slidingSyncLearnMoreURL: ServiceLocator.shared.settings.slidingSyncLearnMoreURL) case .emptyAddress: return ServerSelectionScreenViewModel(homeserverAddress: "", - slidingSyncLearnMoreURL: ServiceLocator.shared.settings.slidingSyncLearnMoreURL, - isModallyPresented: true) + slidingSyncLearnMoreURL: ServiceLocator.shared.settings.slidingSyncLearnMoreURL) case .invalidAddress: let viewModel = ServerSelectionScreenViewModel(homeserverAddress: "thisisbad", - slidingSyncLearnMoreURL: ServiceLocator.shared.settings.slidingSyncLearnMoreURL, - isModallyPresented: true) + slidingSyncLearnMoreURL: ServiceLocator.shared.settings.slidingSyncLearnMoreURL) viewModel.displayError(.footerMessage(L10n.errorUnknown)) return viewModel - case .nonModal: - return ServerSelectionScreenViewModel(homeserverAddress: "https://matrix.org", - slidingSyncLearnMoreURL: ServiceLocator.shared.settings.slidingSyncLearnMoreURL, - isModallyPresented: false) } } } diff --git a/ElementX/Sources/Screens/Authentication/ServerSelectionScreen/ServerSelectionScreenCoordinator.swift b/ElementX/Sources/Screens/Authentication/ServerSelectionScreen/ServerSelectionScreenCoordinator.swift index 6459f24a9e..71e78682e1 100644 --- a/ElementX/Sources/Screens/Authentication/ServerSelectionScreen/ServerSelectionScreenCoordinator.swift +++ b/ElementX/Sources/Screens/Authentication/ServerSelectionScreen/ServerSelectionScreenCoordinator.swift @@ -11,9 +11,9 @@ import SwiftUI struct ServerSelectionScreenCoordinatorParameters { /// The service used to authenticate the user. let authenticationService: AuthenticationServiceProtocol + let authenticationFlow: AuthenticationFlow + let slidingSyncLearnMoreURL: URL let userIndicatorController: UserIndicatorControllerProtocol - /// Whether the screen is presented modally or within a navigation stack. - let isModallyPresented: Bool } enum ServerSelectionScreenCoordinatorAction { @@ -38,8 +38,7 @@ final class ServerSelectionScreenCoordinator: CoordinatorProtocol { init(parameters: ServerSelectionScreenCoordinatorParameters) { self.parameters = parameters viewModel = ServerSelectionScreenViewModel(homeserverAddress: parameters.authenticationService.homeserver.value.address, - slidingSyncLearnMoreURL: ServiceLocator.shared.settings.slidingSyncLearnMoreURL, - isModallyPresented: parameters.isModallyPresented) + slidingSyncLearnMoreURL: parameters.slidingSyncLearnMoreURL) userIndicatorController = parameters.userIndicatorController } @@ -85,7 +84,7 @@ final class ServerSelectionScreenCoordinator: CoordinatorProtocol { startLoading() Task { - switch await authenticationService.configure(for: homeserverAddress) { + switch await authenticationService.configure(for: homeserverAddress, flow: parameters.authenticationFlow) { case .success: MXLog.info("Selected homeserver: \(homeserverAddress)") actionsSubject.send(.updated) @@ -107,6 +106,8 @@ final class ServerSelectionScreenCoordinator: CoordinatorProtocol { viewModel.displayError(.invalidWellKnownAlert(error)) case .slidingSyncNotAvailable: viewModel.displayError(.slidingSyncAlert) + case .registrationNotSupported: + viewModel.displayError(.registrationAlert) // TODO: [DOUG] Test me! default: viewModel.displayError(.footerMessage(L10n.errorUnknown)) } diff --git a/ElementX/Sources/Screens/Authentication/ServerSelectionScreen/ServerSelectionScreenModels.swift b/ElementX/Sources/Screens/Authentication/ServerSelectionScreen/ServerSelectionScreenModels.swift index 94eb3b09d8..ae11444bc8 100644 --- a/ElementX/Sources/Screens/Authentication/ServerSelectionScreen/ServerSelectionScreenModels.swift +++ b/ElementX/Sources/Screens/Authentication/ServerSelectionScreen/ServerSelectionScreenModels.swift @@ -22,19 +22,12 @@ struct ServerSelectionScreenViewState: BindableState { var bindings: ServerSelectionScreenBindings /// An error message to be shown in the text field footer. var footerErrorMessage: String? - /// Whether the screen is presented modally or within a navigation stack. - var isModallyPresented: Bool /// The message to show in the text field footer. var footerMessage: AttributedString { footerErrorMessage.map(AttributedString.init) ?? regularFooterMessage } - /// The title shown on the confirm button. - var buttonTitle: String { - isModallyPresented ? L10n.actionContinue : L10n.actionNext - } - /// The text field is showing an error. var isShowingFooterError: Bool { footerErrorMessage != nil @@ -45,10 +38,9 @@ struct ServerSelectionScreenViewState: BindableState { bindings.homeserverAddress.isEmpty || isShowingFooterError } - init(slidingSyncLearnMoreURL: URL, bindings: ServerSelectionScreenBindings, footerErrorMessage: String? = nil, isModallyPresented: Bool) { + init(slidingSyncLearnMoreURL: URL, bindings: ServerSelectionScreenBindings, footerErrorMessage: String? = nil) { self.bindings = bindings self.footerErrorMessage = footerErrorMessage - self.isModallyPresented = isModallyPresented let linkPlaceholder = "{link}" var message = AttributedString(L10n.screenChangeServerFormNotice(linkPlaceholder)) @@ -82,4 +74,6 @@ 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 registration isn't supported. + case registrationAlert } diff --git a/ElementX/Sources/Screens/Authentication/ServerSelectionScreen/ServerSelectionScreenViewModel.swift b/ElementX/Sources/Screens/Authentication/ServerSelectionScreen/ServerSelectionScreenViewModel.swift index 57d12c3dbb..fea0915420 100644 --- a/ElementX/Sources/Screens/Authentication/ServerSelectionScreen/ServerSelectionScreenViewModel.swift +++ b/ElementX/Sources/Screens/Authentication/ServerSelectionScreen/ServerSelectionScreenViewModel.swift @@ -19,13 +19,12 @@ class ServerSelectionScreenViewModel: ServerSelectionScreenViewModelType, Server actionsSubject.eraseToAnyPublisher() } - init(homeserverAddress: String, slidingSyncLearnMoreURL: URL, isModallyPresented: Bool) { + init(homeserverAddress: String, slidingSyncLearnMoreURL: URL) { self.slidingSyncLearnMoreURL = slidingSyncLearnMoreURL let bindings = ServerSelectionScreenBindings(homeserverAddress: homeserverAddress) super.init(initialViewState: ServerSelectionScreenViewState(slidingSyncLearnMoreURL: slidingSyncLearnMoreURL, - bindings: bindings, - isModallyPresented: isModallyPresented)) + bindings: bindings)) } override func process(viewAction: ServerSelectionScreenViewAction) { @@ -46,7 +45,7 @@ class ServerSelectionScreenViewModel: ServerSelectionScreenViewModelType, Server state.footerErrorMessage = message } case .invalidWellKnownAlert(let error): - state.bindings.alertInfo = AlertInfo(id: .slidingSyncAlert, + state.bindings.alertInfo = AlertInfo(id: .invalidWellKnownAlert(error), title: L10n.commonServerNotSupported, message: L10n.screenChangeServerErrorInvalidWellKnown(error)) case .slidingSyncAlert: @@ -56,6 +55,10 @@ class ServerSelectionScreenViewModel: ServerSelectionScreenViewModelType, Server message: L10n.screenChangeServerErrorNoSlidingSyncMessage, primaryButton: .init(title: L10n.actionLearnMore, role: .cancel, action: openURL), secondaryButton: .init(title: L10n.actionCancel, action: nil)) + case .registrationAlert: + state.bindings.alertInfo = AlertInfo(id: .registrationAlert, + title: L10n.errorUnknown, + message: L10n.errorAccountCreationNotPossible) } } diff --git a/ElementX/Sources/Screens/Authentication/ServerSelectionScreen/View/ServerSelectionScreen.swift b/ElementX/Sources/Screens/Authentication/ServerSelectionScreen/View/ServerSelectionScreen.swift index b166aabc67..03bb915504 100644 --- a/ElementX/Sources/Screens/Authentication/ServerSelectionScreen/View/ServerSelectionScreen.swift +++ b/ElementX/Sources/Screens/Authentication/ServerSelectionScreen/View/ServerSelectionScreen.swift @@ -64,7 +64,7 @@ struct ServerSelectionScreen: View { .onSubmit(submit) Button(action: submit) { - Text(context.viewState.buttonTitle) + Text(L10n.actionContinue) } .buttonStyle(.compound(.primary)) .disabled(context.viewState.hasValidationError) @@ -72,15 +72,12 @@ struct ServerSelectionScreen: View { } } - @ToolbarContentBuilder var toolbar: some ToolbarContent { ToolbarItem(placement: .cancellationAction) { - if context.viewState.isModallyPresented { - Button { context.send(viewAction: .dismiss) } label: { - Text(L10n.actionCancel) - } - .accessibilityIdentifier(A11yIdentifiers.changeServerScreen.dismiss) + Button { context.send(viewAction: .dismiss) } label: { + Text(L10n.actionCancel) } + .accessibilityIdentifier(A11yIdentifiers.changeServerScreen.dismiss) } } diff --git a/ElementX/Sources/Services/Authentication/AuthenticationService.swift b/ElementX/Sources/Services/Authentication/AuthenticationService.swift index c799a8f313..18f9a1d550 100644 --- a/ElementX/Sources/Services/Authentication/AuthenticationService.swift +++ b/ElementX/Sources/Services/Authentication/AuthenticationService.swift @@ -20,6 +20,7 @@ class AuthenticationService: AuthenticationServiceProtocol { private let homeserverSubject: CurrentValueSubject var homeserver: CurrentValuePublisher { homeserverSubject.asCurrentValuePublisher() } + private(set) var flow: AuthenticationFlow init(userSessionStore: UserSessionStoreProtocol, encryptionKeyProvider: EncryptionKeyProviderProtocol, appSettings: AppSettings, appHooks: AppHooks) { sessionDirectories = .init() @@ -28,13 +29,14 @@ class AuthenticationService: AuthenticationServiceProtocol { self.appSettings = appSettings self.appHooks = appHooks - homeserverSubject = .init(LoginHomeserver(address: appSettings.defaultHomeserverAddress, - loginMode: .unknown)) + // When updating these, don't forget to update the reset method too. + homeserverSubject = .init(LoginHomeserver(address: appSettings.defaultHomeserverAddress, loginMode: .unknown)) + flow = .login } // MARK: - Public - func configure(for homeserverAddress: String) async -> Result { + func configure(for homeserverAddress: String, flow: AuthenticationFlow) async -> Result { do { var homeserver = LoginHomeserver(address: homeserverAddress, loginMode: .unknown) @@ -57,7 +59,12 @@ class AuthenticationService: AuthenticationServiceProtocol { case .failure: nil } + if flow == .register, !homeserver.supportsRegistration { + return .failure(.registrationNotSupported) + } + self.client = client + self.flow = flow homeserverSubject.send(homeserver) return .success(()) } catch ClientBuildError.WellKnownDeserializationError(let error) { @@ -150,6 +157,12 @@ class AuthenticationService: AuthenticationServiceProtocol { } } + func reset() { + homeserverSubject.send(LoginHomeserver(address: appSettings.defaultHomeserverAddress, loginMode: .unknown)) + flow = .login + client = nil + } + // MARK: - Private private func makeClientBuilder() -> AuthenticationClientBuilder { diff --git a/ElementX/Sources/Services/Authentication/AuthenticationServiceProtocol.swift b/ElementX/Sources/Services/Authentication/AuthenticationServiceProtocol.swift index e347bc7b98..0daa77088c 100644 --- a/ElementX/Sources/Services/Authentication/AuthenticationServiceProtocol.swift +++ b/ElementX/Sources/Services/Authentication/AuthenticationServiceProtocol.swift @@ -24,6 +24,7 @@ enum AuthenticationServiceError: Error { case invalidHomeserverAddress case invalidWellKnown(String) case slidingSyncNotAvailable + case registrationNotSupported case accountDeactivated case failedLoggingIn case sessionTokenRefreshNotSupported @@ -33,9 +34,11 @@ enum AuthenticationServiceError: Error { protocol AuthenticationServiceProtocol { /// The currently configured homeserver. var homeserver: CurrentValuePublisher { get } + /// The type of flow the service is currently configured with. + var flow: AuthenticationFlow { get } /// Sets up the service for login on the specified homeserver address. - func configure(for homeserverAddress: String) async -> Result + func configure(for homeserverAddress: String, flow: AuthenticationFlow) async -> Result /// Performs login using OIDC for the current homeserver. func urlForOIDCLogin() async -> Result /// Asks the SDK to abort an ongoing OIDC login if we didn't get a callback to complete the request with. @@ -46,6 +49,9 @@ protocol AuthenticationServiceProtocol { func login(username: String, password: String, initialDeviceName: String?, deviceID: String?) async -> Result /// Completes registration using the credentials obtained via the helper URL. func completeWebRegistration(using credentials: WebRegistrationCredentials) async -> Result + + /// Resets the current configuration requiring `configure(for:flow:)` to be called again. + func reset() } // MARK: - OIDC diff --git a/ElementX/Sources/Services/Authentication/MockAuthenticationService.swift b/ElementX/Sources/Services/Authentication/MockAuthenticationService.swift index 3cd72631ae..8e6b4ffb49 100644 --- a/ElementX/Sources/Services/Authentication/MockAuthenticationService.swift +++ b/ElementX/Sources/Services/Authentication/MockAuthenticationService.swift @@ -14,23 +14,29 @@ class MockAuthenticationService: AuthenticationServiceProtocol { private let homeserverSubject: CurrentValueSubject var homeserver: CurrentValuePublisher { homeserverSubject.asCurrentValuePublisher() } + private(set) var flow: AuthenticationFlow = .login init(homeserver: LoginHomeserver = .mockMatrixDotOrg) { homeserverSubject = .init(homeserver) } - func configure(for homeserverAddress: String) async -> Result { + func configure(for homeserverAddress: String, flow: AuthenticationFlow) async -> Result { + #warning("[DOUG] Handle flow (or lets mock the real service?)") // Map the address to the mock homeservers if LoginHomeserver.mockMatrixDotOrg.address.contains(homeserverAddress) { + self.flow = flow homeserverSubject.send(.mockMatrixDotOrg) return .success(()) } else if LoginHomeserver.mockOIDC.address.contains(homeserverAddress) { + self.flow = flow homeserverSubject.send(.mockOIDC) return .success(()) } else if LoginHomeserver.mockBasicServer.address.contains(homeserverAddress) { + self.flow = flow homeserverSubject.send(.mockBasicServer) return .success(()) } else if LoginHomeserver.mockUnsupported.address.contains(homeserverAddress) { + self.flow = flow homeserverSubject.send(.mockUnsupported) return .success(()) } else { @@ -62,4 +68,8 @@ class MockAuthenticationService: AuthenticationServiceProtocol { func completeWebRegistration(using credentials: WebRegistrationCredentials) async -> Result { .failure(.failedLoggingIn) } + + func reset() { + fatalError("Not mocked") + } } diff --git a/ElementX/Sources/UITests/UITestsAppCoordinator.swift b/ElementX/Sources/UITests/UITestsAppCoordinator.swift index ac29d717a4..8fc7b05f8c 100644 --- a/ElementX/Sources/UITests/UITestsAppCoordinator.swift +++ b/ElementX/Sources/UITests/UITestsAppCoordinator.swift @@ -119,8 +119,9 @@ class MockScreen: Identifiable { case .serverSelection: let navigationStackCoordinator = NavigationStackCoordinator() let coordinator = ServerSelectionScreenCoordinator(parameters: .init(authenticationService: MockAuthenticationService(), - userIndicatorController: ServiceLocator.shared.userIndicatorController, - isModallyPresented: true)) + authenticationFlow: .login, + slidingSyncLearnMoreURL: ServiceLocator.shared.settings.slidingSyncLearnMoreURL, + userIndicatorController: ServiceLocator.shared.userIndicatorController)) navigationStackCoordinator.setRootCoordinator(coordinator) return navigationStackCoordinator case .authenticationFlow: diff --git a/UnitTests/Sources/ServerSelectionViewModelTests.swift b/UnitTests/Sources/ServerSelectionViewModelTests.swift index b6aafa1a6d..7808b62249 100644 --- a/UnitTests/Sources/ServerSelectionViewModelTests.swift +++ b/UnitTests/Sources/ServerSelectionViewModelTests.swift @@ -16,8 +16,7 @@ class ServerSelectionViewModelTests: XCTestCase { @MainActor override func setUp() { viewModel = ServerSelectionScreenViewModel(homeserverAddress: "", - slidingSyncLearnMoreURL: ServiceLocator.shared.settings.slidingSyncLearnMoreURL, - isModallyPresented: true) + slidingSyncLearnMoreURL: ServiceLocator.shared.settings.slidingSyncLearnMoreURL) context = viewModel.context } From d4760dc86ff412af115d7c9ecda6d9c0d5ebe262 Mon Sep 17 00:00:00 2001 From: Doug Date: Fri, 20 Sep 2024 13:38:14 +0100 Subject: [PATCH 2/5] Setup the infrastructure to test AuthenticationService. Implement basic tests for configuration & password login. --- ElementX.xcodeproj/project.pbxproj | 40 +- ...thenticationClientBuilderFactoryMock.swift | 22 + .../AuthenticationClientBuilderMock.swift | 22 + .../Mocks/Generated/GeneratedMocks.swift | 489 ++++++++++++++++++ .../Sources/Mocks/SDK/ClientSDKMock.swift | 59 +++ .../{ => SDK}/EventTimelineItemSDKMock.swift | 0 .../Sources/Mocks/UserSessionStoreMock.swift | 17 + .../AuthenticationClientBuilder.swift | 14 +- .../AuthenticationClientBuilderFactory.swift | 33 ++ .../AuthenticationService.swift | 24 +- .../AuthenticationServiceProtocol.swift | 2 +- .../Services/QRCode/QRCodeLoginService.swift | 2 +- .../UserSession/UserSessionStore.swift | 4 +- .../UserSessionStoreProtocol.swift | 3 +- .../Sources/AuthenticationServiceTests.swift | 92 ++++ 15 files changed, 802 insertions(+), 21 deletions(-) create mode 100644 ElementX/Sources/Mocks/AuthenticationClientBuilderFactoryMock.swift create mode 100644 ElementX/Sources/Mocks/AuthenticationClientBuilderMock.swift create mode 100644 ElementX/Sources/Mocks/SDK/ClientSDKMock.swift rename ElementX/Sources/Mocks/{ => SDK}/EventTimelineItemSDKMock.swift (100%) create mode 100644 ElementX/Sources/Mocks/UserSessionStoreMock.swift create mode 100644 ElementX/Sources/Services/Authentication/AuthenticationClientBuilderFactory.swift create mode 100644 UnitTests/Sources/AuthenticationServiceTests.swift diff --git a/ElementX.xcodeproj/project.pbxproj b/ElementX.xcodeproj/project.pbxproj index b29471d756..1893cab8e0 100644 --- a/ElementX.xcodeproj/project.pbxproj +++ b/ElementX.xcodeproj/project.pbxproj @@ -83,6 +83,7 @@ 0EA6537A07E2DC882AEA5962 /* Localizable.stringsdict in Resources */ = {isa = PBXBuildFile; fileRef = 187853A7E643995EE49FAD43 /* Localizable.stringsdict */; }; 0EE5EBA18BA1FE10254BB489 /* UIFont+AttributedStringBuilder.m in Sources */ = {isa = PBXBuildFile; fileRef = E8CA187FE656EE5A3F6C7DE5 /* UIFont+AttributedStringBuilder.m */; }; 0EEC614342F823E5BF966C2C /* AppLockTimerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A5B4CD611DE7E94F5BA87B2 /* AppLockTimerTests.swift */; }; + 0F4709282FCCFBEFED427B8A /* AuthenticationClientBuilderMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4760CE2128FBC217304272AB /* AuthenticationClientBuilderMock.swift */; }; 0F6C8033FA60CFD36F7CA205 /* AppLockSetupPINScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = A019A12C866D64CF072024B9 /* AppLockSetupPINScreenViewModel.swift */; }; 108D3C0707A90B0F848CDBB9 /* ResolveVerifiedUserSendFailureScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 60011EF0086E49DBD78E16E5 /* ResolveVerifiedUserSendFailureScreenModels.swift */; }; 109AEB7D33C4497727AFB87F /* TimelineInteractionHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2BA894BC09972DC45E497D37 /* TimelineInteractionHandler.swift */; }; @@ -150,6 +151,7 @@ 206F0DBAB6AF042CA1FF2C0D /* SettingsViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D487C1185D658F8B15B8F55 /* SettingsViewModelTests.swift */; }; 208C19811613F9A10F8A7B75 /* MediaLoader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8AFCE895ECFFA53FEE64D62B /* MediaLoader.swift */; }; 20C16A3F718802B0E4A19C83 /* URLComponentsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 76310030C831D4610A705603 /* URLComponentsTests.swift */; }; + 210DB40676DF2A23E69C2D06 /* AuthenticationClientBuilderFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = B655A536341D2695158C6664 /* AuthenticationClientBuilderFactory.swift */; }; 2118E35D312951B241067BD5 /* MessageComposerTextField.swift in Sources */ = {isa = PBXBuildFile; fileRef = 345172AD4377E83A44BD864F /* MessageComposerTextField.swift */; }; 211B5F524E851178EE549417 /* CurrentValuePublisher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 127C8472672A5BA09EF1ACF8 /* CurrentValuePublisher.swift */; }; 21813AF91CFC6F3E3896DB53 /* AppLockSetupBiometricsScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 10F130DF775CE6BC51A4E392 /* AppLockSetupBiometricsScreenModels.swift */; }; @@ -290,6 +292,7 @@ 407DCE030E0F9B7C9861D38A /* LRUCache in Frameworks */ = {isa = PBXBuildFile; productRef = 1081D3630AAD3ACEDDEC3A98 /* LRUCache */; }; 40B79D20A873620F7F128A2C /* UserPreference.swift in Sources */ = {isa = PBXBuildFile; fileRef = 35FA991289149D31F4286747 /* UserPreference.swift */; }; 414F50CFCFEEE2611127DCFB /* RestorationToken.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3558A15CFB934F9229301527 /* RestorationToken.swift */; }; + 41C5DA0C06F30311A221E85B /* ClientSDKMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8EAF4A49F3ACD8BB8B0D2371 /* ClientSDKMock.swift */; }; 41CE5E1289C8768FC5B6490C /* RoomTimelineItemViewState.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0C2D52E36AD614B3C003EF6 /* RoomTimelineItemViewState.swift */; }; 41DFDD212D1BE57CA50D783B /* KZFileWatchers in Frameworks */ = {isa = PBXBuildFile; productRef = 81DB3AB6CE996AB3954F4F03 /* KZFileWatchers */; }; 41F553349AF44567184822D8 /* APNSPayload.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94D670124FC3E84F23A62CCF /* APNSPayload.swift */; }; @@ -405,6 +408,7 @@ 5AE6404C4FD4848ACCFF9EDC /* SecureBackupLogoutConfirmationScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1573D28C8A9FB6399D0EEFB /* SecureBackupLogoutConfirmationScreenCoordinator.swift */; }; 5B6E5AD224509E6C0B520D6E /* RoomMemberDetailsScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7DDF49CEBC0DFC59C308335F /* RoomMemberDetailsScreenViewModelProtocol.swift */; }; 5B7D24A318AFF75AD611A026 /* RoomDirectorySearchScreenScreenViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE6BFF453838CF6C3982C5A3 /* RoomDirectorySearchScreenScreenViewModelTests.swift */; }; + 5BBDF9926CB645DE2F7BC258 /* EventTimelineItemSDKMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = B86D447D771CEF6194348F5F /* EventTimelineItemSDKMock.swift */; }; 5BC6C4ADFE7F2A795ECDE130 /* SecureBackupKeyBackupScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B2D4EEBE8C098BBADD10939 /* SecureBackupKeyBackupScreenCoordinator.swift */; }; 5C02841B2A86327B2C377682 /* NotificationConstants.swift in Sources */ = {isa = PBXBuildFile; fileRef = C830A64609CBD152F06E0457 /* NotificationConstants.swift */; }; 5C164551F7D26E24F09083D3 /* StaticLocationScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = C616D90B1E2F033CAA325439 /* StaticLocationScreenViewModelProtocol.swift */; }; @@ -541,6 +545,7 @@ 79741C1953269FF1A211D246 /* RoomPollsHistoryScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0E14FF533D25A0692F7CEB0 /* RoomPollsHistoryScreenViewModel.swift */; }; 798BF3072137833FBD3F4C96 /* TimelineDeliveryStatusView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 66F91544AC136BF6477BDAB8 /* TimelineDeliveryStatusView.swift */; }; 79959F8E45C3749997482A7F /* TimelineItemBubbledStylerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A459AE4B6566B2FA99E86B2 /* TimelineItemBubbledStylerView.swift */; }; + 79D57E9AE03A2DC689D14EA2 /* UserSessionStoreMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EB9BA2F30EB8C33226D8FF1 /* UserSessionStoreMock.swift */; }; 7A02EB29F3B993AB20E0A198 /* RoomPollsHistoryScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 42C8C368A611B9CB79C7F5FA /* RoomPollsHistoryScreen.swift */; }; 7A0D335D38ECA095A575B4F7 /* TimelineStyler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DB0E533508094156D8024C3 /* TimelineStyler.swift */; }; 7A170A5A4A352954BB2A1B96 /* AuthenticationStartScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 24E8C8817F59BEC7E358EB78 /* AuthenticationStartScreen.swift */; }; @@ -655,6 +660,7 @@ 915B4CDAF220D9AEB4047D45 /* PollInteractionHandlerProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 259E5B05BDE6E20C26CF11B4 /* PollInteractionHandlerProtocol.swift */; }; 91ABC91758A6E4A5FAA2E9C4 /* ReadReceipt.swift in Sources */ = {isa = PBXBuildFile; fileRef = 314F1C79850BE46E8ABEAFCB /* ReadReceipt.swift */; }; 91C6AC0E9D2B9C0C76CC6AD4 /* RoomDirectorySearchScreenScreenModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3984C93B8E9B10C92DADF9EE /* RoomDirectorySearchScreenScreenModelProtocol.swift */; }; + 92012C96039BC8C2CAEBA9E2 /* AuthenticationServiceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 671C338B7259DC5774816885 /* AuthenticationServiceTests.swift */; }; 9219640F4D980CFC5FE855AD /* target.yml in Resources */ = {isa = PBXBuildFile; fileRef = 536E72DCBEEC4A1FE66CFDCE /* target.yml */; }; 92720AB0DA9AB5EEF1DAF56B /* SecureBackupLogoutConfirmationScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7DC017C3CB6B0F7C63F460F2 /* SecureBackupLogoutConfirmationScreenViewModel.swift */; }; 9278EC51D24E57445B290521 /* AudioSessionProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = BB284643AF7AB131E307DCE0 /* AudioSessionProtocol.swift */; }; @@ -749,6 +755,7 @@ A4B123C635F70DDD4BC2FAC9 /* BlockedUsersScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = E76A706B3EEA32B882DA5E2D /* BlockedUsersScreenViewModelProtocol.swift */; }; A4C29D373986AFE4559696D5 /* SecureBackupKeyBackupScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4525E8C0FBDD27D1ACE90952 /* SecureBackupKeyBackupScreenViewModelProtocol.swift */; }; A4E885358D7DD5A072A06824 /* PostHog in Frameworks */ = {isa = PBXBuildFile; productRef = CCE5BF78B125320CBF3BB834 /* PostHog */; }; + A51C65E5A3C9F2464A91A380 /* AuthenticationClientBuilderFactoryMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0554FEA301486A8CFA475D5A /* AuthenticationClientBuilderFactoryMock.swift */; }; A52090A4FE0DB826578DFC03 /* Client.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0724EBDFE8BB4C9E5547C57D /* Client.swift */; }; A5B9EF45C7B8ACEB4954AE36 /* LoginScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9780389F8A53E4D26E23DD03 /* LoginScreenViewModelProtocol.swift */; }; A5D551E5691749066E0E0C44 /* RoomDetailsScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 837B440C4705E4B899BCB899 /* RoomDetailsScreenViewModel.swift */; }; @@ -1054,7 +1061,6 @@ EDF8919F15DE0FF00EF99E70 /* DocumentPicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0F5567A7EF6F2AB9473236F6 /* DocumentPicker.swift */; }; EE4E2C1922BBF5169E213555 /* PillAttachmentViewProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1B53D6C5C0D14B04D3AB3F6E /* PillAttachmentViewProvider.swift */; }; EE56238683BC3ECA9BA00684 /* GlobalSearchScreenViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA4D639E27D5882A6A71AECF /* GlobalSearchScreenViewModelTests.swift */; }; - EE57A96130DD8DB053790AE2 /* EventTimelineItemSDKMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1C7A6BBC686B1F840FA807FB /* EventTimelineItemSDKMock.swift */; }; EE8491AD81F47DF3C192497B /* DecorationTimelineItemProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 184CF8C196BE143AE226628D /* DecorationTimelineItemProtocol.swift */; }; EEAE954289DE813A61656AE0 /* LayoutDirection.swift in Sources */ = {isa = PBXBuildFile; fileRef = C14D83B2B7CD5501A0089EFC /* LayoutDirection.swift */; }; EEB9C1555C63B93CA9C372C2 /* EmojiPickerScreenHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6B5E29E9A22F45534FBD5B58 /* EmojiPickerScreenHeaderView.swift */; }; @@ -1219,6 +1225,7 @@ 052B2F924572AFD70B5F500E /* StartChatScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StartChatScreenViewModel.swift; sourceTree = ""; }; 054F469E433864CC6FE6EE8E /* ServerSelectionUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerSelectionUITests.swift; sourceTree = ""; }; 05512FB13987D221B7205DE0 /* HomeScreenRecoveryKeyConfirmationBanner.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeScreenRecoveryKeyConfirmationBanner.swift; sourceTree = ""; }; + 0554FEA301486A8CFA475D5A /* AuthenticationClientBuilderFactoryMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthenticationClientBuilderFactoryMock.swift; sourceTree = ""; }; 05596E4A11A8C9346E9E54AE /* SoftLogoutScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SoftLogoutScreenCoordinator.swift; sourceTree = ""; }; 05A3E8741D199CD1A37F4CBF /* UIView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIView.swift; sourceTree = ""; }; 05AF58372CA884A789EB9C5A /* AppMediatorProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppMediatorProtocol.swift; sourceTree = ""; }; @@ -1311,7 +1318,6 @@ 1BA5A62DA4B543827FF82354 /* LAContextMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LAContextMock.swift; sourceTree = ""; }; 1C21A715237F2B6D6E80998C /* SecureBackupControllerProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecureBackupControllerProtocol.swift; sourceTree = ""; }; 1C25B6EBEB414431187D73B7 /* TimelineReplyView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineReplyView.swift; sourceTree = ""; }; - 1C7A6BBC686B1F840FA807FB /* EventTimelineItemSDKMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventTimelineItemSDKMock.swift; sourceTree = ""; }; 1C7F63EB1525E697CAEB002B /* BlankFormCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlankFormCoordinator.swift; sourceTree = ""; }; 1CC575D1895FA62591451A93 /* RoomMemberDetailsScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomMemberDetailsScreen.swift; sourceTree = ""; }; 1CD7C0A2750998C2D77AD00F /* JoinRoomScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JoinRoomScreenViewModel.swift; sourceTree = ""; }; @@ -1519,6 +1525,7 @@ 471BB7276C97AF60B3A5463B /* RoomDirectorySearchProxy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomDirectorySearchProxy.swift; sourceTree = ""; }; 475D47D0BFE961B02BAC5D49 /* id */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = id; path = id.lproj/Localizable.stringsdict; sourceTree = ""; }; 475EB595D7527E9A8A14043E /* uz */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = uz; path = uz.lproj/Localizable.strings; sourceTree = ""; }; + 4760CE2128FBC217304272AB /* AuthenticationClientBuilderMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthenticationClientBuilderMock.swift; sourceTree = ""; }; 47873756E45B46683D97DC32 /* LegalInformationScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LegalInformationScreenModels.swift; sourceTree = ""; }; 47EBB5D698CE9A25BB553A2D /* Strings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Strings.swift; sourceTree = ""; }; 47F29139BC2A804CE5E0757E /* MediaUploadPreviewScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaUploadPreviewScreenViewModel.swift; sourceTree = ""; }; @@ -1646,6 +1653,7 @@ 66AFD800AF033D8B0D11191A /* UserPropertiesExt.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserPropertiesExt.swift; sourceTree = ""; }; 66F2402D738694F98729A441 /* RoomTimelineProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomTimelineProvider.swift; sourceTree = ""; }; 66F91544AC136BF6477BDAB8 /* TimelineDeliveryStatusView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineDeliveryStatusView.swift; sourceTree = ""; }; + 671C338B7259DC5774816885 /* AuthenticationServiceTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthenticationServiceTests.swift; sourceTree = ""; }; 6722709BD6178E10B70C9641 /* es */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = es; path = es.lproj/SAS.strings; sourceTree = ""; }; 68010886142843705E342645 /* ProgressMaskModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProgressMaskModifier.swift; sourceTree = ""; }; 6861FE915C7B5466E6962BBA /* StartChatScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StartChatScreen.swift; sourceTree = ""; }; @@ -1805,6 +1813,7 @@ 8DA1E8F287680C8ED25EDBAC /* NetworkMonitorMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkMonitorMock.swift; sourceTree = ""; }; 8E088F2A1B9EC529D3221931 /* UITests.xctestplan */ = {isa = PBXFileReference; path = UITests.xctestplan; sourceTree = ""; }; 8E1584F8BCF407BB94F48F04 /* EncryptionResetPasswordScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EncryptionResetPasswordScreen.swift; sourceTree = ""; }; + 8EAF4A49F3ACD8BB8B0D2371 /* ClientSDKMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClientSDKMock.swift; sourceTree = ""; }; 8F21ED7205048668BEB44A38 /* AppActivityView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppActivityView.swift; sourceTree = ""; }; 8F6210134203BE1F2DD5C679 /* RoomDirectoryCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomDirectoryCell.swift; sourceTree = ""; }; 8F841F219ACDFC1D3F42FEFB /* RoomChangeRolesScreenViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomChangeRolesScreenViewModelTests.swift; sourceTree = ""; }; @@ -1871,6 +1880,7 @@ 9CE3C90E487B255B735D73C8 /* RoomScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomScreenViewModel.swift; sourceTree = ""; }; 9CF1EE0AA78470C674554262 /* PillTextAttachment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PillTextAttachment.swift; sourceTree = ""; }; 9E6D88E8AFFBF2C1D589C0FA /* UIConstants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIConstants.swift; sourceTree = ""; }; + 9EB9BA2F30EB8C33226D8FF1 /* UserSessionStoreMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserSessionStoreMock.swift; sourceTree = ""; }; 9ECF11669EF253E98AA2977A /* CompletionSuggestionServiceProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CompletionSuggestionServiceProtocol.swift; sourceTree = ""; }; 9F1DF3FFFE5ED2B8133F43A7 /* MessageComposer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageComposer.swift; sourceTree = ""; }; 9F40FB0A43DAECEC27C73722 /* bg */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = bg; path = bg.lproj/SAS.strings; sourceTree = ""; }; @@ -1981,6 +1991,7 @@ B61C339A2FDDBD067FF6635C /* ConfettiScene.scn */ = {isa = PBXFileReference; path = ConfettiScene.scn; sourceTree = ""; }; B63B69F9A2BC74DD40DC75C8 /* AdvancedSettingsScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdvancedSettingsScreenViewModel.swift; sourceTree = ""; }; B6404166CBF5CC88673FF9E2 /* RoomDetails.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomDetails.swift; sourceTree = ""; }; + B655A536341D2695158C6664 /* AuthenticationClientBuilderFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthenticationClientBuilderFactory.swift; sourceTree = ""; }; B6E4AB573FAEBB7B853DD04C /* AppHooks.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppHooks.swift; sourceTree = ""; }; B6E89E530A8E92EC44301CA1 /* Bundle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Bundle.swift; sourceTree = ""; }; B70A50C41C5871B4DB905E7E /* VoiceMessageRoomTimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VoiceMessageRoomTimelineView.swift; sourceTree = ""; }; @@ -1994,6 +2005,7 @@ B81B6170DB690013CEB646F4 /* MapLibreModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapLibreModels.swift; sourceTree = ""; }; B8516302ACCA94A0E680AB3B /* VoiceMessageButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VoiceMessageButton.swift; sourceTree = ""; }; B858A61F2A570DFB8DE570A7 /* AggregratedReaction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AggregratedReaction.swift; sourceTree = ""; }; + B86D447D771CEF6194348F5F /* EventTimelineItemSDKMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventTimelineItemSDKMock.swift; sourceTree = ""; }; B8A3B7637DDBD6AA97AC2545 /* CameraPicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CameraPicker.swift; sourceTree = ""; }; B8F28602AC7AC881AED37EBA /* NavigationCoordinators.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationCoordinators.swift; sourceTree = ""; }; B902EA6CD3296B0E10EE432B /* HomeScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeScreen.swift; sourceTree = ""; }; @@ -2887,10 +2899,11 @@ children = ( 69CB8242D69B7E4D0B32E18D /* AggregatedReactionMock.swift */, 3BAC027034248429A438886B /* AppMediatorMock.swift */, + 0554FEA301486A8CFA475D5A /* AuthenticationClientBuilderFactoryMock.swift */, + 4760CE2128FBC217304272AB /* AuthenticationClientBuilderMock.swift */, E2F96CCBEAAA7F2185BFA354 /* ClientProxyMock.swift */, 4E600B315B920B9687F8EE1B /* ComposerDraftServiceMock.swift */, E321E840DCC63790049984F4 /* ElementCallServiceMock.swift */, - 1C7A6BBC686B1F840FA807FB /* EventTimelineItemSDKMock.swift */, 867DC9530C42F7B5176BE465 /* JoinedRoomProxyMock.swift */, 8DA1E8F287680C8ED25EDBAC /* NetworkMonitorMock.swift */, 382B50F7E379B3DBBD174364 /* NotificationSettingsProxyMock.swift */, @@ -2907,7 +2920,9 @@ 7893780A1FD6E3F38B3E9049 /* UserIndicatorControllerMock.swift */, AAD01F7FC2BBAC7351948595 /* UserProfile+Mock.swift */, F4469F6AE311BDC439B3A5EC /* UserSessionMock.swift */, + 9EB9BA2F30EB8C33226D8FF1 /* UserSessionStoreMock.swift */, B23135B06B044CB811139D2F /* Generated */, + E5E545F92D01588360A9BAC5 /* SDK */, ); path = Mocks; sourceTree = ""; @@ -3790,6 +3805,7 @@ 89233612A8632AD7E2803620 /* AudioPlayerStateTests.swift */, C55CC239AE12339C565F6C9A /* AudioRecorderStateTests.swift */, 2441E2424E78A40FC95DBA76 /* AudioRecorderTests.swift */, + 671C338B7259DC5774816885 /* AuthenticationServiceTests.swift */, 8FB89DC7F9A4A91020037001 /* AuthenticationStartScreenViewModelTests.swift */, 93E1FF0DFBB3768F79FDBF6D /* AVMetadataMachineReadableCodeObjectExtensionsTest.swift */, 240610DF32F3213BEC5611D7 /* BlockedUsersScreenViewModelTests.swift */, @@ -4651,6 +4667,7 @@ isa = PBXGroup; children = ( 0F569CFB77E0D40BD82203D9 /* AuthenticationClientBuilder.swift */, + B655A536341D2695158C6664 /* AuthenticationClientBuilderFactory.swift */, F3A1AB5A84D843B6AC8D5F1E /* AuthenticationService.swift */, 5E75948AA1FE1D1A7809931F /* AuthenticationServiceProtocol.swift */, DA38899517F08FE2AF34EB45 /* MockAuthenticationService.swift */, @@ -5243,6 +5260,15 @@ path = Screens; sourceTree = ""; }; + E5E545F92D01588360A9BAC5 /* SDK */ = { + isa = PBXGroup; + children = ( + 8EAF4A49F3ACD8BB8B0D2371 /* ClientSDKMock.swift */, + B86D447D771CEF6194348F5F /* EventTimelineItemSDKMock.swift */, + ); + path = SDK; + sourceTree = ""; + }; E600AACDF87CDBCE32683236 /* Resources */ = { isa = PBXGroup; children = ( @@ -6072,6 +6098,7 @@ C1429699A6A5BB09A25775C1 /* AudioPlayerStateTests.swift in Sources */, 3042527CB344A9EF1157FC26 /* AudioRecorderStateTests.swift in Sources */, 192A3CDCD0174AD1E4A128E4 /* AudioRecorderTests.swift in Sources */, + 92012C96039BC8C2CAEBA9E2 /* AuthenticationServiceTests.swift in Sources */, 8ED8AF57A06F5EE9978ED23F /* AuthenticationStartScreenViewModelTests.swift in Sources */, CEAEA57B7665C8E790599A78 /* BlockedUsersScreenViewModelTests.swift in Sources */, 1B2F9F368619FFF8C63C87CC /* BugReportScreenViewModelTests.swift in Sources */, @@ -6278,6 +6305,9 @@ 7BD2123144A32F082CECC108 /* AudioRoomTimelineView.swift in Sources */, 9278EC51D24E57445B290521 /* AudioSessionProtocol.swift in Sources */, 8A6CB15C8FC68F557750BF54 /* AuthenticationClientBuilder.swift in Sources */, + 210DB40676DF2A23E69C2D06 /* AuthenticationClientBuilderFactory.swift in Sources */, + A51C65E5A3C9F2464A91A380 /* AuthenticationClientBuilderFactoryMock.swift in Sources */, + 0F4709282FCCFBEFED427B8A /* AuthenticationClientBuilderMock.swift in Sources */, 67E9926C4572C54F59FCA91A /* AuthenticationFlowCoordinator.swift in Sources */, 9847B056C1A216C314D21E68 /* AuthenticationService.swift in Sources */, 56DACDD379A86A1F5DEFE7BE /* AuthenticationServiceProtocol.swift in Sources */, @@ -6333,6 +6363,7 @@ 1950A80CD198BED283DFC2CE /* ClientProxy.swift in Sources */, DDFBDEE1DC32BDD5488F898C /* ClientProxyMock.swift in Sources */, 24BDDD09A90B8BFE3793F3AA /* ClientProxyProtocol.swift in Sources */, + 41C5DA0C06F30311A221E85B /* ClientSDKMock.swift in Sources */, 0C797CD650DFD2876BEC5173 /* CollapsibleReactionLayout.swift in Sources */, 78A3D84BA47DAC69B4D0A34C /* CollapsibleRoomTimelineView.swift in Sources */, 0DC815CA24E1BD7F408F37D3 /* CollapsibleTimelineItem.swift in Sources */, @@ -6423,7 +6454,7 @@ 50539366B408780B232C1910 /* EstimatedWaveformView.swift in Sources */, F78BAD28482A467287A9A5A3 /* EventBasedMessageTimelineItemProtocol.swift in Sources */, 02D8DF8EB7537EB4E9019DDB /* EventBasedTimelineItemProtocol.swift in Sources */, - EE57A96130DD8DB053790AE2 /* EventTimelineItemSDKMock.swift in Sources */, + 5BBDF9926CB645DE2F7BC258 /* EventTimelineItemSDKMock.swift in Sources */, 63E46D18B91D08E15FC04125 /* ExpiringTaskRunner.swift in Sources */, 5F06AD3C66884CE793AE6119 /* FileManager.swift in Sources */, D33AC79A50DFC26D2498DD28 /* FileRoomTimelineItem.swift in Sources */, @@ -6982,6 +7013,7 @@ 6586E1F1D5F0651D0638FFAF /* UserSessionMock.swift in Sources */, 978BB24F2A5D31EE59EEC249 /* UserSessionProtocol.swift in Sources */, 7E91BAC17963ED41208F489B /* UserSessionStore.swift in Sources */, + 79D57E9AE03A2DC689D14EA2 /* UserSessionStoreMock.swift in Sources */, AC69B6DF15FC451AB2945036 /* UserSessionStoreProtocol.swift in Sources */, F07D88421A9BC4D03D4A5055 /* VideoRoomTimelineItem.swift in Sources */, 1A83DD22F3E6F76B13B6E2F9 /* VideoRoomTimelineItemContent.swift in Sources */, diff --git a/ElementX/Sources/Mocks/AuthenticationClientBuilderFactoryMock.swift b/ElementX/Sources/Mocks/AuthenticationClientBuilderFactoryMock.swift new file mode 100644 index 0000000000..03e962e31c --- /dev/null +++ b/ElementX/Sources/Mocks/AuthenticationClientBuilderFactoryMock.swift @@ -0,0 +1,22 @@ +// +// Copyright 2024 New Vector Ltd. +// +// SPDX-License-Identifier: AGPL-3.0-only +// Please see LICENSE in the repository root for full details. +// + +import Foundation +import MatrixRustSDK + +extension AuthenticationClientBuilderFactoryMock { + struct Configuration { + var builtClient: ClientProtocol + } + + convenience init(configuration: Configuration) { + self.init() + + let clientBuilder = AuthenticationClientBuilderMock(configuration: .init(builtClient: configuration.builtClient)) + makeBuilderSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksReturnValue = clientBuilder + } +} diff --git a/ElementX/Sources/Mocks/AuthenticationClientBuilderMock.swift b/ElementX/Sources/Mocks/AuthenticationClientBuilderMock.swift new file mode 100644 index 0000000000..15969587a9 --- /dev/null +++ b/ElementX/Sources/Mocks/AuthenticationClientBuilderMock.swift @@ -0,0 +1,22 @@ +// +// Copyright 2024 New Vector Ltd. +// +// SPDX-License-Identifier: AGPL-3.0-only +// Please see LICENSE in the repository root for full details. +// + +import Foundation +import MatrixRustSDK + +struct AuthenticationClientBuilderMockConfiguration { + var builtClient: ClientProtocol +} + +extension AuthenticationClientBuilderMock { + convenience init(configuration: AuthenticationClientBuilderMockConfiguration) { + self.init() + + buildHomeserverAddressReturnValue = configuration.builtClient + buildWithQRCodeQrCodeDataOidcConfigurationProgressListenerReturnValue = configuration.builtClient + } +} diff --git a/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift b/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift index ba521052fb..1cc39fdd50 100644 --- a/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift +++ b/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift @@ -1819,6 +1819,230 @@ class AudioSessionMock: AudioSessionProtocol { try setActiveOptionsClosure?(active, options) } } +class AuthenticationClientBuilderFactoryMock: AuthenticationClientBuilderFactoryProtocol { + + //MARK: - makeBuilder + + var makeBuilderSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksUnderlyingCallsCount = 0 + var makeBuilderSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount: Int { + get { + if Thread.isMainThread { + return makeBuilderSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksUnderlyingCallsCount + } else { + var returnValue: Int? = nil + DispatchQueue.main.sync { + returnValue = makeBuilderSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksUnderlyingCallsCount + } + + return returnValue! + } + } + set { + if Thread.isMainThread { + makeBuilderSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksUnderlyingCallsCount = newValue + } else { + DispatchQueue.main.sync { + makeBuilderSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksUnderlyingCallsCount = newValue + } + } + } + } + var makeBuilderSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCalled: Bool { + return makeBuilderSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount > 0 + } + var makeBuilderSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksReceivedArguments: (sessionDirectories: SessionDirectories, passphrase: String, clientSessionDelegate: ClientSessionDelegate, appSettings: AppSettings, appHooks: AppHooks)? + var makeBuilderSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksReceivedInvocations: [(sessionDirectories: SessionDirectories, passphrase: String, clientSessionDelegate: ClientSessionDelegate, appSettings: AppSettings, appHooks: AppHooks)] = [] + + var makeBuilderSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksUnderlyingReturnValue: AuthenticationClientBuilderProtocol! + var makeBuilderSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksReturnValue: AuthenticationClientBuilderProtocol! { + get { + if Thread.isMainThread { + return makeBuilderSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksUnderlyingReturnValue + } else { + var returnValue: AuthenticationClientBuilderProtocol? = nil + DispatchQueue.main.sync { + returnValue = makeBuilderSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksUnderlyingReturnValue + } + + return returnValue! + } + } + set { + if Thread.isMainThread { + makeBuilderSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksUnderlyingReturnValue = newValue + } else { + DispatchQueue.main.sync { + makeBuilderSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksUnderlyingReturnValue = newValue + } + } + } + } + var makeBuilderSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksClosure: ((SessionDirectories, String, ClientSessionDelegate, AppSettings, AppHooks) -> AuthenticationClientBuilderProtocol)? + + func makeBuilder(sessionDirectories: SessionDirectories, passphrase: String, clientSessionDelegate: ClientSessionDelegate, appSettings: AppSettings, appHooks: AppHooks) -> AuthenticationClientBuilderProtocol { + makeBuilderSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount += 1 + makeBuilderSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksReceivedArguments = (sessionDirectories: sessionDirectories, passphrase: passphrase, clientSessionDelegate: clientSessionDelegate, appSettings: appSettings, appHooks: appHooks) + DispatchQueue.main.async { + self.makeBuilderSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksReceivedInvocations.append((sessionDirectories: sessionDirectories, passphrase: passphrase, clientSessionDelegate: clientSessionDelegate, appSettings: appSettings, appHooks: appHooks)) + } + if let makeBuilderSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksClosure = makeBuilderSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksClosure { + return makeBuilderSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksClosure(sessionDirectories, passphrase, clientSessionDelegate, appSettings, appHooks) + } else { + return makeBuilderSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksReturnValue + } + } +} +class AuthenticationClientBuilderMock: AuthenticationClientBuilderProtocol { + + //MARK: - build + + var buildHomeserverAddressThrowableError: Error? + var buildHomeserverAddressUnderlyingCallsCount = 0 + var buildHomeserverAddressCallsCount: Int { + get { + if Thread.isMainThread { + return buildHomeserverAddressUnderlyingCallsCount + } else { + var returnValue: Int? = nil + DispatchQueue.main.sync { + returnValue = buildHomeserverAddressUnderlyingCallsCount + } + + return returnValue! + } + } + set { + if Thread.isMainThread { + buildHomeserverAddressUnderlyingCallsCount = newValue + } else { + DispatchQueue.main.sync { + buildHomeserverAddressUnderlyingCallsCount = newValue + } + } + } + } + var buildHomeserverAddressCalled: Bool { + return buildHomeserverAddressCallsCount > 0 + } + var buildHomeserverAddressReceivedHomeserverAddress: String? + var buildHomeserverAddressReceivedInvocations: [String] = [] + + var buildHomeserverAddressUnderlyingReturnValue: ClientProtocol! + var buildHomeserverAddressReturnValue: ClientProtocol! { + get { + if Thread.isMainThread { + return buildHomeserverAddressUnderlyingReturnValue + } else { + var returnValue: ClientProtocol? = nil + DispatchQueue.main.sync { + returnValue = buildHomeserverAddressUnderlyingReturnValue + } + + return returnValue! + } + } + set { + if Thread.isMainThread { + buildHomeserverAddressUnderlyingReturnValue = newValue + } else { + DispatchQueue.main.sync { + buildHomeserverAddressUnderlyingReturnValue = newValue + } + } + } + } + var buildHomeserverAddressClosure: ((String) async throws -> ClientProtocol)? + + func build(homeserverAddress: String) async throws -> ClientProtocol { + if let error = buildHomeserverAddressThrowableError { + throw error + } + buildHomeserverAddressCallsCount += 1 + buildHomeserverAddressReceivedHomeserverAddress = homeserverAddress + DispatchQueue.main.async { + self.buildHomeserverAddressReceivedInvocations.append(homeserverAddress) + } + if let buildHomeserverAddressClosure = buildHomeserverAddressClosure { + return try await buildHomeserverAddressClosure(homeserverAddress) + } else { + return buildHomeserverAddressReturnValue + } + } + //MARK: - buildWithQRCode + + var buildWithQRCodeQrCodeDataOidcConfigurationProgressListenerThrowableError: Error? + var buildWithQRCodeQrCodeDataOidcConfigurationProgressListenerUnderlyingCallsCount = 0 + var buildWithQRCodeQrCodeDataOidcConfigurationProgressListenerCallsCount: Int { + get { + if Thread.isMainThread { + return buildWithQRCodeQrCodeDataOidcConfigurationProgressListenerUnderlyingCallsCount + } else { + var returnValue: Int? = nil + DispatchQueue.main.sync { + returnValue = buildWithQRCodeQrCodeDataOidcConfigurationProgressListenerUnderlyingCallsCount + } + + return returnValue! + } + } + set { + if Thread.isMainThread { + buildWithQRCodeQrCodeDataOidcConfigurationProgressListenerUnderlyingCallsCount = newValue + } else { + DispatchQueue.main.sync { + buildWithQRCodeQrCodeDataOidcConfigurationProgressListenerUnderlyingCallsCount = newValue + } + } + } + } + var buildWithQRCodeQrCodeDataOidcConfigurationProgressListenerCalled: Bool { + return buildWithQRCodeQrCodeDataOidcConfigurationProgressListenerCallsCount > 0 + } + var buildWithQRCodeQrCodeDataOidcConfigurationProgressListenerReceivedArguments: (qrCodeData: QrCodeData, oidcConfiguration: OIDCConfigurationProxy, progressListener: QrLoginProgressListenerProxy)? + var buildWithQRCodeQrCodeDataOidcConfigurationProgressListenerReceivedInvocations: [(qrCodeData: QrCodeData, oidcConfiguration: OIDCConfigurationProxy, progressListener: QrLoginProgressListenerProxy)] = [] + + var buildWithQRCodeQrCodeDataOidcConfigurationProgressListenerUnderlyingReturnValue: ClientProtocol! + var buildWithQRCodeQrCodeDataOidcConfigurationProgressListenerReturnValue: ClientProtocol! { + get { + if Thread.isMainThread { + return buildWithQRCodeQrCodeDataOidcConfigurationProgressListenerUnderlyingReturnValue + } else { + var returnValue: ClientProtocol? = nil + DispatchQueue.main.sync { + returnValue = buildWithQRCodeQrCodeDataOidcConfigurationProgressListenerUnderlyingReturnValue + } + + return returnValue! + } + } + set { + if Thread.isMainThread { + buildWithQRCodeQrCodeDataOidcConfigurationProgressListenerUnderlyingReturnValue = newValue + } else { + DispatchQueue.main.sync { + buildWithQRCodeQrCodeDataOidcConfigurationProgressListenerUnderlyingReturnValue = newValue + } + } + } + } + var buildWithQRCodeQrCodeDataOidcConfigurationProgressListenerClosure: ((QrCodeData, OIDCConfigurationProxy, QrLoginProgressListenerProxy) async throws -> ClientProtocol)? + + func buildWithQRCode(qrCodeData: QrCodeData, oidcConfiguration: OIDCConfigurationProxy, progressListener: QrLoginProgressListenerProxy) async throws -> ClientProtocol { + if let error = buildWithQRCodeQrCodeDataOidcConfigurationProgressListenerThrowableError { + throw error + } + buildWithQRCodeQrCodeDataOidcConfigurationProgressListenerCallsCount += 1 + buildWithQRCodeQrCodeDataOidcConfigurationProgressListenerReceivedArguments = (qrCodeData: qrCodeData, oidcConfiguration: oidcConfiguration, progressListener: progressListener) + DispatchQueue.main.async { + self.buildWithQRCodeQrCodeDataOidcConfigurationProgressListenerReceivedInvocations.append((qrCodeData: qrCodeData, oidcConfiguration: oidcConfiguration, progressListener: progressListener)) + } + if let buildWithQRCodeQrCodeDataOidcConfigurationProgressListenerClosure = buildWithQRCodeQrCodeDataOidcConfigurationProgressListenerClosure { + return try await buildWithQRCodeQrCodeDataOidcConfigurationProgressListenerClosure(qrCodeData, oidcConfiguration, progressListener) + } else { + return buildWithQRCodeQrCodeDataOidcConfigurationProgressListenerReturnValue + } + } +} class BugReportServiceMock: BugReportServiceProtocol { var crashedLastRun: Bool { get { return underlyingCrashedLastRun } @@ -15296,6 +15520,271 @@ class UserSessionMock: UserSessionProtocol { var underlyingCallbacks: PassthroughSubject! } +class UserSessionStoreMock: UserSessionStoreProtocol { + var hasSessions: Bool { + get { return underlyingHasSessions } + set(value) { underlyingHasSessions = value } + } + var underlyingHasSessions: Bool! + var userIDs: [String] = [] + var clientSessionDelegate: ClientSessionDelegate { + get { return underlyingClientSessionDelegate } + set(value) { underlyingClientSessionDelegate = value } + } + var underlyingClientSessionDelegate: ClientSessionDelegate! + + //MARK: - reset + + var resetUnderlyingCallsCount = 0 + var resetCallsCount: Int { + get { + if Thread.isMainThread { + return resetUnderlyingCallsCount + } else { + var returnValue: Int? = nil + DispatchQueue.main.sync { + returnValue = resetUnderlyingCallsCount + } + + return returnValue! + } + } + set { + if Thread.isMainThread { + resetUnderlyingCallsCount = newValue + } else { + DispatchQueue.main.sync { + resetUnderlyingCallsCount = newValue + } + } + } + } + var resetCalled: Bool { + return resetCallsCount > 0 + } + var resetClosure: (() -> Void)? + + func reset() { + resetCallsCount += 1 + resetClosure?() + } + //MARK: - restoreUserSession + + var restoreUserSessionUnderlyingCallsCount = 0 + var restoreUserSessionCallsCount: Int { + get { + if Thread.isMainThread { + return restoreUserSessionUnderlyingCallsCount + } else { + var returnValue: Int? = nil + DispatchQueue.main.sync { + returnValue = restoreUserSessionUnderlyingCallsCount + } + + return returnValue! + } + } + set { + if Thread.isMainThread { + restoreUserSessionUnderlyingCallsCount = newValue + } else { + DispatchQueue.main.sync { + restoreUserSessionUnderlyingCallsCount = newValue + } + } + } + } + var restoreUserSessionCalled: Bool { + return restoreUserSessionCallsCount > 0 + } + + var restoreUserSessionUnderlyingReturnValue: Result! + var restoreUserSessionReturnValue: Result! { + get { + if Thread.isMainThread { + return restoreUserSessionUnderlyingReturnValue + } else { + var returnValue: Result? = nil + DispatchQueue.main.sync { + returnValue = restoreUserSessionUnderlyingReturnValue + } + + return returnValue! + } + } + set { + if Thread.isMainThread { + restoreUserSessionUnderlyingReturnValue = newValue + } else { + DispatchQueue.main.sync { + restoreUserSessionUnderlyingReturnValue = newValue + } + } + } + } + var restoreUserSessionClosure: (() async -> Result)? + + func restoreUserSession() async -> Result { + restoreUserSessionCallsCount += 1 + if let restoreUserSessionClosure = restoreUserSessionClosure { + return await restoreUserSessionClosure() + } else { + return restoreUserSessionReturnValue + } + } + //MARK: - userSession + + var userSessionForSessionDirectoriesPassphraseUnderlyingCallsCount = 0 + var userSessionForSessionDirectoriesPassphraseCallsCount: Int { + get { + if Thread.isMainThread { + return userSessionForSessionDirectoriesPassphraseUnderlyingCallsCount + } else { + var returnValue: Int? = nil + DispatchQueue.main.sync { + returnValue = userSessionForSessionDirectoriesPassphraseUnderlyingCallsCount + } + + return returnValue! + } + } + set { + if Thread.isMainThread { + userSessionForSessionDirectoriesPassphraseUnderlyingCallsCount = newValue + } else { + DispatchQueue.main.sync { + userSessionForSessionDirectoriesPassphraseUnderlyingCallsCount = newValue + } + } + } + } + var userSessionForSessionDirectoriesPassphraseCalled: Bool { + return userSessionForSessionDirectoriesPassphraseCallsCount > 0 + } + var userSessionForSessionDirectoriesPassphraseReceivedArguments: (client: ClientProtocol, sessionDirectories: SessionDirectories, passphrase: String?)? + var userSessionForSessionDirectoriesPassphraseReceivedInvocations: [(client: ClientProtocol, sessionDirectories: SessionDirectories, passphrase: String?)] = [] + + var userSessionForSessionDirectoriesPassphraseUnderlyingReturnValue: Result! + var userSessionForSessionDirectoriesPassphraseReturnValue: Result! { + get { + if Thread.isMainThread { + return userSessionForSessionDirectoriesPassphraseUnderlyingReturnValue + } else { + var returnValue: Result? = nil + DispatchQueue.main.sync { + returnValue = userSessionForSessionDirectoriesPassphraseUnderlyingReturnValue + } + + return returnValue! + } + } + set { + if Thread.isMainThread { + userSessionForSessionDirectoriesPassphraseUnderlyingReturnValue = newValue + } else { + DispatchQueue.main.sync { + userSessionForSessionDirectoriesPassphraseUnderlyingReturnValue = newValue + } + } + } + } + var userSessionForSessionDirectoriesPassphraseClosure: ((ClientProtocol, SessionDirectories, String?) async -> Result)? + + func userSession(for client: ClientProtocol, sessionDirectories: SessionDirectories, passphrase: String?) async -> Result { + userSessionForSessionDirectoriesPassphraseCallsCount += 1 + userSessionForSessionDirectoriesPassphraseReceivedArguments = (client: client, sessionDirectories: sessionDirectories, passphrase: passphrase) + DispatchQueue.main.async { + self.userSessionForSessionDirectoriesPassphraseReceivedInvocations.append((client: client, sessionDirectories: sessionDirectories, passphrase: passphrase)) + } + if let userSessionForSessionDirectoriesPassphraseClosure = userSessionForSessionDirectoriesPassphraseClosure { + return await userSessionForSessionDirectoriesPassphraseClosure(client, sessionDirectories, passphrase) + } else { + return userSessionForSessionDirectoriesPassphraseReturnValue + } + } + //MARK: - logout + + var logoutUserSessionUnderlyingCallsCount = 0 + var logoutUserSessionCallsCount: Int { + get { + if Thread.isMainThread { + return logoutUserSessionUnderlyingCallsCount + } else { + var returnValue: Int? = nil + DispatchQueue.main.sync { + returnValue = logoutUserSessionUnderlyingCallsCount + } + + return returnValue! + } + } + set { + if Thread.isMainThread { + logoutUserSessionUnderlyingCallsCount = newValue + } else { + DispatchQueue.main.sync { + logoutUserSessionUnderlyingCallsCount = newValue + } + } + } + } + var logoutUserSessionCalled: Bool { + return logoutUserSessionCallsCount > 0 + } + var logoutUserSessionReceivedUserSession: UserSessionProtocol? + var logoutUserSessionReceivedInvocations: [UserSessionProtocol] = [] + var logoutUserSessionClosure: ((UserSessionProtocol) -> Void)? + + func logout(userSession: UserSessionProtocol) { + logoutUserSessionCallsCount += 1 + logoutUserSessionReceivedUserSession = userSession + DispatchQueue.main.async { + self.logoutUserSessionReceivedInvocations.append(userSession) + } + logoutUserSessionClosure?(userSession) + } + //MARK: - clearCache + + var clearCacheForUnderlyingCallsCount = 0 + var clearCacheForCallsCount: Int { + get { + if Thread.isMainThread { + return clearCacheForUnderlyingCallsCount + } else { + var returnValue: Int? = nil + DispatchQueue.main.sync { + returnValue = clearCacheForUnderlyingCallsCount + } + + return returnValue! + } + } + set { + if Thread.isMainThread { + clearCacheForUnderlyingCallsCount = newValue + } else { + DispatchQueue.main.sync { + clearCacheForUnderlyingCallsCount = newValue + } + } + } + } + var clearCacheForCalled: Bool { + return clearCacheForCallsCount > 0 + } + var clearCacheForReceivedUserID: String? + var clearCacheForReceivedInvocations: [String] = [] + var clearCacheForClosure: ((String) -> Void)? + + func clearCache(for userID: String) { + clearCacheForCallsCount += 1 + clearCacheForReceivedUserID = userID + DispatchQueue.main.async { + self.clearCacheForReceivedInvocations.append(userID) + } + clearCacheForClosure?(userID) + } +} class VoiceMessageCacheMock: VoiceMessageCacheProtocol { var urlForRecording: URL { get { return underlyingUrlForRecording } diff --git a/ElementX/Sources/Mocks/SDK/ClientSDKMock.swift b/ElementX/Sources/Mocks/SDK/ClientSDKMock.swift new file mode 100644 index 0000000000..a83a2e9b2a --- /dev/null +++ b/ElementX/Sources/Mocks/SDK/ClientSDKMock.swift @@ -0,0 +1,59 @@ +// +// Copyright 2024 New Vector Ltd. +// +// SPDX-License-Identifier: AGPL-3.0-only +// Please see LICENSE in the repository root for full details. +// + +import Foundation +import MatrixRustSDK + +extension ClientSDKMock { + struct Configuration { + // MARK: Authentication + + var serverAddress = "matrix.org" + var homeserverURL = "https://matrix-client.matrix.org" + var slidingSyncVersion = SlidingSyncVersion.native + var supportsPasswordLogin = true + var supportsOIDCLogin = false + var elementWellKnown = "{\"registration_helper_url\":\"https://develop.element.io/#/mobile_register\"}" + + // MARK: Session + + var userID: String? + var session = Session(accessToken: UUID().uuidString, + refreshToken: nil, + userId: "@alice:matrix.org", + deviceId: UUID().uuidString, + homeserverUrl: "https://matrix-client.matrix.org", + oidcData: nil, + slidingSyncVersion: .native) + } + + enum MockError: Error { case generic } + + convenience init(configuration: Configuration) { + self.init() + + homeserverLoginDetailsReturnValue = HomeserverLoginDetailsSDKMock(configuration: configuration) + slidingSyncVersionReturnValue = configuration.slidingSyncVersion + userIdServerNameThrowableError = MockError.generic + serverReturnValue = "https://\(configuration.serverAddress)" + getUrlUrlReturnValue = configuration.elementWellKnown + + userIdReturnValue = configuration.userID + sessionReturnValue = configuration.session + } +} + +extension HomeserverLoginDetailsSDKMock { + convenience init(configuration: ClientSDKMock.Configuration) { + self.init() + + slidingSyncVersionReturnValue = configuration.slidingSyncVersion + supportsPasswordLoginReturnValue = configuration.supportsPasswordLogin + supportsOidcLoginReturnValue = configuration.supportsOIDCLogin + urlReturnValue = configuration.homeserverURL + } +} diff --git a/ElementX/Sources/Mocks/EventTimelineItemSDKMock.swift b/ElementX/Sources/Mocks/SDK/EventTimelineItemSDKMock.swift similarity index 100% rename from ElementX/Sources/Mocks/EventTimelineItemSDKMock.swift rename to ElementX/Sources/Mocks/SDK/EventTimelineItemSDKMock.swift diff --git a/ElementX/Sources/Mocks/UserSessionStoreMock.swift b/ElementX/Sources/Mocks/UserSessionStoreMock.swift new file mode 100644 index 0000000000..69b8418f1c --- /dev/null +++ b/ElementX/Sources/Mocks/UserSessionStoreMock.swift @@ -0,0 +1,17 @@ +// +// Copyright 2024 New Vector Ltd. +// +// SPDX-License-Identifier: AGPL-3.0-only +// Please see LICENSE in the repository root for full details. +// + +extension UserSessionStoreMock { + struct Configuration { } + + convenience init(configuration: Configuration) { + self.init() + + userSessionForSessionDirectoriesPassphraseReturnValue = .success(UserSessionMock(.init(clientProxy: ClientProxyMock(.init())))) + clientSessionDelegate = KeychainControllerMock() + } +} diff --git a/ElementX/Sources/Services/Authentication/AuthenticationClientBuilder.swift b/ElementX/Sources/Services/Authentication/AuthenticationClientBuilder.swift index 350e388246..dee843b5f5 100644 --- a/ElementX/Sources/Services/Authentication/AuthenticationClientBuilder.swift +++ b/ElementX/Sources/Services/Authentication/AuthenticationClientBuilder.swift @@ -8,8 +8,16 @@ import Foundation import MatrixRustSDK +// sourcery: AutoMockable +protocol AuthenticationClientBuilderProtocol { + func build(homeserverAddress: String) async throws -> ClientProtocol + func buildWithQRCode(qrCodeData: QrCodeData, + oidcConfiguration: OIDCConfigurationProxy, + progressListener: QrLoginProgressListenerProxy) async throws -> ClientProtocol +} + /// A wrapper around `ClientBuilder` to share reusable code between Normal and QR logins. -struct AuthenticationClientBuilder { +struct AuthenticationClientBuilder: AuthenticationClientBuilderProtocol { let sessionDirectories: SessionDirectories let passphrase: String let clientSessionDelegate: ClientSessionDelegate @@ -18,7 +26,7 @@ struct AuthenticationClientBuilder { let appHooks: AppHooks /// Builds a Client for login using OIDC or password authentication. - func build(homeserverAddress: String) async throws -> Client { + func build(homeserverAddress: String) async throws -> ClientProtocol { if appSettings.slidingSyncDiscovery == .forceNative { return try await makeClientBuilder(slidingSync: .forceNative).serverNameOrHomeserverUrl(serverNameOrUrl: homeserverAddress).build() } @@ -38,7 +46,7 @@ struct AuthenticationClientBuilder { /// Builds a Client, authenticating with the given QR code data. func buildWithQRCode(qrCodeData: QrCodeData, oidcConfiguration: OIDCConfigurationProxy, - progressListener: QrLoginProgressListenerProxy) async throws -> Client { + progressListener: QrLoginProgressListenerProxy) async throws -> ClientProtocol { if appSettings.slidingSyncDiscovery == .forceNative { return try await makeClientBuilder(slidingSync: .forceNative).buildWithQrCode(qrCodeData: qrCodeData, oidcConfiguration: oidcConfiguration.rustValue, diff --git a/ElementX/Sources/Services/Authentication/AuthenticationClientBuilderFactory.swift b/ElementX/Sources/Services/Authentication/AuthenticationClientBuilderFactory.swift new file mode 100644 index 0000000000..873a17dfd9 --- /dev/null +++ b/ElementX/Sources/Services/Authentication/AuthenticationClientBuilderFactory.swift @@ -0,0 +1,33 @@ +// +// Copyright 2024 New Vector Ltd. +// +// SPDX-License-Identifier: AGPL-3.0-only +// Please see LICENSE in the repository root for full details. +// + +import Foundation +import MatrixRustSDK + +// sourcery: AutoMockable +protocol AuthenticationClientBuilderFactoryProtocol { + func makeBuilder(sessionDirectories: SessionDirectories, + passphrase: String, + clientSessionDelegate: ClientSessionDelegate, + appSettings: AppSettings, + appHooks: AppHooks) -> AuthenticationClientBuilderProtocol +} + +/// A wrapper around `ClientBuilder` to share reusable code between Normal and QR logins. +struct AuthenticationClientBuilderFactory: AuthenticationClientBuilderFactoryProtocol { + func makeBuilder(sessionDirectories: SessionDirectories, + passphrase: String, + clientSessionDelegate: ClientSessionDelegate, + appSettings: AppSettings, + appHooks: AppHooks) -> AuthenticationClientBuilderProtocol { + AuthenticationClientBuilder(sessionDirectories: sessionDirectories, + passphrase: passphrase, + clientSessionDelegate: clientSessionDelegate, + appSettings: appSettings, + appHooks: appHooks) + } +} diff --git a/ElementX/Sources/Services/Authentication/AuthenticationService.swift b/ElementX/Sources/Services/Authentication/AuthenticationService.swift index 18f9a1d550..0c4ced888d 100644 --- a/ElementX/Sources/Services/Authentication/AuthenticationService.swift +++ b/ElementX/Sources/Services/Authentication/AuthenticationService.swift @@ -10,10 +10,11 @@ import Foundation import MatrixRustSDK class AuthenticationService: AuthenticationServiceProtocol { - private var client: Client? + private var client: ClientProtocol? private var sessionDirectories: SessionDirectories private let passphrase: String + private let clientBuilderFactory: AuthenticationClientBuilderFactoryProtocol private let userSessionStore: UserSessionStoreProtocol private let appSettings: AppSettings private let appHooks: AppHooks @@ -22,9 +23,14 @@ class AuthenticationService: AuthenticationServiceProtocol { var homeserver: CurrentValuePublisher { homeserverSubject.asCurrentValuePublisher() } private(set) var flow: AuthenticationFlow - init(userSessionStore: UserSessionStoreProtocol, encryptionKeyProvider: EncryptionKeyProviderProtocol, appSettings: AppSettings, appHooks: AppHooks) { + init(userSessionStore: UserSessionStoreProtocol, + encryptionKeyProvider: EncryptionKeyProviderProtocol, + clientBuilderFactory: AuthenticationClientBuilderFactoryProtocol = AuthenticationClientBuilderFactory(), + appSettings: AppSettings, + appHooks: AppHooks) { sessionDirectories = .init() passphrase = encryptionKeyProvider.generateKey().base64EncodedString() + self.clientBuilderFactory = clientBuilderFactory self.userSessionStore = userSessionStore self.appSettings = appSettings self.appHooks = appHooks @@ -165,16 +171,16 @@ class AuthenticationService: AuthenticationServiceProtocol { // MARK: - Private - private func makeClientBuilder() -> AuthenticationClientBuilder { + private func makeClientBuilder() -> AuthenticationClientBuilderProtocol { // Use a fresh session directory each time the user enters a different server // so that caches (e.g. server versions) are always fresh for the new server. rotateSessionDirectory() - return AuthenticationClientBuilder(sessionDirectories: sessionDirectories, - passphrase: passphrase, - clientSessionDelegate: userSessionStore.clientSessionDelegate, - appSettings: appSettings, - appHooks: appHooks) + return clientBuilderFactory.makeBuilder(sessionDirectories: sessionDirectories, + passphrase: passphrase, + clientSessionDelegate: userSessionStore.clientSessionDelegate, + appSettings: appSettings, + appHooks: appHooks) } private func rotateSessionDirectory() { @@ -182,7 +188,7 @@ class AuthenticationService: AuthenticationServiceProtocol { sessionDirectories = .init() } - private func userSession(for client: Client) async -> Result { + private func userSession(for client: ClientProtocol) async -> Result { switch await userSessionStore.userSession(for: client, sessionDirectories: sessionDirectories, passphrase: passphrase) { case .success(let clientProxy): return .success(clientProxy) diff --git a/ElementX/Sources/Services/Authentication/AuthenticationServiceProtocol.swift b/ElementX/Sources/Services/Authentication/AuthenticationServiceProtocol.swift index 0daa77088c..3efa258e90 100644 --- a/ElementX/Sources/Services/Authentication/AuthenticationServiceProtocol.swift +++ b/ElementX/Sources/Services/Authentication/AuthenticationServiceProtocol.swift @@ -16,7 +16,7 @@ enum AuthenticationFlow { case register } -enum AuthenticationServiceError: Error { +enum AuthenticationServiceError: Error, Equatable { /// An error occurred during OIDC authentication. case oidcError(OIDCError) case invalidServer diff --git a/ElementX/Sources/Services/QRCode/QRCodeLoginService.swift b/ElementX/Sources/Services/QRCode/QRCodeLoginService.swift index 015a6a9d09..d1f4eaad2f 100644 --- a/ElementX/Sources/Services/QRCode/QRCodeLoginService.swift +++ b/ElementX/Sources/Services/QRCode/QRCodeLoginService.swift @@ -81,7 +81,7 @@ final class QRCodeLoginService: QRCodeLoginServiceProtocol { sessionDirectories = .init() } - private func userSession(for client: Client) async -> Result { + private func userSession(for client: ClientProtocol) async -> Result { switch await userSessionStore.userSession(for: client, sessionDirectories: sessionDirectories, passphrase: passphrase) { case .success(let session): return .success(session) diff --git a/ElementX/Sources/Services/UserSession/UserSessionStore.swift b/ElementX/Sources/Services/UserSession/UserSessionStore.swift index 6b7f94908c..ab38d90a9c 100644 --- a/ElementX/Sources/Services/UserSession/UserSessionStore.swift +++ b/ElementX/Sources/Services/UserSession/UserSessionStore.swift @@ -60,7 +60,7 @@ class UserSessionStore: UserSessionStoreProtocol { } } - func userSession(for client: Client, sessionDirectories: SessionDirectories, passphrase: String?) async -> Result { + func userSession(for client: ClientProtocol, sessionDirectories: SessionDirectories, passphrase: String?) async -> Result { do { let session = try client.session() let userID = try client.userId() @@ -146,7 +146,7 @@ class UserSessionStore: UserSessionStoreProtocol { } } - private func setupProxyForClient(_ client: Client) async -> ClientProxyProtocol { + private func setupProxyForClient(_ client: ClientProtocol) async -> ClientProxyProtocol { await ClientProxy(client: client, networkMonitor: networkMonitor, appSettings: appSettings) diff --git a/ElementX/Sources/Services/UserSession/UserSessionStoreProtocol.swift b/ElementX/Sources/Services/UserSession/UserSessionStoreProtocol.swift index fd841ae699..64772c3672 100644 --- a/ElementX/Sources/Services/UserSession/UserSessionStoreProtocol.swift +++ b/ElementX/Sources/Services/UserSession/UserSessionStoreProtocol.swift @@ -14,6 +14,7 @@ enum UserSessionStoreError: Error { case failedSettingUpSession } +// sourcery: AutoMockable protocol UserSessionStoreProtocol { /// Deletes all data stored in the shared container and keychain func reset() @@ -31,7 +32,7 @@ protocol UserSessionStoreProtocol { func restoreUserSession() async -> Result /// Creates a user session for a new client from the SDK along with the passphrase used for the data stores. - func userSession(for client: Client, sessionDirectories: SessionDirectories, passphrase: String?) async -> Result + func userSession(for client: ClientProtocol, sessionDirectories: SessionDirectories, passphrase: String?) async -> Result /// Logs out of the specified session. func logout(userSession: UserSessionProtocol) diff --git a/UnitTests/Sources/AuthenticationServiceTests.swift b/UnitTests/Sources/AuthenticationServiceTests.swift new file mode 100644 index 0000000000..f3202862ae --- /dev/null +++ b/UnitTests/Sources/AuthenticationServiceTests.swift @@ -0,0 +1,92 @@ +// +// Copyright 2024 New Vector Ltd. +// +// SPDX-License-Identifier: AGPL-3.0-only +// Please see LICENSE in the repository root for full details. +// + +import XCTest + +@testable import ElementX + +class AuthenticationServiceTests: XCTestCase { + var client: ClientSDKMock! + var userSessionStore: UserSessionStoreMock! + var encryptionKeyProvider: MockEncryptionKeyProvider! + + var service: AuthenticationService! + + func testLogin() async { + setupMocks() + + switch await service.configure(for: "matrix.org", flow: .login) { + case .success: + break + case .failure(let error): + XCTFail("Unexpected failure: \(error)") + } + + XCTAssertEqual(service.flow, .login) + XCTAssertEqual(service.homeserver.value, .mockMatrixDotOrg) + + switch await service.login(username: "alice", password: "p4ssw0rd", initialDeviceName: nil, deviceID: nil) { + case .success: + XCTAssertEqual(client.loginUsernamePasswordInitialDeviceNameDeviceIdCallsCount, 1) + XCTAssertEqual(userSessionStore.userSessionForSessionDirectoriesPassphraseCallsCount, 1) + XCTAssertEqual(userSessionStore.userSessionForSessionDirectoriesPassphraseReceivedArguments?.passphrase, + encryptionKeyProvider.generateKey().base64EncodedString()) + case .failure(let error): + XCTFail("Unexpected failure: \(error)") + } + } + + func testConfigureRegister() async { + setupMocks() + + switch await service.configure(for: "matrix.org", flow: .register) { + case .success: + break + case .failure(let error): + XCTFail("Unexpected failure: \(error)") + } + + XCTAssertEqual(service.flow, .register) + XCTAssertEqual(service.homeserver.value, .mockMatrixDotOrg) + } + + func testConfigureRegisterNoSupport() async { + setupMocks(clientConfiguration: .init(elementWellKnown: "")) + + switch await service.configure(for: "matrix.org", flow: .register) { + case .success: + XCTFail("Configuration should have failed") + case .failure(let error): + XCTAssertEqual(error, .registrationNotSupported) + } + + XCTAssertEqual(service.flow, .login) + XCTAssertEqual(service.homeserver.value, .init(address: "matrix.org", loginMode: .unknown)) + } + + // MARK: - Helpers + + private func setupMocks(clientConfiguration: ClientSDKMock.Configuration = .init()) { + client = ClientSDKMock(configuration: clientConfiguration) + userSessionStore = UserSessionStoreMock(configuration: .init()) + encryptionKeyProvider = MockEncryptionKeyProvider() + + service = AuthenticationService(userSessionStore: userSessionStore, + encryptionKeyProvider: encryptionKeyProvider, + clientBuilderFactory: AuthenticationClientBuilderFactoryMock(configuration: .init(builtClient: client)), + appSettings: ServiceLocator.shared.settings, + appHooks: AppHooks()) + } +} + +struct MockEncryptionKeyProvider: EncryptionKeyProviderProtocol { + private let key = "12345678" + + func generateKey() -> Data { + Data(key.utf8) + } +} From ab2ea295c210363cdcf62c1c101b802c2244a450 Mon Sep 17 00:00:00 2001 From: Doug Date: Fri, 20 Sep 2024 16:15:42 +0100 Subject: [PATCH 3/5] Use the real AuthenticationService with a mock Client in all of the tests. --- ElementX.xcodeproj/project.pbxproj | 8 -- ...thenticationClientBuilderFactoryMock.swift | 4 +- .../AuthenticationClientBuilderMock.swift | 39 ++++++++-- .../Sources/Mocks/SDK/ClientSDKMock.swift | 20 ++++- .../View/ServerConfirmationScreen.swift | 2 +- .../AuthenticationService.swift | 10 +++ .../MockAuthenticationService.swift | 75 ------------------- .../UITests/UITestsAppCoordinator.swift | 11 +-- .../UITests/UITestsScreenIdentifier.swift | 1 - ...AuthenticationFlowCoordinatorUITests.swift | 37 +++++++-- UITests/Sources/LoginScreenUITests.swift | 35 --------- ...onFlow-1-iPad-10th-generation-en-GB.UI.png | 3 + ...uthenticationFlow-1-iPhone-15-en-GB.UI.png | 3 + .../login-0-iPad-10th-generation-en-GB.UI.png | 3 - .../login-0-iPhone-16-en-GB.UI.png | 3 - .../login-1-iPad-10th-generation-en-GB.UI.png | 3 - .../login-1-iPhone-16-en-GB.UI.png | 3 - .../login-iPad-10th-generation-en-GB.UI.png | 3 - .../Application/login-iPhone-16-en-GB.UI.png | 3 - .../Sources/AuthenticationServiceTests.swift | 16 ++-- 20 files changed, 113 insertions(+), 169 deletions(-) delete mode 100644 ElementX/Sources/Services/Authentication/MockAuthenticationService.swift delete mode 100644 UITests/Sources/LoginScreenUITests.swift create mode 100644 UITests/Sources/__Snapshots__/Application/authenticationFlow-1-iPad-10th-generation-en-GB.UI.png create mode 100644 UITests/Sources/__Snapshots__/Application/authenticationFlow-1-iPhone-15-en-GB.UI.png delete mode 100644 UITests/Sources/__Snapshots__/Application/login-0-iPad-10th-generation-en-GB.UI.png delete mode 100644 UITests/Sources/__Snapshots__/Application/login-0-iPhone-16-en-GB.UI.png delete mode 100644 UITests/Sources/__Snapshots__/Application/login-1-iPad-10th-generation-en-GB.UI.png delete mode 100644 UITests/Sources/__Snapshots__/Application/login-1-iPhone-16-en-GB.UI.png delete mode 100644 UITests/Sources/__Snapshots__/Application/login-iPad-10th-generation-en-GB.UI.png delete mode 100644 UITests/Sources/__Snapshots__/Application/login-iPhone-16-en-GB.UI.png diff --git a/ElementX.xcodeproj/project.pbxproj b/ElementX.xcodeproj/project.pbxproj index 1893cab8e0..d0c76aba64 100644 --- a/ElementX.xcodeproj/project.pbxproj +++ b/ElementX.xcodeproj/project.pbxproj @@ -227,7 +227,6 @@ 3116693C5EB476E028990416 /* XCTestCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74611A4182DCF5F4D42696EC /* XCTestCase.swift */; }; 3118D9ABFD4BE5A3492FF88A /* ElementCallConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC437C491EA6996513B1CEAB /* ElementCallConfiguration.swift */; }; 32B7891D937377A59606EDFC /* UserFlowTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 21DD8599815136EFF5B73F38 /* UserFlowTests.swift */; }; - 32FC143630CE22A9E403370B /* MockAuthenticationService.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA38899517F08FE2AF34EB45 /* MockAuthenticationService.swift */; }; 339BC18777912E1989F2F17D /* Section.swift in Sources */ = {isa = PBXBuildFile; fileRef = 584A61D9C459FAFEF038A7C0 /* Section.swift */; }; 33CAC1226DFB8B5D8447D286 /* GZIP in Frameworks */ = {isa = PBXBuildFile; productRef = 1BCD21310B997A6837B854D6 /* GZIP */; }; 33F1FB19F222BA9930AB1A00 /* RoomListFiltersView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E6372DD10DED30E7AD7BCE21 /* RoomListFiltersView.swift */; }; @@ -412,7 +411,6 @@ 5BC6C4ADFE7F2A795ECDE130 /* SecureBackupKeyBackupScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B2D4EEBE8C098BBADD10939 /* SecureBackupKeyBackupScreenCoordinator.swift */; }; 5C02841B2A86327B2C377682 /* NotificationConstants.swift in Sources */ = {isa = PBXBuildFile; fileRef = C830A64609CBD152F06E0457 /* NotificationConstants.swift */; }; 5C164551F7D26E24F09083D3 /* StaticLocationScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = C616D90B1E2F033CAA325439 /* StaticLocationScreenViewModelProtocol.swift */; }; - 5C8AFBF168A41E20835F3B86 /* LoginScreenUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DB34B0C74CD242FED9DD069 /* LoginScreenUITests.swift */; }; 5D27B6537591471A42C89027 /* EmoteRoomTimelineItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 450E04B2A976CC4C8CC1807C /* EmoteRoomTimelineItem.swift */; }; 5D52925FEB1B780C65B0529F /* PinnedEventsTimelineScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA4F6D7000EDCD187E0989E7 /* PinnedEventsTimelineScreen.swift */; }; 5D53AE9342A4C06B704247ED /* MediaLoaderProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A02406480C351B8C6E0682C /* MediaLoaderProtocol.swift */; }; @@ -1329,7 +1327,6 @@ 1D9F148717D74F73BE724434 /* LongPressWithFeedback.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LongPressWithFeedback.swift; sourceTree = ""; }; 1DA7E93C2E148B96EF6A8500 /* TimelineItemAccessibilityModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineItemAccessibilityModifier.swift; sourceTree = ""; }; 1DB2FC2AA9A07EE792DF65CF /* NotificationPermissionsScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationPermissionsScreenModels.swift; sourceTree = ""; }; - 1DB34B0C74CD242FED9DD069 /* LoginScreenUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginScreenUITests.swift; sourceTree = ""; }; 1DE7969EBCAF078813E18EA1 /* RoomRolesAndPermissionsScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomRolesAndPermissionsScreenModels.swift; sourceTree = ""; }; 1DF8F7A3AD83D04C08D75E01 /* RoomDetailsEditScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomDetailsEditScreenViewModelProtocol.swift; sourceTree = ""; }; 1DFE0E493FB55E5A62E7852A /* ProposedViewSize.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProposedViewSize.swift; sourceTree = ""; }; @@ -2150,7 +2147,6 @@ D95E8C0EFEC0C6F96EDAA71A /* PreviewTests.xctest */ = {isa = PBXFileReference; includeInIndex = 0; lastKnownFileType = wrapper.cfbundle; path = PreviewTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; DA14564EE143F73F7E4D1F79 /* RoomNotificationSettingsScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomNotificationSettingsScreenModels.swift; sourceTree = ""; }; DA2AEC1AB349A341FE13DEC1 /* StartChatScreenUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StartChatScreenUITests.swift; sourceTree = ""; }; - DA38899517F08FE2AF34EB45 /* MockAuthenticationService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockAuthenticationService.swift; sourceTree = ""; }; DA3D82522494E78746B2214E /* de */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = de; path = de.lproj/SAS.strings; sourceTree = ""; }; DAB8D7926A5684E18196B538 /* VoiceMessageCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VoiceMessageCache.swift; sourceTree = ""; }; DB06F22CFA34885B40976061 /* RoomDetailsEditScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomDetailsEditScreen.swift; sourceTree = ""; }; @@ -4421,7 +4417,6 @@ 295E28C3B9EAADF519BF2F44 /* AuthenticationFlowCoordinatorUITests.swift */, C6FEA87EA3752203065ECE27 /* BugReportUITests.swift */, F8CEB4634C0DD7779C4AB504 /* CreateRoomScreenUITests.swift */, - 1DB34B0C74CD242FED9DD069 /* LoginScreenUITests.swift */, 3368395F06AA180138E185B6 /* PollFormScreenUITests.swift */, C5B7A755E985FA14469E86B2 /* RoomMembersListScreenUITests.swift */, 45571C2EBD98ED7E0CEA7AF7 /* RoomRolesAndPermissionsUITests.swift */, @@ -4670,7 +4665,6 @@ B655A536341D2695158C6664 /* AuthenticationClientBuilderFactory.swift */, F3A1AB5A84D843B6AC8D5F1E /* AuthenticationService.swift */, 5E75948AA1FE1D1A7809931F /* AuthenticationServiceProtocol.swift */, - DA38899517F08FE2AF34EB45 /* MockAuthenticationService.swift */, A69869844D2B6F5BD9AABF85 /* OIDCConfigurationProxy.swift */, ); path = Authentication; @@ -6593,7 +6587,6 @@ F54E2D6CAD96E1AC15BC526F /* MessageForwardingScreenViewModel.swift in Sources */, C13128AAA787A4C2CBE4EE82 /* MessageForwardingScreenViewModelProtocol.swift in Sources */, C97325EFDCCEE457432A9E82 /* MessageText.swift in Sources */, - 32FC143630CE22A9E403370B /* MockAuthenticationService.swift in Sources */, B659E3A49889E749E3239EA7 /* MockMediaProvider.swift in Sources */, 09C83DDDB07C28364F325209 /* MockRoomTimelineController.swift in Sources */, B721125D17A0BA86794F29FB /* MockServerSelectionScreenState.swift in Sources */, @@ -7058,7 +7051,6 @@ 7756C4E90CABE6F14F7920A0 /* BugReportUITests.swift in Sources */, 94D0F36A87E596A93C0C178A /* Bundle.swift in Sources */, 9F19096BFA629C0AC282B1E4 /* CreateRoomScreenUITests.swift in Sources */, - 5C8AFBF168A41E20835F3B86 /* LoginScreenUITests.swift in Sources */, 0CF81807BE5FBFC9E2BBCECF /* PollFormScreenUITests.swift in Sources */, 44121202B4A260C98BF615A7 /* RoomMembersListScreenUITests.swift in Sources */, D29E046C1E3045E0346C479D /* RoomRolesAndPermissionsUITests.swift in Sources */, diff --git a/ElementX/Sources/Mocks/AuthenticationClientBuilderFactoryMock.swift b/ElementX/Sources/Mocks/AuthenticationClientBuilderFactoryMock.swift index 03e962e31c..841a0701d3 100644 --- a/ElementX/Sources/Mocks/AuthenticationClientBuilderFactoryMock.swift +++ b/ElementX/Sources/Mocks/AuthenticationClientBuilderFactoryMock.swift @@ -10,13 +10,13 @@ import MatrixRustSDK extension AuthenticationClientBuilderFactoryMock { struct Configuration { - var builtClient: ClientProtocol + var builderConfiguration: AuthenticationClientBuilderMock.Configuration = .init() } convenience init(configuration: Configuration) { self.init() - let clientBuilder = AuthenticationClientBuilderMock(configuration: .init(builtClient: configuration.builtClient)) + let clientBuilder = AuthenticationClientBuilderMock(configuration: configuration.builderConfiguration) makeBuilderSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksReturnValue = clientBuilder } } diff --git a/ElementX/Sources/Mocks/AuthenticationClientBuilderMock.swift b/ElementX/Sources/Mocks/AuthenticationClientBuilderMock.swift index 15969587a9..0cccf9e539 100644 --- a/ElementX/Sources/Mocks/AuthenticationClientBuilderMock.swift +++ b/ElementX/Sources/Mocks/AuthenticationClientBuilderMock.swift @@ -8,15 +8,40 @@ import Foundation import MatrixRustSDK -struct AuthenticationClientBuilderMockConfiguration { - var builtClient: ClientProtocol -} - extension AuthenticationClientBuilderMock { - convenience init(configuration: AuthenticationClientBuilderMockConfiguration) { + struct Configuration { + var homeserverClients = [ + "matrix.org": ClientSDKMock(configuration: .init()), + "example.com": ClientSDKMock(configuration: .init(serverAddress: "example.com", + homeserverURL: "https://matrix.example.com", + slidingSyncVersion: .native, + supportsPasswordLogin: true, + elementWellKnown: "")), + "company.com": ClientSDKMock(configuration: .init(serverAddress: "company.com", + homeserverURL: "https://matrix.company.com", + slidingSyncVersion: .native, + oidcLoginURL: "https://auth.company.com/oidc", + supportsPasswordLogin: false, + elementWellKnown: "")), + "server.net": ClientSDKMock(configuration: .init(serverAddress: "server.net", + homeserverURL: "https://matrix.example.com", + slidingSyncVersion: .native, + supportsPasswordLogin: false, + elementWellKnown: "")) + ] + var qrCodeClient = ClientSDKMock(configuration: .init()) + } + + convenience init(configuration: Configuration) { self.init() - buildHomeserverAddressReturnValue = configuration.builtClient - buildWithQRCodeQrCodeDataOidcConfigurationProgressListenerReturnValue = configuration.builtClient + buildHomeserverAddressClosure = { address in + guard let client = configuration.homeserverClients[address] else { + throw ClientBuildError.ServerUnreachable(message: "Not a known homeserver.") + } + return client + } + + buildWithQRCodeQrCodeDataOidcConfigurationProgressListenerReturnValue = configuration.qrCodeClient } } diff --git a/ElementX/Sources/Mocks/SDK/ClientSDKMock.swift b/ElementX/Sources/Mocks/SDK/ClientSDKMock.swift index a83a2e9b2a..61738a095f 100644 --- a/ElementX/Sources/Mocks/SDK/ClientSDKMock.swift +++ b/ElementX/Sources/Mocks/SDK/ClientSDKMock.swift @@ -15,9 +15,10 @@ extension ClientSDKMock { var serverAddress = "matrix.org" var homeserverURL = "https://matrix-client.matrix.org" var slidingSyncVersion = SlidingSyncVersion.native + var oidcLoginURL: String? var supportsPasswordLogin = true - var supportsOIDCLogin = false var elementWellKnown = "{\"registration_helper_url\":\"https://develop.element.io/#/mobile_register\"}" + var validCredentials = (username: "alice", password: "12345678") // MARK: Session @@ -41,6 +42,13 @@ extension ClientSDKMock { userIdServerNameThrowableError = MockError.generic serverReturnValue = "https://\(configuration.serverAddress)" getUrlUrlReturnValue = configuration.elementWellKnown + urlForOidcLoginOidcConfigurationReturnValue = OidcAuthorizationDataSDKMock(configuration: configuration) + loginUsernamePasswordInitialDeviceNameDeviceIdClosure = { username, password, _, _ in + guard username == configuration.validCredentials.username, + password == configuration.validCredentials.password else { + throw MockError.generic // use the matrix error + } + } userIdReturnValue = configuration.userID sessionReturnValue = configuration.session @@ -53,7 +61,15 @@ extension HomeserverLoginDetailsSDKMock { slidingSyncVersionReturnValue = configuration.slidingSyncVersion supportsPasswordLoginReturnValue = configuration.supportsPasswordLogin - supportsOidcLoginReturnValue = configuration.supportsOIDCLogin + supportsOidcLoginReturnValue = configuration.oidcLoginURL != nil urlReturnValue = configuration.homeserverURL } } + +extension OidcAuthorizationDataSDKMock { + convenience init(configuration: ClientSDKMock.Configuration) { + self.init() + + loginUrlReturnValue = configuration.oidcLoginURL + } +} diff --git a/ElementX/Sources/Screens/Authentication/ServerConfirmationScreen/View/ServerConfirmationScreen.swift b/ElementX/Sources/Screens/Authentication/ServerConfirmationScreen/View/ServerConfirmationScreen.swift index 1ea19d1422..14beaed6f9 100644 --- a/ElementX/Sources/Screens/Authentication/ServerConfirmationScreen/View/ServerConfirmationScreen.swift +++ b/ElementX/Sources/Screens/Authentication/ServerConfirmationScreen/View/ServerConfirmationScreen.swift @@ -86,7 +86,7 @@ struct ServerConfirmationScreen_Previews: PreviewProvider, TestablePreview { } static func makeViewModel(flow: AuthenticationFlow) -> ServerConfirmationScreenViewModel { - ServerConfirmationScreenViewModel(authenticationService: MockAuthenticationService(), + ServerConfirmationScreenViewModel(authenticationService: AuthenticationService.mock, authenticationFlow: flow, slidingSyncLearnMoreURL: ServiceLocator.shared.settings.slidingSyncLearnMoreURL, userIndicatorController: UserIndicatorControllerMock()) diff --git a/ElementX/Sources/Services/Authentication/AuthenticationService.swift b/ElementX/Sources/Services/Authentication/AuthenticationService.swift index 0c4ced888d..66dbacb2ae 100644 --- a/ElementX/Sources/Services/Authentication/AuthenticationService.swift +++ b/ElementX/Sources/Services/Authentication/AuthenticationService.swift @@ -197,3 +197,13 @@ class AuthenticationService: AuthenticationServiceProtocol { } } } + +// MARK: - Mocks + +extension AuthenticationService { + static var mock = AuthenticationService(userSessionStore: UserSessionStoreMock(configuration: .init()), + encryptionKeyProvider: EncryptionKeyProvider(), + clientBuilderFactory: AuthenticationClientBuilderFactoryMock(configuration: .init()), + appSettings: ServiceLocator.shared.settings, + appHooks: AppHooks()) +} diff --git a/ElementX/Sources/Services/Authentication/MockAuthenticationService.swift b/ElementX/Sources/Services/Authentication/MockAuthenticationService.swift deleted file mode 100644 index 8e6b4ffb49..0000000000 --- a/ElementX/Sources/Services/Authentication/MockAuthenticationService.swift +++ /dev/null @@ -1,75 +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 Combine -import Foundation -import MatrixRustSDK - -class MockAuthenticationService: AuthenticationServiceProtocol { - let validCredentials = (username: "alice", password: "12345678") - - private let homeserverSubject: CurrentValueSubject - var homeserver: CurrentValuePublisher { homeserverSubject.asCurrentValuePublisher() } - private(set) var flow: AuthenticationFlow = .login - - init(homeserver: LoginHomeserver = .mockMatrixDotOrg) { - homeserverSubject = .init(homeserver) - } - - func configure(for homeserverAddress: String, flow: AuthenticationFlow) async -> Result { - #warning("[DOUG] Handle flow (or lets mock the real service?)") - // Map the address to the mock homeservers - if LoginHomeserver.mockMatrixDotOrg.address.contains(homeserverAddress) { - self.flow = flow - homeserverSubject.send(.mockMatrixDotOrg) - return .success(()) - } else if LoginHomeserver.mockOIDC.address.contains(homeserverAddress) { - self.flow = flow - homeserverSubject.send(.mockOIDC) - return .success(()) - } else if LoginHomeserver.mockBasicServer.address.contains(homeserverAddress) { - self.flow = flow - homeserverSubject.send(.mockBasicServer) - return .success(()) - } else if LoginHomeserver.mockUnsupported.address.contains(homeserverAddress) { - self.flow = flow - homeserverSubject.send(.mockUnsupported) - return .success(()) - } else { - // Otherwise fail with an invalid server. - return .failure(.invalidServer) - } - } - - func urlForOIDCLogin() async -> Result { - .failure(.oidcError(.notSupported)) - } - - func abortOIDCLogin(data: OIDCAuthorizationDataProxy) async { } - - func loginWithOIDCCallback(_ callbackURL: URL, data: OIDCAuthorizationDataProxy) async -> Result { - .failure(.oidcError(.notSupported)) - } - - func login(username: String, password: String, initialDeviceName: String?, deviceID: String?) async -> Result { - // Login only succeeds if the username and password match the valid credentials property - guard username == validCredentials.username, password == validCredentials.password else { - return .failure(.invalidCredentials) - } - - let userSession = UserSessionMock(.init(clientProxy: ClientProxyMock(.init(userID: username)))) - return .success(userSession) - } - - func completeWebRegistration(using credentials: WebRegistrationCredentials) async -> Result { - .failure(.failedLoggingIn) - } - - func reset() { - fatalError("Not mocked") - } -} diff --git a/ElementX/Sources/UITests/UITestsAppCoordinator.swift b/ElementX/Sources/UITests/UITestsAppCoordinator.swift index 8fc7b05f8c..2af7f39bcc 100644 --- a/ElementX/Sources/UITests/UITestsAppCoordinator.swift +++ b/ElementX/Sources/UITests/UITestsAppCoordinator.swift @@ -109,23 +109,16 @@ class MockScreen: Identifiable { lazy var coordinator: CoordinatorProtocol? = { switch id { - case .login: - let navigationStackCoordinator = NavigationStackCoordinator() - let coordinator = LoginScreenCoordinator(parameters: .init(authenticationService: MockAuthenticationService(), - analytics: ServiceLocator.shared.analytics, - userIndicatorController: ServiceLocator.shared.userIndicatorController)) - navigationStackCoordinator.setRootCoordinator(coordinator) - return navigationStackCoordinator case .serverSelection: let navigationStackCoordinator = NavigationStackCoordinator() - let coordinator = ServerSelectionScreenCoordinator(parameters: .init(authenticationService: MockAuthenticationService(), + let coordinator = ServerSelectionScreenCoordinator(parameters: .init(authenticationService: AuthenticationService.mock, authenticationFlow: .login, slidingSyncLearnMoreURL: ServiceLocator.shared.settings.slidingSyncLearnMoreURL, userIndicatorController: ServiceLocator.shared.userIndicatorController)) navigationStackCoordinator.setRootCoordinator(coordinator) return navigationStackCoordinator case .authenticationFlow: - let flowCoordinator = AuthenticationFlowCoordinator(authenticationService: MockAuthenticationService(), + let flowCoordinator = AuthenticationFlowCoordinator(authenticationService: AuthenticationService.mock, qrCodeLoginService: QRCodeLoginServiceMock(), bugReportService: BugReportServiceMock(), navigationRootCoordinator: navigationRootCoordinator, diff --git a/ElementX/Sources/UITests/UITestsScreenIdentifier.swift b/ElementX/Sources/UITests/UITestsScreenIdentifier.swift index 043b0b3778..b725b2060a 100644 --- a/ElementX/Sources/UITests/UITestsScreenIdentifier.swift +++ b/ElementX/Sources/UITests/UITestsScreenIdentifier.swift @@ -20,7 +20,6 @@ enum UITestsScreenIdentifier: String { case createPoll case createRoom case createRoomNoUsers - case login case roomLayoutBottom case roomLayoutMiddle case roomLayoutTop diff --git a/UITests/Sources/AuthenticationFlowCoordinatorUITests.swift b/UITests/Sources/AuthenticationFlowCoordinatorUITests.swift index e65b8c7875..0e38601c4b 100644 --- a/UITests/Sources/AuthenticationFlowCoordinatorUITests.swift +++ b/UITests/Sources/AuthenticationFlowCoordinatorUITests.swift @@ -19,6 +19,10 @@ class AuthenticationFlowCoordinatorUITests: XCTestCase { // Server Confirmation: Tap continue button app.buttons[A11yIdentifiers.serverConfirmationScreen.continue].tap() + // Login Screen: Wait for continue button to appear + let continueButton = app.buttons[A11yIdentifiers.loginScreen.continue] + XCTAssertTrue(continueButton.waitForExistence(timeout: 2.0)) + // Login Screen: Enter valid credentials app.textFields[A11yIdentifiers.loginScreen.emailUsername].clearAndTypeText("alice\n") app.secureTextFields[A11yIdentifiers.loginScreen.password].clearAndTypeText("12345678") @@ -39,20 +43,43 @@ class AuthenticationFlowCoordinatorUITests: XCTestCase { // Server Confirmation: Tap continue button app.buttons[A11yIdentifiers.serverConfirmationScreen.continue].tap() + // Login Screen: Wait for continue button to appear + let continueButton = app.buttons[A11yIdentifiers.loginScreen.continue] + XCTAssertTrue(continueButton.waitForExistence(timeout: 2.0)) + // Login Screen: Enter invalid credentials app.textFields[A11yIdentifiers.loginScreen.emailUsername].clearAndTypeText("alice") app.secureTextFields[A11yIdentifiers.loginScreen.password].clearAndTypeText("87654321") - // Login Screen: Tap next - let nextButton = app.buttons[A11yIdentifiers.loginScreen.continue] - XCTAssertTrue(nextButton.waitForExistence(timeout: 2.0)) - XCTAssertTrue(nextButton.isEnabled) - nextButton.tap() + // Login Screen: Tap continue + XCTAssertTrue(continueButton.isEnabled) + continueButton.tap() // Then login should fail. XCTAssertTrue(app.alerts.element.waitForExistence(timeout: 2.0), "An error alert should be shown when attempting login with invalid credentials.") } + func testLoginWithUnsupportedUserID() async throws { + // Given the authentication flow. + let app = Application.launch(.authenticationFlow) + + // Splash Screen: Tap get started button + app.buttons[A11yIdentifiers.authenticationStartScreen.signIn].tap() + + // Server Confirmation: Tap continue button + app.buttons[A11yIdentifiers.serverConfirmationScreen.continue].tap() + + // Login Screen: Wait for continue button to appear + let continueButton = app.buttons[A11yIdentifiers.loginScreen.continue] + XCTAssertTrue(continueButton.waitForExistence(timeout: 2.0)) + + // When entering a username on a homeserver with an unsupported flow. + app.textFields[A11yIdentifiers.loginScreen.emailUsername].clearAndTypeText("@test:server.net\n") + + // Then the screen should not allow login to continue. + try await app.assertScreenshot(.authenticationFlow, step: 1) + } + func testSelectingOIDCServer() { // Given the authentication flow. let app = Application.launch(.authenticationFlow) diff --git a/UITests/Sources/LoginScreenUITests.swift b/UITests/Sources/LoginScreenUITests.swift deleted file mode 100644 index 2275e1648a..0000000000 --- a/UITests/Sources/LoginScreenUITests.swift +++ /dev/null @@ -1,35 +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 XCTest - -@MainActor -class LoginScreenUITests: XCTestCase { - func testMatrixDotOrg() async throws { - // Given the initial login screen which defaults to matrix.org. - let app = Application.launch(.login) - try await app.assertScreenshot(.login) - - // When typing in a username and password. - app.textFields[A11yIdentifiers.loginScreen.emailUsername].clearAndTypeText("@test:matrix.org") - app.secureTextFields[A11yIdentifiers.loginScreen.password].clearAndTypeText("12345678") - - // Then the form should be ready to submit. - try await app.assertScreenshot(.login, step: 0) - } - - func testUnsupported() async throws { - // Given the initial login screen. - let app = Application.launch(.login) - - // When entering a username on a homeserver with an unsupported flow. - app.textFields[A11yIdentifiers.loginScreen.emailUsername].clearAndTypeText("@test:server.net\n") - - // Then the screen should not allow login to continue. - try await app.assertScreenshot(.login, step: 1) - } -} diff --git a/UITests/Sources/__Snapshots__/Application/authenticationFlow-1-iPad-10th-generation-en-GB.UI.png b/UITests/Sources/__Snapshots__/Application/authenticationFlow-1-iPad-10th-generation-en-GB.UI.png new file mode 100644 index 0000000000..c65a1a76d9 --- /dev/null +++ b/UITests/Sources/__Snapshots__/Application/authenticationFlow-1-iPad-10th-generation-en-GB.UI.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9b059e871878224c54b4b69d59f95394cf73c5d86e0f83ea030808efb9b48767 +size 88666 diff --git a/UITests/Sources/__Snapshots__/Application/authenticationFlow-1-iPhone-15-en-GB.UI.png b/UITests/Sources/__Snapshots__/Application/authenticationFlow-1-iPhone-15-en-GB.UI.png new file mode 100644 index 0000000000..5de379a22c --- /dev/null +++ b/UITests/Sources/__Snapshots__/Application/authenticationFlow-1-iPhone-15-en-GB.UI.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:58bcbecf677ba5b1ce9fc86a541e1df0389464677b882d34903be09512c65f46 +size 94767 diff --git a/UITests/Sources/__Snapshots__/Application/login-0-iPad-10th-generation-en-GB.UI.png b/UITests/Sources/__Snapshots__/Application/login-0-iPad-10th-generation-en-GB.UI.png deleted file mode 100644 index 7bde1d70de..0000000000 --- a/UITests/Sources/__Snapshots__/Application/login-0-iPad-10th-generation-en-GB.UI.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:bbe00c86de31bcf15f2c80631a11ea4ff89a4b6d220209f18c11499750472e44 -size 86311 diff --git a/UITests/Sources/__Snapshots__/Application/login-0-iPhone-16-en-GB.UI.png b/UITests/Sources/__Snapshots__/Application/login-0-iPhone-16-en-GB.UI.png deleted file mode 100644 index a08e21ecba..0000000000 --- a/UITests/Sources/__Snapshots__/Application/login-0-iPhone-16-en-GB.UI.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:ac640dca1b9e754b7ecdb958568473270838728f7dcf2d6a638b3495541f4420 -size 89495 diff --git a/UITests/Sources/__Snapshots__/Application/login-1-iPad-10th-generation-en-GB.UI.png b/UITests/Sources/__Snapshots__/Application/login-1-iPad-10th-generation-en-GB.UI.png deleted file mode 100644 index 9544d2d882..0000000000 --- a/UITests/Sources/__Snapshots__/Application/login-1-iPad-10th-generation-en-GB.UI.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:4f14c2216af345d3f620642f344f60fc79e877e78e651b33e20c3ebc0e8ef752 -size 85957 diff --git a/UITests/Sources/__Snapshots__/Application/login-1-iPhone-16-en-GB.UI.png b/UITests/Sources/__Snapshots__/Application/login-1-iPhone-16-en-GB.UI.png deleted file mode 100644 index 4e9a7119dd..0000000000 --- a/UITests/Sources/__Snapshots__/Application/login-1-iPhone-16-en-GB.UI.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:b22d205a8a1f5d5889271a7fa93678112bebc8c8e73e21eaa8722d1e98bd427e -size 89727 diff --git a/UITests/Sources/__Snapshots__/Application/login-iPad-10th-generation-en-GB.UI.png b/UITests/Sources/__Snapshots__/Application/login-iPad-10th-generation-en-GB.UI.png deleted file mode 100644 index dcc8187fec..0000000000 --- a/UITests/Sources/__Snapshots__/Application/login-iPad-10th-generation-en-GB.UI.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:58e3224bd82abb9548d042e02a081ca5a73fd4685c2769d46f39d14da5769ace -size 84034 diff --git a/UITests/Sources/__Snapshots__/Application/login-iPhone-16-en-GB.UI.png b/UITests/Sources/__Snapshots__/Application/login-iPhone-16-en-GB.UI.png deleted file mode 100644 index 491ac73e17..0000000000 --- a/UITests/Sources/__Snapshots__/Application/login-iPhone-16-en-GB.UI.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:c626719a64e6f68e087f616f43ab6890a2c0be081c66b0bc51f6b308e1849197 -size 86066 diff --git a/UnitTests/Sources/AuthenticationServiceTests.swift b/UnitTests/Sources/AuthenticationServiceTests.swift index f3202862ae..e512db7318 100644 --- a/UnitTests/Sources/AuthenticationServiceTests.swift +++ b/UnitTests/Sources/AuthenticationServiceTests.swift @@ -29,7 +29,7 @@ class AuthenticationServiceTests: XCTestCase { XCTAssertEqual(service.flow, .login) XCTAssertEqual(service.homeserver.value, .mockMatrixDotOrg) - switch await service.login(username: "alice", password: "p4ssw0rd", initialDeviceName: nil, deviceID: nil) { + switch await service.login(username: "alice", password: "12345678", initialDeviceName: nil, deviceID: nil) { case .success: XCTAssertEqual(client.loginUsernamePasswordInitialDeviceNameDeviceIdCallsCount, 1) XCTAssertEqual(userSessionStore.userSessionForSessionDirectoriesPassphraseCallsCount, 1) @@ -55,9 +55,10 @@ class AuthenticationServiceTests: XCTestCase { } func testConfigureRegisterNoSupport() async { - setupMocks(clientConfiguration: .init(elementWellKnown: "")) + let homeserverAddress = "example.com" + setupMocks(serverAddress: homeserverAddress) - switch await service.configure(for: "matrix.org", flow: .register) { + switch await service.configure(for: homeserverAddress, flow: .register) { case .success: XCTFail("Configuration should have failed") case .failure(let error): @@ -70,14 +71,17 @@ class AuthenticationServiceTests: XCTestCase { // MARK: - Helpers - private func setupMocks(clientConfiguration: ClientSDKMock.Configuration = .init()) { - client = ClientSDKMock(configuration: clientConfiguration) + private func setupMocks(serverAddress: String = "matrix.org") { + let configuration: AuthenticationClientBuilderMock.Configuration = .init() + let clientBuilderFactory = AuthenticationClientBuilderFactoryMock(configuration: .init(builderConfiguration: configuration)) + + client = configuration.homeserverClients[serverAddress] userSessionStore = UserSessionStoreMock(configuration: .init()) encryptionKeyProvider = MockEncryptionKeyProvider() service = AuthenticationService(userSessionStore: userSessionStore, encryptionKeyProvider: encryptionKeyProvider, - clientBuilderFactory: AuthenticationClientBuilderFactoryMock(configuration: .init(builtClient: client)), + clientBuilderFactory: clientBuilderFactory, appSettings: ServiceLocator.shared.settings, appHooks: AppHooks()) } From 6997ba2b58bb55bc46286a75541efa83be53e5bb Mon Sep 17 00:00:00 2001 From: Doug Date: Fri, 20 Sep 2024 17:35:28 +0100 Subject: [PATCH 4/5] Add tests for the ServerConfirmationScreenViewModel. --- .../ServerConfirmationScreenViewModel.swift | 3 +- ...rverConfirmationScreenViewModelTests.swift | 114 +++++++++++++++++- 2 files changed, 114 insertions(+), 3 deletions(-) diff --git a/ElementX/Sources/Screens/Authentication/ServerConfirmationScreen/ServerConfirmationScreenViewModel.swift b/ElementX/Sources/Screens/Authentication/ServerConfirmationScreen/ServerConfirmationScreenViewModel.swift index 860939c7f6..bb7687a26f 100644 --- a/ElementX/Sources/Screens/Authentication/ServerConfirmationScreen/ServerConfirmationScreenViewModel.swift +++ b/ElementX/Sources/Screens/Authentication/ServerConfirmationScreen/ServerConfirmationScreenViewModel.swift @@ -66,7 +66,6 @@ class ServerConfirmationScreenViewModel: ServerConfirmationScreenViewModelType, // If the login mode is unknown, the service hasn't be configured and we need to do it now. // Otherwise we can continue the flow as server selection has been performed and succeeded. guard homeserver.loginMode == .unknown || authenticationService.flow != authenticationFlow else { - // TODO: [DOUG] Test this. actionsSubject.send(.confirm) return } @@ -86,7 +85,7 @@ class ServerConfirmationScreenViewModel: ServerConfirmationScreenViewModelType, case .slidingSyncNotAvailable: displayError(.slidingSync) case .registrationNotSupported: - displayError(.registration) // TODO: [DOUG] Test me! + displayError(.registration) default: displayError(.unknownError) } diff --git a/UnitTests/Sources/ServerConfirmationScreenViewModelTests.swift b/UnitTests/Sources/ServerConfirmationScreenViewModelTests.swift index 0d89417d09..9f9d3148d1 100644 --- a/UnitTests/Sources/ServerConfirmationScreenViewModelTests.swift +++ b/UnitTests/Sources/ServerConfirmationScreenViewModelTests.swift @@ -11,5 +11,117 @@ import XCTest @MainActor class ServerConfirmationScreenViewModelTests: XCTestCase { - // Nothing to test, the view model has no mutable state. + var clientBuilderFactory: AuthenticationClientBuilderFactoryMock! + var service: AuthenticationServiceProtocol! + + var viewModel: ServerConfirmationScreenViewModel! + var context: ServerConfirmationScreenViewModel.Context { viewModel.context } + + func testConfirmLoginWithoutConfiguration() async throws { + // Given a view model for login using a service that hasn't been configured. + setupViewModel(authenticationFlow: .login) + XCTAssertEqual(service.homeserver.value.loginMode, .unknown) + XCTAssertEqual(clientBuilderFactory.makeBuilderSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount, 0) + + // When continuing from the confirmation screen. + let deferred = deferFulfillment(viewModel.actions) { $0 == .confirm } + context.send(viewAction: .confirm) + try await deferred.fulfill() + + // Then a call to configure service should be made. + XCTAssertEqual(clientBuilderFactory.makeBuilderSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount, 1) + XCTAssertNotEqual(service.homeserver.value.loginMode, .unknown) + } + + func testConfirmLoginAfterConfiguration() async throws { + // Given a view model for login using a service that has already been configured (via the server selection screen). + setupViewModel(authenticationFlow: .login) + guard case .success = await service.configure(for: viewModel.state.homeserverAddress, flow: .login) else { + XCTFail("The configuration should succeed.") + return + } + XCTAssertNotEqual(service.homeserver.value.loginMode, .unknown) + XCTAssertEqual(clientBuilderFactory.makeBuilderSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount, 1) + + // When continuing from the confirmation screen. + let deferred = deferFulfillment(viewModel.actions) { $0 == .confirm } + context.send(viewAction: .confirm) + try await deferred.fulfill() + + // Then the configured homeserver should be used and no additional call should be made to the service. + XCTAssertEqual(clientBuilderFactory.makeBuilderSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount, 1) + } + + func testConfirmRegisterWithoutConfiguration() async throws { + // Given a view model for registration using a service that hasn't been configured. + setupViewModel(authenticationFlow: .register) + XCTAssertEqual(service.homeserver.value.loginMode, .unknown) + XCTAssertEqual(clientBuilderFactory.makeBuilderSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount, 0) + + // When continuing from the confirmation screen. + let deferred = deferFulfillment(viewModel.actions) { $0 == .confirm } + context.send(viewAction: .confirm) + try await deferred.fulfill() + + // Then a call to configure service should be made. + XCTAssertEqual(clientBuilderFactory.makeBuilderSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount, 1) + XCTAssertNotEqual(service.homeserver.value.loginMode, .unknown) + } + + func testConfirmRegisterAfterConfiguration() async throws { + // Given a view model for registration using a service that has already been configured (via the server selection screen). + setupViewModel(authenticationFlow: .register) + guard case .success = await service.configure(for: viewModel.state.homeserverAddress, flow: .register) else { + XCTFail("The configuration should succeed.") + return + } + XCTAssertNotEqual(service.homeserver.value.loginMode, .unknown) + XCTAssertEqual(clientBuilderFactory.makeBuilderSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount, 1) + + // When continuing from the confirmation screen. + let deferred = deferFulfillment(viewModel.actions) { $0 == .confirm } + context.send(viewAction: .confirm) + try await deferred.fulfill() + + // Then the configured homeserver should be used and no additional call should be made to the service. + XCTAssertEqual(clientBuilderFactory.makeBuilderSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount, 1) + } + + 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) + 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 configured homeserver should be used and no additional call should be made to the service. + XCTAssertEqual(clientBuilderFactory.makeBuilderSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount, 1) + XCTAssertEqual(context.alertInfo?.id, .registration) + } + + // MARK: - Helpers + + private func setupViewModel(authenticationFlow: AuthenticationFlow, elementWellKnown: Bool = true) { + let client = ClientSDKMock(configuration: elementWellKnown ? .init() : .init(elementWellKnown: "")) + let configuration = AuthenticationClientBuilderMock.Configuration(homeserverClients: ["matrix.org": client], + qrCodeClient: client) + + clientBuilderFactory = AuthenticationClientBuilderFactoryMock(configuration: .init(builderConfiguration: configuration)) + service = AuthenticationService(userSessionStore: UserSessionStoreMock(configuration: .init()), + encryptionKeyProvider: EncryptionKeyProvider(), + clientBuilderFactory: clientBuilderFactory, + appSettings: ServiceLocator.shared.settings, + appHooks: AppHooks()) + + viewModel = ServerConfirmationScreenViewModel(authenticationService: service, + authenticationFlow: authenticationFlow, + slidingSyncLearnMoreURL: ServiceLocator.shared.settings.slidingSyncLearnMoreURL, + userIndicatorController: UserIndicatorControllerMock()) + } } From 5af32d9b4ecd9d6327ee61dfd79ec1cfcc6a7762 Mon Sep 17 00:00:00 2001 From: Doug Date: Mon, 23 Sep 2024 10:12:52 +0100 Subject: [PATCH 5/5] Remove redundant view state and test for it. --- .../ServerConfirmationScreenModels.swift | 2 -- .../ServerConfirmationScreenViewModel.swift | 4 +--- .../ServerConfigurationScreenViewStateTests.swift | 11 ++--------- 3 files changed, 3 insertions(+), 14 deletions(-) diff --git a/ElementX/Sources/Screens/Authentication/ServerConfirmationScreen/ServerConfirmationScreenModels.swift b/ElementX/Sources/Screens/Authentication/ServerConfirmationScreen/ServerConfirmationScreenModels.swift index 7a89f5c0b2..811a7fa676 100644 --- a/ElementX/Sources/Screens/Authentication/ServerConfirmationScreen/ServerConfirmationScreenModels.swift +++ b/ElementX/Sources/Screens/Authentication/ServerConfirmationScreen/ServerConfirmationScreenModels.swift @@ -19,8 +19,6 @@ struct ServerConfirmationScreenViewState: BindableState { var homeserverAddress: String /// The flow being attempted on the selected homeserver. let authenticationFlow: AuthenticationFlow - /// Whether or not the homeserver supports registration. - var homeserverSupportsRegistration = false /// The presentation anchor used for OIDC authentication. var window: UIWindow? diff --git a/ElementX/Sources/Screens/Authentication/ServerConfirmationScreen/ServerConfirmationScreenViewModel.swift b/ElementX/Sources/Screens/Authentication/ServerConfirmationScreen/ServerConfirmationScreenViewModel.swift index bb7687a26f..29563103da 100644 --- a/ElementX/Sources/Screens/Authentication/ServerConfirmationScreen/ServerConfirmationScreenViewModel.swift +++ b/ElementX/Sources/Screens/Authentication/ServerConfirmationScreen/ServerConfirmationScreenViewModel.swift @@ -33,15 +33,13 @@ class ServerConfirmationScreenViewModel: ServerConfirmationScreenViewModelType, let homeserver = authenticationService.homeserver.value super.init(initialViewState: ServerConfirmationScreenViewState(homeserverAddress: homeserver.address, - authenticationFlow: authenticationFlow, - homeserverSupportsRegistration: homeserver.supportsRegistration)) + authenticationFlow: authenticationFlow)) authenticationService.homeserver .receive(on: DispatchQueue.main) .sink { [weak self] homeserver in guard let self else { return } state.homeserverAddress = homeserver.address - state.homeserverSupportsRegistration = homeserver.supportsRegistration } .store(in: &cancellables) } diff --git a/UnitTests/Sources/ServerConfigurationScreenViewStateTests.swift b/UnitTests/Sources/ServerConfigurationScreenViewStateTests.swift index b859e17414..d6a0428dae 100644 --- a/UnitTests/Sources/ServerConfigurationScreenViewStateTests.swift +++ b/UnitTests/Sources/ServerConfigurationScreenViewStateTests.swift @@ -26,18 +26,11 @@ class ServerConfirmationScreenViewStateTests: XCTestCase { func testRegisterMessageString() { let matrixDotOrgRegister = ServerConfirmationScreenViewState(homeserverAddress: LoginHomeserver.mockMatrixDotOrg.address, - authenticationFlow: .register, - homeserverSupportsRegistration: true) + authenticationFlow: .register) XCTAssertEqual(matrixDotOrgRegister.message, L10n.screenServerConfirmationMessageRegister, "The registration message should always be the same.") let oidcRegister = ServerConfirmationScreenViewState(homeserverAddress: LoginHomeserver.mockOIDC.address, - authenticationFlow: .register, - homeserverSupportsRegistration: true) + authenticationFlow: .register) XCTAssertEqual(oidcRegister.message, L10n.screenServerConfirmationMessageRegister, "The registration message should always be the same.") - - let otherRegister = ServerConfirmationScreenViewState(homeserverAddress: LoginHomeserver.mockBasicServer.address, - authenticationFlow: .register, - homeserverSupportsRegistration: false) - XCTAssertEqual(otherRegister.message, L10n.errorAccountCreationNotPossible, "The registration message should always be the same.") } }