#40: Add basic AuthenticationService and missing UI tests.

* Add MockAuthenticationService and ServerSelectionUITests.
* Add tests covering the Authentication flow.
This commit is contained in:
Doug
2022-07-04 10:00:27 +01:00
committed by GitHub
parent 0a199ee611
commit 26d8affaa8
29 changed files with 641 additions and 203 deletions
@@ -0,0 +1,82 @@
//
// AuthenticationService.swift
// ElementX
//
// Created by Doug on 29/06/2022.
// Copyright © 2022 Element. All rights reserved.
//
import Foundation
import MatrixRustSDK
class AuthenticationService: AuthenticationServiceProtocol {
// MARK: - Properties
// MARK: Private
private(set) var homeserver: LoginHomeserver = LoginHomeserver(address: BuildSettings.defaultHomeserverURLString)
private let userSessionStore: UserSessionStoreProtocol
// MARK: - Setup
init(userSessionStore: UserSessionStoreProtocol) {
self.userSessionStore = userSessionStore
}
// MARK: - Public
func startLogin(for homeserverAddress: String) async -> Result<Void, AuthenticationServiceError> {
homeserver = LoginHomeserver(address: homeserverAddress)
return .success(())
}
func login(username: String, password: String) async -> Result<UserSessionProtocol, AuthenticationServiceError> {
Benchmark.startTrackingForIdentifier("Login", message: "Started new login")
// Workaround whilst the SDK requires a full MXID.
let username = username.isMatrixUserID ? username : "@\(username):\(homeserver.address)"
let basePath = userSessionStore.baseDirectoryPath(for: username)
let builder = ClientBuilder()
.basePath(path: basePath)
.username(username: username)
let loginTask: Task<Client, Error> = Task.detached {
let client = try builder.build()
try client.login(username: username, password: password)
return client
}
switch await loginTask.result {
case .success(let client):
Benchmark.endTrackingForIdentifier("Login", message: "Finished login")
return await userSession(for: client)
case .failure(let error):
Benchmark.endTrackingForIdentifier("Login", message: "Login failed")
MXLog.error("Failed logging in with error: \(error)")
guard let error = error as? ClientError else { return .failure(.failedLoggingIn) }
switch error.code {
case .forbidden:
return .failure(.invalidCredentials)
case .userDeactivated:
return .failure(.accountDeactivated)
default:
return .failure(.failedLoggingIn)
}
}
}
// MARK: - Private
private func userSession(for client: Client) async -> Result<UserSessionProtocol, AuthenticationServiceError> {
switch await userSessionStore.userSession(for: client) {
case .success(let clientProxy):
return .success(clientProxy)
case .failure:
return .failure(.failedLoggingIn)
}
}
}
@@ -0,0 +1,26 @@
//
// AuthenticationServiceProtocol.swift
// ElementX
//
// Created by Doug on 29/06/2022.
// Copyright © 2022 Element. All rights reserved.
//
import Foundation
enum AuthenticationServiceError: Error {
case invalidServer
case invalidCredentials
case accountDeactivated
case failedLoggingIn
}
@MainActor
protocol AuthenticationServiceProtocol {
var homeserver: LoginHomeserver { get }
/// Sets up the service for login on the specified homeserver address.
func startLogin(for homeserverAddress: String) async -> Result<Void, AuthenticationServiceError>
/// Performs a password login using the current homeserver.
func login(username: String, password: String) async -> Result<UserSessionProtocol, AuthenticationServiceError>
}
@@ -0,0 +1,45 @@
//
// MockAuthenticationService.swift
// ElementX
//
// Created by Doug on 29/06/2022.
// Copyright © 2022 Element. All rights reserved.
//
import Foundation
class MockAuthenticationService: AuthenticationServiceProtocol {
let validCredentials = (username: "alice", password: "12345678")
private(set) var homeserver: LoginHomeserver = .mockMatrixDotOrg
func startLogin(for homeserverAddress: String) async -> Result<Void, AuthenticationServiceError> {
// Map the address to the mock homeservers
if LoginHomeserver.mockMatrixDotOrg.address.contains(homeserverAddress) {
homeserver = .mockMatrixDotOrg
return .success(())
} else if LoginHomeserver.mockOIDC.address.contains(homeserverAddress) {
homeserver = .mockOIDC
return .success(())
} else if LoginHomeserver.mockBasicServer.address.contains(homeserverAddress) {
homeserver = .mockBasicServer
return .success(())
} else if LoginHomeserver.mockUnsupported.address.contains(homeserverAddress) {
homeserver = .mockUnsupported
return .success(())
} else {
// Otherwise fail with an invalid server.
return .failure(.invalidServer)
}
}
func login(username: String, password: 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 = MockUserSession(clientProxy: MockClientProxy(userIdentifier: username),
mediaProvider: MockMediaProvider())
return .success(userSession)
}
}
@@ -0,0 +1,30 @@
//
// ClientError.swift
// ElementX
//
// Created by Doug on 30/06/2022.
// Copyright © 2022 Element. All rights reserved.
//
import Foundation
import MatrixRustSDK
enum MatrixErrorCode: String, CaseIterable {
case unknown = "M_UNKNOWN"
case userDeactivated = "M_USER_DEACTIVATED"
case forbidden = "M_FORBIDDEN"
}
extension ClientError {
var code: MatrixErrorCode {
guard case let .Generic(message) = self else { return .unknown }
for code in MatrixErrorCode.allCases {
if message.contains(code.rawValue) {
return code
}
}
return .unknown
}
}
@@ -19,6 +19,7 @@ enum ClientProxyError: Error {
case failedRetrievingAvatarURL
case failedRetrievingDisplayName
case failedRetrievingSessionVerificationController
case failedLoadingMedia
}
protocol ClientProxyProtocol {
@@ -0,0 +1,39 @@
//
// MockClientProxy.swift
// ElementX
//
// Created by Doug on 29/06/2022.
// Copyright © 2022 Element. All rights reserved.
//
import Combine
import MatrixRustSDK
struct MockClientProxy: ClientProxyProtocol {
let callbacks = PassthroughSubject<ClientProxyCallback, Never>()
let userIdentifier: String
let rooms = [RoomProxy]()
func loadUserDisplayName() async -> Result<String, ClientProxyError> {
.failure(.failedRetrievingDisplayName)
}
func loadUserAvatarURLString() async -> Result<String, ClientProxyError> {
.failure(.failedRetrievingAvatarURL)
}
func mediaSourceForURLString(_ urlString: String) -> MatrixRustSDK.MediaSource {
MatrixRustSDK.mediaSourceFromUrl(url: urlString)
}
func loadMediaContentForSource(_ source: MatrixRustSDK.MediaSource) throws -> Data {
throw ClientProxyError.failedLoadingMedia
}
func sessionVerificationControllerProxy() async -> Result<SessionVerificationControllerProxyProtocol, ClientProxyError> {
.failure(.failedRetrievingSessionVerificationController)
}
}
@@ -0,0 +1,17 @@
//
// MockUserSession.swift
// ElementX
//
// Created by Doug on 29/06/2022.
// Copyright © 2022 Element. All rights reserved.
//
import Combine
struct MockUserSession: UserSessionProtocol {
let callbacks = PassthroughSubject<UserSessionCallback, Never>()
let sessionVerificationController: SessionVerificationControllerProxyProtocol? = nil
let clientProxy: ClientProxyProtocol
let mediaProvider: MediaProviderProtocol
}
@@ -98,8 +98,6 @@ class UserSessionStore: UserSessionStoreProtocol {
}
private func setupProxyForClient(_ client: Client) async -> Result<ClientProxyProtocol, UserSessionStoreError> {
Benchmark.endTrackingForIdentifier("Login", message: "Finished login")
do {
let accessToken = try client.restoreToken()
let userId = try client.userId()