Configure the AuthenticationService later now that we have 2 flows on the start screen. (#3316)

* Don't query the homeserver until confirming it (or selecting a different one).

* Setup the infrastructure to test AuthenticationService.

Implement basic tests for configuration & password login.

* Use the real AuthenticationService with a mock Client in all of the tests.

* Add tests for the ServerConfirmationScreenViewModel.

* Remove redundant view state and test for it.
This commit is contained in:
Doug
2024-09-25 14:40:18 +01:00
committed by GitHub
parent af8c16150b
commit a8dbda90d9
44 changed files with 1209 additions and 298 deletions
@@ -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,
@@ -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)
}
}
@@ -10,31 +10,39 @@ 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
private let homeserverSubject: CurrentValueSubject<LoginHomeserver, Never>
var homeserver: CurrentValuePublisher<LoginHomeserver, Never> { 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
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<Void, AuthenticationServiceError> {
func configure(for homeserverAddress: String, flow: AuthenticationFlow) async -> Result<Void, AuthenticationServiceError> {
do {
var homeserver = LoginHomeserver(address: homeserverAddress, loginMode: .unknown)
@@ -57,7 +65,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,18 +163,24 @@ class AuthenticationService: AuthenticationServiceProtocol {
}
}
func reset() {
homeserverSubject.send(LoginHomeserver(address: appSettings.defaultHomeserverAddress, loginMode: .unknown))
flow = .login
client = nil
}
// 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() {
@@ -169,7 +188,7 @@ class AuthenticationService: AuthenticationServiceProtocol {
sessionDirectories = .init()
}
private func userSession(for client: Client) async -> Result<UserSessionProtocol, AuthenticationServiceError> {
private func userSession(for client: ClientProtocol) async -> Result<UserSessionProtocol, AuthenticationServiceError> {
switch await userSessionStore.userSession(for: client, sessionDirectories: sessionDirectories, passphrase: passphrase) {
case .success(let clientProxy):
return .success(clientProxy)
@@ -178,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())
}
@@ -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
@@ -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<LoginHomeserver, Never> { 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<Void, AuthenticationServiceError>
func configure(for homeserverAddress: String, flow: AuthenticationFlow) async -> Result<Void, AuthenticationServiceError>
/// Performs login using OIDC for the current homeserver.
func urlForOIDCLogin() async -> Result<OIDCAuthorizationDataProxy, AuthenticationServiceError>
/// 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<UserSessionProtocol, AuthenticationServiceError>
/// Completes registration using the credentials obtained via the helper URL.
func completeWebRegistration(using credentials: WebRegistrationCredentials) async -> Result<UserSessionProtocol, AuthenticationServiceError>
/// Resets the current configuration requiring `configure(for:flow:)` to be called again.
func reset()
}
// MARK: - OIDC
@@ -1,65 +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<LoginHomeserver, Never>
var homeserver: CurrentValuePublisher<LoginHomeserver, Never> { homeserverSubject.asCurrentValuePublisher() }
init(homeserver: LoginHomeserver = .mockMatrixDotOrg) {
homeserverSubject = .init(homeserver)
}
func configure(for homeserverAddress: String) async -> Result<Void, AuthenticationServiceError> {
// Map the address to the mock homeservers
if LoginHomeserver.mockMatrixDotOrg.address.contains(homeserverAddress) {
homeserverSubject.send(.mockMatrixDotOrg)
return .success(())
} else if LoginHomeserver.mockOIDC.address.contains(homeserverAddress) {
homeserverSubject.send(.mockOIDC)
return .success(())
} else if LoginHomeserver.mockBasicServer.address.contains(homeserverAddress) {
homeserverSubject.send(.mockBasicServer)
return .success(())
} else if LoginHomeserver.mockUnsupported.address.contains(homeserverAddress) {
homeserverSubject.send(.mockUnsupported)
return .success(())
} else {
// Otherwise fail with an invalid server.
return .failure(.invalidServer)
}
}
func urlForOIDCLogin() async -> Result<OIDCAuthorizationDataProxy, AuthenticationServiceError> {
.failure(.oidcError(.notSupported))
}
func abortOIDCLogin(data: OIDCAuthorizationDataProxy) async { }
func loginWithOIDCCallback(_ callbackURL: URL, data: OIDCAuthorizationDataProxy) async -> Result<UserSessionProtocol, AuthenticationServiceError> {
.failure(.oidcError(.notSupported))
}
func login(username: String, password: String, initialDeviceName: String?, deviceID: String?) async -> Result<UserSessionProtocol, AuthenticationServiceError> {
// 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<any UserSessionProtocol, AuthenticationServiceError> {
.failure(.failedLoggingIn)
}
}