mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-11-05 11:58:32 +00:00
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:
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -81,7 +81,7 @@ final class QRCodeLoginService: QRCodeLoginServiceProtocol {
|
||||
sessionDirectories = .init()
|
||||
}
|
||||
|
||||
private func userSession(for client: Client) async -> Result<UserSessionProtocol, QRCodeLoginServiceError> {
|
||||
private func userSession(for client: ClientProtocol) async -> Result<UserSessionProtocol, QRCodeLoginServiceError> {
|
||||
switch await userSessionStore.userSession(for: client, sessionDirectories: sessionDirectories, passphrase: passphrase) {
|
||||
case .success(let session):
|
||||
return .success(session)
|
||||
|
||||
@@ -60,7 +60,7 @@ class UserSessionStore: UserSessionStoreProtocol {
|
||||
}
|
||||
}
|
||||
|
||||
func userSession(for client: Client, sessionDirectories: SessionDirectories, passphrase: String?) async -> Result<UserSessionProtocol, UserSessionStoreError> {
|
||||
func userSession(for client: ClientProtocol, sessionDirectories: SessionDirectories, passphrase: String?) async -> Result<UserSessionProtocol, UserSessionStoreError> {
|
||||
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)
|
||||
|
||||
@@ -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<UserSessionProtocol, UserSessionStoreError>
|
||||
|
||||
/// 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<UserSessionProtocol, UserSessionStoreError>
|
||||
func userSession(for client: ClientProtocol, sessionDirectories: SessionDirectories, passphrase: String?) async -> Result<UserSessionProtocol, UserSessionStoreError>
|
||||
|
||||
/// Logs out of the specified session.
|
||||
func logout(userSession: UserSessionProtocol)
|
||||
|
||||
Reference in New Issue
Block a user