mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-11-04 01:20:12 +00:00
#40: Add basic AuthenticationService and missing UI tests.
* Add MockAuthenticationService and ServerSelectionUITests. * Add tests covering the Authentication flow.
This commit is contained in:
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user