Add an AccountProvider type and use it within the authentication flow. (#6103)

* Add an AccountProvider type and use it within AppSettings.

* Use the AccountProvider type within the Authentication flow too.

* Fix unit test compilation with Swift 6.4

* Make sure defaultAccountProvider takes allowOtherAccountProviders into account.

* Document a caveat on using AuthenticationService.homeserver.value.accountProvider.
This commit is contained in:
Doug
2026-09-14 15:25:33 +01:00
committed by GitHub
parent 26fddb8aa2
commit 64aae09c6d
29 changed files with 442 additions and 265 deletions
@@ -50,7 +50,7 @@ final class AuthenticationStartScreenViewModelTests {
try await deferred.fulfill()
// Then the authentication service should not be used yet.
#expect(clientFactory.makeAuthenticationClientHomeserverAddressSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount == 0)
#expect(clientFactory.makeAuthenticationClientServerNameOrBaseURLSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount == 0)
#expect(client.urlForOauthOauthConfigurationPromptLoginHintDeviceIdAdditionalScopesCallsCount == 0)
#expect(authenticationService.homeserver.value.loginMode == .unknown)
}
@@ -69,7 +69,7 @@ final class AuthenticationStartScreenViewModelTests {
context.send(viewAction: .login)
try await deferred.fulfill()
#expect(clientFactory.makeAuthenticationClientHomeserverAddressSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount == 1)
#expect(clientFactory.makeAuthenticationClientServerNameOrBaseURLSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount == 1)
#expect(client.urlForOauthOauthConfigurationPromptLoginHintDeviceIdAdditionalScopesCallsCount == 1)
#expect(client.urlForOauthOauthConfigurationPromptLoginHintDeviceIdAdditionalScopesReceivedArguments?.prompt == .consent)
#expect(client.urlForOauthOauthConfigurationPromptLoginHintDeviceIdAdditionalScopesReceivedArguments?.loginHint == "user@company.com")
@@ -90,14 +90,14 @@ final class AuthenticationStartScreenViewModelTests {
try await deferred.fulfill()
// Then a call to configure service should be made.
#expect(clientFactory.makeAuthenticationClientHomeserverAddressSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount == 1)
#expect(clientFactory.makeAuthenticationClientServerNameOrBaseURLSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount == 1)
#expect(authenticationService.homeserver.value.loginMode == .password)
}
@Test
func singleProviderOAuthState() async throws {
// Given a view model that for an app that only allows the use of a single provider that supports OAuth.
setAllowedAccountProviders(["company.com"])
setAllowedAccountProviders([.generic("company.com")])
await setupViewModel()
#expect(authenticationService.homeserver.value.loginMode == .unknown)
#expect(client.urlForOauthOauthConfigurationPromptLoginHintDeviceIdAdditionalScopesCallsCount == 0)
@@ -108,7 +108,7 @@ final class AuthenticationStartScreenViewModelTests {
context.send(viewAction: .login)
try await deferred.fulfill()
#expect(clientFactory.makeAuthenticationClientHomeserverAddressSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount == 1)
#expect(clientFactory.makeAuthenticationClientServerNameOrBaseURLSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount == 1)
#expect(client.urlForOauthOauthConfigurationPromptLoginHintDeviceIdAdditionalScopesCallsCount == 1)
#expect(client.urlForOauthOauthConfigurationPromptLoginHintDeviceIdAdditionalScopesReceivedArguments?.prompt == .consent)
#expect(client.urlForOauthOauthConfigurationPromptLoginHintDeviceIdAdditionalScopesReceivedArguments?.loginHint == nil)
@@ -118,7 +118,7 @@ final class AuthenticationStartScreenViewModelTests {
@Test
func singleProviderPasswordState() async throws {
// Given a view model that for an app that only allows the use of a single provider that does not support OAuth.
setAllowedAccountProviders(["company.com"])
setAllowedAccountProviders([.generic("company.com")])
await setupViewModel(supportsOAuth: false)
#expect(authenticationService.homeserver.value.loginMode == .unknown)
#expect(client.urlForOauthOauthConfigurationPromptLoginHintDeviceIdAdditionalScopesCallsCount == 0)
@@ -130,7 +130,7 @@ final class AuthenticationStartScreenViewModelTests {
try await deferred.fulfill()
// Then a call to configure service should be made.
#expect(clientFactory.makeAuthenticationClientHomeserverAddressSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount == 1)
#expect(clientFactory.makeAuthenticationClientServerNameOrBaseURLSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount == 1)
#expect(authenticationService.homeserver.value.loginMode == .password)
}
@@ -153,8 +153,8 @@ final class AuthenticationStartScreenViewModelTests {
context.send(viewAction: .continueWithClassic(classicAppAccount))
try await deferred.fulfill()
#expect(clientFactory.makeAuthenticationClientHomeserverAddressSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount == 1)
#expect(clientFactory.makeAuthenticationClientHomeserverAddressSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksReceivedArguments?.homeserverAddress == "company.com")
#expect(clientFactory.makeAuthenticationClientServerNameOrBaseURLSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount == 1)
#expect(clientFactory.makeAuthenticationClientServerNameOrBaseURLSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksReceivedArguments?.serverNameOrBaseURL == "company.com")
#expect(authenticationService.homeserver.value.loginMode == .oAuth(supportsCreatePrompt: false))
#expect(client.urlForOauthOauthConfigurationPromptLoginHintDeviceIdAdditionalScopesReceivedArguments?.loginHint == "mxid:\(classicAppAccount.userID)")
}
@@ -177,8 +177,8 @@ final class AuthenticationStartScreenViewModelTests {
context.send(viewAction: .continueWithClassic(classicAppAccount))
try await deferred.fulfill()
#expect(clientFactory.makeAuthenticationClientHomeserverAddressSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount == 2)
#expect(clientFactory.makeAuthenticationClientHomeserverAddressSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksReceivedArguments?.homeserverAddress == "https://matrix.company.com")
#expect(clientFactory.makeAuthenticationClientServerNameOrBaseURLSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount == 2)
#expect(clientFactory.makeAuthenticationClientServerNameOrBaseURLSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksReceivedArguments?.serverNameOrBaseURL == "https://matrix.company.com")
#expect(authenticationService.homeserver.value.loginMode == .oAuth(supportsCreatePrompt: false))
#expect(client.urlForOauthOauthConfigurationPromptLoginHintDeviceIdAdditionalScopesReceivedArguments?.loginHint == "mxid:\(classicAppAccount.userID)")
}
@@ -214,7 +214,7 @@ final class AuthenticationStartScreenViewModelTests {
// Given a view model for an app that only allows a single provider that matches the Classic account's server.
let classicAppAccount = makeClassicAppAccount(serverName: "company.com",
homeserverURL: "https://matrix.company.com")
setAllowedAccountProviders(["company.com"])
setAllowedAccountProviders([.generic("company.com")])
await setupViewModel(classicAppAccount: classicAppAccount)
// Then the Classic app account should be shown as a welcome-back option.
@@ -230,7 +230,7 @@ final class AuthenticationStartScreenViewModelTests {
// Given a view model for an app that only allows a single provider that does NOT match the Classic account's server.
let classicAppAccount = makeClassicAppAccount(serverName: "other-server.org",
homeserverURL: "https://matrix.other-server.org")
setAllowedAccountProviders(["company.com"])
setAllowedAccountProviders([.generic("company.com")])
await setupViewModel(classicAppAccount: classicAppAccount)
// Then the Classic app account should not be shown since the server is not in the allowed providers.
@@ -278,7 +278,7 @@ final class AuthenticationStartScreenViewModelTests {
supportsOAuth: Bool = true,
supportsPasswordLogin: Bool = true,
availableSecrets: ClassicAppAccount.AvailableSecrets = .complete) async {
// Manually create a configuration as the default homeserver address setting is immutable.
// Manually create a configuration as the default account provider setting is immutable.
client = ClientSDKMock(.init(oAuthLoginURL: supportsOAuth ? "https://account.company.com/authorize" : nil,
supportsOAuthCreatePrompt: false,
supportsPasswordLogin: supportsPasswordLogin))
@@ -330,7 +330,7 @@ final class AuthenticationStartScreenViewModelTests {
accessToken: "accessToken")
}
private func setAllowedAccountProviders(_ providers: [String]) {
private func setAllowedAccountProviders(_ providers: [AccountProvider]) {
appSettings.override(accountProviders: providers,
allowOtherAccountProviders: false,
hideBrandChrome: false,