mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-11-03 16:10:20 +00:00
Add support for Account Provisioning links. (#4108)
* Add support for account provisioning links and route them to the authentication flow. * Use the provisioning parameters to configure the authentication flow. * Add UI tests for the provisioned authentication flow. * Record new preview snapshots. * Add unit tests. * Make the domain configurable in the app settings. * Use the loginHint in the login screen too.
This commit is contained in:
@@ -9,6 +9,117 @@ import XCTest
|
||||
|
||||
@testable import ElementX
|
||||
|
||||
@MainActor
|
||||
class AuthenticationStartScreenViewModelTests: XCTestCase {
|
||||
// Nothing to test, the view model has no mutable state.
|
||||
var clientBuilderFactory: AuthenticationClientBuilderFactoryMock!
|
||||
var client: ClientSDKMock!
|
||||
var authenticationService: AuthenticationServiceProtocol!
|
||||
|
||||
var viewModel: AuthenticationStartScreenViewModel!
|
||||
var context: AuthenticationStartScreenViewModel.Context { viewModel.context }
|
||||
|
||||
func testInitialState() async throws {
|
||||
// Given a view model that has no provisioning parameters.
|
||||
setupViewModel()
|
||||
XCTAssertEqual(authenticationService.homeserver.value.loginMode, .unknown)
|
||||
XCTAssertEqual(client.urlForOidcOidcConfigurationPromptLoginHintCallsCount, 0)
|
||||
|
||||
// When tapping any of the buttons on the screen
|
||||
let actions: [(AuthenticationStartScreenViewAction, AuthenticationStartScreenViewModelAction)] = [
|
||||
(.loginWithQR, .loginWithQR),
|
||||
(.login, .login),
|
||||
(.register, .register),
|
||||
(.reportProblem, .reportProblem)
|
||||
]
|
||||
|
||||
for action in actions {
|
||||
let deferred = deferFulfillment(viewModel.actions) { $0 == action.1 }
|
||||
context.send(viewAction: action.0)
|
||||
try await deferred.fulfill()
|
||||
|
||||
// Then the authentication service should not be used yet.
|
||||
XCTAssertEqual(clientBuilderFactory.makeBuilderSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount, 0)
|
||||
XCTAssertEqual(client.urlForOidcOidcConfigurationPromptLoginHintCallsCount, 0)
|
||||
XCTAssertEqual(authenticationService.homeserver.value.loginMode, .unknown)
|
||||
}
|
||||
}
|
||||
|
||||
func testProvisionedOIDCState() async throws {
|
||||
// Given a view model that has been provisioned with a server that supports OIDC.
|
||||
setupViewModel(provisioningParameters: .init(accountProvider: "company.com", loginHint: "user@company.com"))
|
||||
XCTAssertEqual(authenticationService.homeserver.value.loginMode, .unknown)
|
||||
XCTAssertEqual(client.urlForOidcOidcConfigurationPromptLoginHintCallsCount, 0)
|
||||
|
||||
// When tapping the login button the authentication service should be used and the screen
|
||||
// should request to continue the flow without any server selection needed.
|
||||
let deferred = deferFulfillment(viewModel.actions) { $0.isLoginDirectlyWithOIDC }
|
||||
context.send(viewAction: .login)
|
||||
try await deferred.fulfill()
|
||||
|
||||
XCTAssertEqual(clientBuilderFactory.makeBuilderSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount, 1)
|
||||
XCTAssertEqual(client.urlForOidcOidcConfigurationPromptLoginHintCallsCount, 1)
|
||||
XCTAssertEqual(client.urlForOidcOidcConfigurationPromptLoginHintReceivedArguments?.prompt, .consent)
|
||||
XCTAssertEqual(client.urlForOidcOidcConfigurationPromptLoginHintReceivedArguments?.loginHint, "user@company.com")
|
||||
XCTAssertEqual(authenticationService.homeserver.value.loginMode, .oidc(supportsCreatePrompt: false))
|
||||
}
|
||||
|
||||
func testProvisionedPasswordState() async throws {
|
||||
// Given a view model that has been provisioned with a server that does not support OIDC.
|
||||
setupViewModel(provisioningParameters: .init(accountProvider: "company.com", loginHint: "user@company.com"), supportsOIDC: false)
|
||||
XCTAssertEqual(authenticationService.homeserver.value.loginMode, .unknown)
|
||||
XCTAssertEqual(client.urlForOidcOidcConfigurationPromptLoginHintCallsCount, 0)
|
||||
|
||||
// When tapping the login button the authentication service should be used and the screen
|
||||
// should request to continue the flow without any server selection needed.
|
||||
let deferred = deferFulfillment(viewModel.actions) { $0.isLoginDirectlyWithPassword }
|
||||
context.send(viewAction: .login)
|
||||
try await deferred.fulfill()
|
||||
|
||||
// Then a call to configure service should be made.
|
||||
XCTAssertEqual(clientBuilderFactory.makeBuilderSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount, 1)
|
||||
XCTAssertEqual(authenticationService.homeserver.value.loginMode, .password)
|
||||
}
|
||||
|
||||
// MARK: - Helpers
|
||||
|
||||
private func setupViewModel(provisioningParameters: AccountProvisioningParameters? = nil, supportsOIDC: Bool = true) {
|
||||
// Manually create a configuration as the default homeserver address setting is immutable.
|
||||
client = ClientSDKMock(configuration: .init(oidcLoginURL: supportsOIDC ? "https://account.company.com/authorize" : nil,
|
||||
supportsOIDCCreatePrompt: false,
|
||||
supportsPasswordLogin: true))
|
||||
let configuration = AuthenticationClientBuilderMock.Configuration(homeserverClients: ["company.com": client],
|
||||
qrCodeClient: client)
|
||||
|
||||
clientBuilderFactory = AuthenticationClientBuilderFactoryMock(configuration: .init(builderConfiguration: configuration))
|
||||
authenticationService = AuthenticationService(userSessionStore: UserSessionStoreMock(configuration: .init()),
|
||||
encryptionKeyProvider: EncryptionKeyProvider(),
|
||||
clientBuilderFactory: clientBuilderFactory,
|
||||
appSettings: ServiceLocator.shared.settings,
|
||||
appHooks: AppHooks())
|
||||
|
||||
viewModel = AuthenticationStartScreenViewModel(authenticationService: authenticationService,
|
||||
provisioningParameters: provisioningParameters,
|
||||
isBugReportServiceEnabled: true,
|
||||
appSettings: ServiceLocator.shared.settings,
|
||||
userIndicatorController: UserIndicatorControllerMock())
|
||||
|
||||
// Add a fake window in order for the OIDC flow to continue
|
||||
viewModel.context.send(viewAction: .updateWindow(UIWindow()))
|
||||
}
|
||||
}
|
||||
|
||||
extension AuthenticationStartScreenViewModelAction {
|
||||
var isLoginDirectlyWithOIDC: Bool {
|
||||
switch self {
|
||||
case .loginDirectlyWithOIDC: true
|
||||
default: false
|
||||
}
|
||||
}
|
||||
|
||||
var isLoginDirectlyWithPassword: Bool {
|
||||
switch self {
|
||||
case .loginDirectlyWithPassword: true
|
||||
default: false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,24 +17,6 @@ class LoginScreenViewModelTests: XCTestCase {
|
||||
var clientBuilderFactory: AuthenticationClientBuilderFactoryMock!
|
||||
var service: AuthenticationServiceProtocol!
|
||||
|
||||
private func setupViewModel(homeserverAddress: String = "example.com") async {
|
||||
clientBuilderFactory = AuthenticationClientBuilderFactoryMock(configuration: .init())
|
||||
service = AuthenticationService(userSessionStore: UserSessionStoreMock(configuration: .init()),
|
||||
encryptionKeyProvider: EncryptionKeyProvider(),
|
||||
clientBuilderFactory: clientBuilderFactory,
|
||||
appSettings: ServiceLocator.shared.settings,
|
||||
appHooks: AppHooks())
|
||||
|
||||
guard case .success = await service.configure(for: homeserverAddress, flow: .login) else {
|
||||
XCTFail("A valid server should be configured for the test.")
|
||||
return
|
||||
}
|
||||
|
||||
viewModel = LoginScreenViewModel(authenticationService: service,
|
||||
userIndicatorController: UserIndicatorControllerMock(),
|
||||
analytics: ServiceLocator.shared.analytics)
|
||||
}
|
||||
|
||||
func testBasicServer() async {
|
||||
// Given the view model configured for a basic server example.com that only supports password authentication.
|
||||
await setupViewModel()
|
||||
@@ -160,4 +142,36 @@ class LoginScreenViewModelTests: XCTestCase {
|
||||
// Then the view state should be updated to show an alert.
|
||||
XCTAssertEqual(context.alertInfo?.id, .unknown, "An alert should be shown to the user.")
|
||||
}
|
||||
|
||||
func testLoginHint() async throws {
|
||||
await setupViewModel(loginHint: "")
|
||||
XCTAssertEqual(context.username, "")
|
||||
|
||||
await setupViewModel(loginHint: "alice")
|
||||
XCTAssertEqual(context.username, "alice")
|
||||
|
||||
await setupViewModel(loginHint: "mxid:@alice:example.com")
|
||||
XCTAssertEqual(context.username, "@alice:example.com")
|
||||
}
|
||||
|
||||
// MARK: - Helpers
|
||||
|
||||
private func setupViewModel(homeserverAddress: String = "example.com", loginHint: String? = nil) async {
|
||||
clientBuilderFactory = AuthenticationClientBuilderFactoryMock(configuration: .init())
|
||||
service = AuthenticationService(userSessionStore: UserSessionStoreMock(configuration: .init()),
|
||||
encryptionKeyProvider: EncryptionKeyProvider(),
|
||||
clientBuilderFactory: clientBuilderFactory,
|
||||
appSettings: ServiceLocator.shared.settings,
|
||||
appHooks: AppHooks())
|
||||
|
||||
guard case .success = await service.configure(for: homeserverAddress, flow: .login) else {
|
||||
XCTFail("A valid server should be configured for the test.")
|
||||
return
|
||||
}
|
||||
|
||||
viewModel = LoginScreenViewModel(authenticationService: service,
|
||||
loginHint: loginHint,
|
||||
userIndicatorController: UserIndicatorControllerMock(),
|
||||
analytics: ServiceLocator.shared.analytics)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@ class ServerConfirmationScreenViewModelTests: XCTestCase {
|
||||
setupViewModel(authenticationFlow: .login)
|
||||
XCTAssertEqual(service.homeserver.value.loginMode, .unknown)
|
||||
XCTAssertEqual(clientBuilderFactory.makeBuilderSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount, 0)
|
||||
XCTAssertEqual(client.urlForOidcOidcConfigurationPromptCallsCount, 0)
|
||||
XCTAssertEqual(client.urlForOidcOidcConfigurationPromptLoginHintCallsCount, 0)
|
||||
|
||||
// When continuing from the confirmation screen.
|
||||
let deferred = deferFulfillment(viewModel.actions) { $0.isContinueWithOIDC }
|
||||
@@ -32,8 +32,8 @@ class ServerConfirmationScreenViewModelTests: XCTestCase {
|
||||
|
||||
// Then a call to configure service should be made.
|
||||
XCTAssertEqual(clientBuilderFactory.makeBuilderSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount, 1)
|
||||
XCTAssertEqual(client.urlForOidcOidcConfigurationPromptCallsCount, 1)
|
||||
XCTAssertEqual(client.urlForOidcOidcConfigurationPromptReceivedArguments?.prompt, .consent)
|
||||
XCTAssertEqual(client.urlForOidcOidcConfigurationPromptLoginHintCallsCount, 1)
|
||||
XCTAssertEqual(client.urlForOidcOidcConfigurationPromptLoginHintReceivedArguments?.prompt, .consent)
|
||||
XCTAssertEqual(service.homeserver.value.loginMode, .oidc(supportsCreatePrompt: true))
|
||||
}
|
||||
|
||||
@@ -46,7 +46,7 @@ class ServerConfirmationScreenViewModelTests: XCTestCase {
|
||||
}
|
||||
XCTAssertEqual(service.homeserver.value.loginMode, .oidc(supportsCreatePrompt: true))
|
||||
XCTAssertEqual(clientBuilderFactory.makeBuilderSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount, 1)
|
||||
XCTAssertEqual(client.urlForOidcOidcConfigurationPromptCallsCount, 0)
|
||||
XCTAssertEqual(client.urlForOidcOidcConfigurationPromptLoginHintCallsCount, 0)
|
||||
|
||||
// When continuing from the confirmation screen.
|
||||
let deferred = deferFulfillment(viewModel.actions) { $0.isContinueWithOIDC }
|
||||
@@ -55,8 +55,8 @@ class ServerConfirmationScreenViewModelTests: XCTestCase {
|
||||
|
||||
// Then the configured homeserver should be used and no additional client should be built.
|
||||
XCTAssertEqual(clientBuilderFactory.makeBuilderSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount, 1)
|
||||
XCTAssertEqual(client.urlForOidcOidcConfigurationPromptCallsCount, 1)
|
||||
XCTAssertEqual(client.urlForOidcOidcConfigurationPromptReceivedArguments?.prompt, .consent)
|
||||
XCTAssertEqual(client.urlForOidcOidcConfigurationPromptLoginHintCallsCount, 1)
|
||||
XCTAssertEqual(client.urlForOidcOidcConfigurationPromptLoginHintReceivedArguments?.prompt, .consent)
|
||||
}
|
||||
|
||||
func testConfirmRegisterWithoutConfiguration() async throws {
|
||||
@@ -64,7 +64,7 @@ class ServerConfirmationScreenViewModelTests: XCTestCase {
|
||||
setupViewModel(authenticationFlow: .register)
|
||||
XCTAssertEqual(service.homeserver.value.loginMode, .unknown)
|
||||
XCTAssertEqual(clientBuilderFactory.makeBuilderSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount, 0)
|
||||
XCTAssertEqual(client.urlForOidcOidcConfigurationPromptCallsCount, 0)
|
||||
XCTAssertEqual(client.urlForOidcOidcConfigurationPromptLoginHintCallsCount, 0)
|
||||
|
||||
// When continuing from the confirmation screen.
|
||||
let deferred = deferFulfillment(viewModel.actions) { $0.isContinueWithOIDC }
|
||||
@@ -73,7 +73,7 @@ class ServerConfirmationScreenViewModelTests: XCTestCase {
|
||||
|
||||
// Then a call to configure service should be made.
|
||||
XCTAssertEqual(clientBuilderFactory.makeBuilderSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount, 1)
|
||||
XCTAssertEqual(client.urlForOidcOidcConfigurationPromptCallsCount, 1)
|
||||
XCTAssertEqual(client.urlForOidcOidcConfigurationPromptLoginHintCallsCount, 1)
|
||||
// The create prompt is broken: https://github.com/element-hq/matrix-authentication-service/issues/3429
|
||||
// XCTAssertEqual(client.urlForOidcOidcConfigurationPromptReceivedArguments?.prompt, .create)
|
||||
XCTAssertEqual(service.homeserver.value.loginMode, .oidc(supportsCreatePrompt: true))
|
||||
@@ -88,7 +88,7 @@ class ServerConfirmationScreenViewModelTests: XCTestCase {
|
||||
}
|
||||
XCTAssertEqual(service.homeserver.value.loginMode, .oidc(supportsCreatePrompt: true))
|
||||
XCTAssertEqual(clientBuilderFactory.makeBuilderSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount, 1)
|
||||
XCTAssertEqual(client.urlForOidcOidcConfigurationPromptCallsCount, 0)
|
||||
XCTAssertEqual(client.urlForOidcOidcConfigurationPromptLoginHintCallsCount, 0)
|
||||
|
||||
// When continuing from the confirmation screen.
|
||||
let deferred = deferFulfillment(viewModel.actions) { $0.isContinueWithOIDC }
|
||||
@@ -99,7 +99,7 @@ class ServerConfirmationScreenViewModelTests: XCTestCase {
|
||||
XCTAssertEqual(clientBuilderFactory.makeBuilderSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount, 1)
|
||||
// The create prompt is broken: https://github.com/element-hq/matrix-authentication-service/issues/3429
|
||||
// XCTAssertEqual(client.urlForOidcOidcConfigurationPromptReceivedArguments?.prompt, .create)
|
||||
XCTAssertEqual(client.urlForOidcOidcConfigurationPromptCallsCount, 1)
|
||||
XCTAssertEqual(client.urlForOidcOidcConfigurationPromptLoginHintCallsCount, 1)
|
||||
}
|
||||
|
||||
func testConfirmPasswordLoginWithoutConfiguration() async throws {
|
||||
@@ -107,7 +107,7 @@ class ServerConfirmationScreenViewModelTests: XCTestCase {
|
||||
setupViewModel(authenticationFlow: .login, supportsOIDC: false)
|
||||
XCTAssertEqual(service.homeserver.value.loginMode, .unknown)
|
||||
XCTAssertEqual(clientBuilderFactory.makeBuilderSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount, 0)
|
||||
XCTAssertEqual(client.urlForOidcOidcConfigurationPromptCallsCount, 0)
|
||||
XCTAssertEqual(client.urlForOidcOidcConfigurationPromptLoginHintCallsCount, 0)
|
||||
|
||||
// When continuing from the confirmation screen.
|
||||
let deferred = deferFulfillment(viewModel.actions) { $0.isContinueWithPassword }
|
||||
@@ -116,7 +116,7 @@ class ServerConfirmationScreenViewModelTests: XCTestCase {
|
||||
|
||||
// Then a call to configure service should be made, but not for the OIDC URL.
|
||||
XCTAssertEqual(clientBuilderFactory.makeBuilderSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount, 1)
|
||||
XCTAssertEqual(client.urlForOidcOidcConfigurationPromptCallsCount, 0)
|
||||
XCTAssertEqual(client.urlForOidcOidcConfigurationPromptLoginHintCallsCount, 0)
|
||||
XCTAssertEqual(service.homeserver.value.loginMode, .password)
|
||||
}
|
||||
|
||||
@@ -129,7 +129,7 @@ class ServerConfirmationScreenViewModelTests: XCTestCase {
|
||||
}
|
||||
XCTAssertEqual(service.homeserver.value.loginMode, .password)
|
||||
XCTAssertEqual(clientBuilderFactory.makeBuilderSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount, 1)
|
||||
XCTAssertEqual(client.urlForOidcOidcConfigurationPromptCallsCount, 0)
|
||||
XCTAssertEqual(client.urlForOidcOidcConfigurationPromptLoginHintCallsCount, 0)
|
||||
|
||||
// When continuing from the confirmation screen.
|
||||
let deferred = deferFulfillment(viewModel.actions) { $0.isContinueWithPassword }
|
||||
@@ -138,7 +138,7 @@ class ServerConfirmationScreenViewModelTests: XCTestCase {
|
||||
|
||||
// Then the configured homeserver should be used and no additional client should be built, nor a call to get the OIDC URL.
|
||||
XCTAssertEqual(clientBuilderFactory.makeBuilderSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount, 1)
|
||||
XCTAssertEqual(client.urlForOidcOidcConfigurationPromptCallsCount, 0)
|
||||
XCTAssertEqual(client.urlForOidcOidcConfigurationPromptLoginHintCallsCount, 0)
|
||||
}
|
||||
|
||||
func testRegistrationNotSupportedAlert() async throws {
|
||||
|
||||
Reference in New Issue
Block a user