mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-10-31 08:15:24 +00:00
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:
@@ -0,0 +1,44 @@
|
||||
//
|
||||
// Copyright 2026 Element Creations Ltd.
|
||||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
// Please see LICENSE files in the repository root for full details.
|
||||
//
|
||||
|
||||
@testable import ElementX
|
||||
import Foundation
|
||||
import Testing
|
||||
|
||||
struct AccountProviderTests {
|
||||
@Test
|
||||
func genericAddressIsSanitized() {
|
||||
// Lowercased, whitespace trimmed, https:// dropped and trailing slashes removed.
|
||||
#expect(AccountProvider.generic("Matrix.org").serverNameOrBaseURL == "matrix.org")
|
||||
#expect(AccountProvider.generic(" matrix.org ").serverNameOrBaseURL == "matrix.org")
|
||||
#expect(AccountProvider.generic("https://matrix.org").serverNameOrBaseURL == "matrix.org")
|
||||
#expect(AccountProvider.generic("https://matrix.org/").serverNameOrBaseURL == "matrix.org")
|
||||
#expect(AccountProvider.generic("HTTPS://Matrix.org///").serverNameOrBaseURL == "matrix.org")
|
||||
}
|
||||
|
||||
@Test
|
||||
func genericAddressKeepsHTTPScheme() {
|
||||
#expect(AccountProvider.generic("http://localhost:8008").serverNameOrBaseURL == "http://localhost:8008",
|
||||
"The http:// scheme should be preserved as an indicator")
|
||||
}
|
||||
|
||||
@Test
|
||||
func managedUsesServerNameWhenPresent() {
|
||||
let provider = AccountProvider.managed(serverName: "Matrix.org",
|
||||
baseURL: "https://matrix-client.matrix.org",
|
||||
canUseServerName: true)
|
||||
#expect(provider.serverNameOrBaseURL == "matrix.org")
|
||||
}
|
||||
|
||||
@Test
|
||||
func managedFallsBackToBaseURLWhenShouldNotUserServerName() {
|
||||
let provider = AccountProvider.managed(serverName: "Matrix.org",
|
||||
baseURL: "https://matrix-client.matrix.org",
|
||||
canUseServerName: false)
|
||||
#expect(provider.serverNameOrBaseURL == "matrix-client.matrix.org")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
//
|
||||
// Copyright 2026 Element Creations Ltd.
|
||||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
// Please see LICENSE files in the repository root for full details.
|
||||
//
|
||||
|
||||
@testable import ElementX
|
||||
import Testing
|
||||
|
||||
struct AppSettingsTests {
|
||||
@Test
|
||||
func defaultAccountProvider() {
|
||||
let appSettings = AppSettings.volatile()
|
||||
#expect(appSettings.defaultAccountProvider == appSettings.accountProviders[0])
|
||||
|
||||
appSettings.previousServers = ["example.com"]
|
||||
#expect(appSettings.defaultAccountProvider == .generic("example.com"))
|
||||
|
||||
appSettings.override(allowOtherAccountProviders: false)
|
||||
#expect(appSettings.defaultAccountProvider == appSettings.accountProviders[0])
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Helpers
|
||||
|
||||
private extension AppSettings {
|
||||
func override(allowOtherAccountProviders: Bool) {
|
||||
override(accountProviders: accountProviders,
|
||||
allowOtherAccountProviders: allowOtherAccountProviders,
|
||||
hideBrandChrome: hideBrandChrome,
|
||||
pushGatewayBaseURL: pushGatewayBaseURL,
|
||||
oAuthRedirectURL: oAuthRedirectURL,
|
||||
oAuthClientURIPath: oAuthClientURIPath,
|
||||
websiteURL: websiteURL,
|
||||
logoURL: logoURL,
|
||||
copyrightURL: copyrightURL,
|
||||
acceptableUseURL: acceptableUseURL,
|
||||
privacyURL: privacyURL,
|
||||
encryptionURL: encryptionURL,
|
||||
deviceVerificationURL: deviceVerificationURL,
|
||||
chatBackupDetailsURL: chatBackupDetailsURL,
|
||||
identityPinningViolationDetailsURL: identityPinningViolationDetailsURL,
|
||||
historySharingDetailsURL: historySharingDetailsURL,
|
||||
elementWebHosts: elementWebHosts,
|
||||
accountProvisioningHost: accountProvisioningHost,
|
||||
bugReportApplicationID: bugReportApplicationID,
|
||||
analyticsTermsURL: analyticsTermsURL,
|
||||
mapTilerConfiguration: mapTilerConfiguration.publisher.value)
|
||||
}
|
||||
}
|
||||
@@ -21,7 +21,7 @@ struct AuthenticationServiceTests {
|
||||
|
||||
@Test
|
||||
mutating func passwordLogin() async throws {
|
||||
try await setup(serverAddress: "example.com")
|
||||
try await setup(serverNameOrBaseURL: "example.com")
|
||||
|
||||
switch await service.configure(for: "example.com", flow: .login) {
|
||||
case .success:
|
||||
@@ -65,21 +65,20 @@ struct AuthenticationServiceTests {
|
||||
}
|
||||
|
||||
@Test
|
||||
@MainActor
|
||||
mutating func configureRegisterNoSupport() async throws {
|
||||
let homeserverAddress = "example.com"
|
||||
try await setup(serverAddress: homeserverAddress)
|
||||
let serverNameOrBaseURL = "example.com"
|
||||
try await setup(serverNameOrBaseURL: serverNameOrBaseURL)
|
||||
|
||||
try await #require(throws: AuthenticationServiceError.registrationNotSupported) {
|
||||
try await service.configure(for: homeserverAddress, flow: .register).get()
|
||||
try await service.configure(for: serverNameOrBaseURL, flow: .register).get()
|
||||
}
|
||||
|
||||
#expect(service.flow == .login)
|
||||
#expect(service.homeserver.value == .init(address: "matrix.org", loginMode: .unknown))
|
||||
#expect(service.homeserver.value == .init(accountProvider: .managed(serverName: "matrix.org", baseURL: "https://matrix-client.matrix.org"),
|
||||
loginMode: .unknown))
|
||||
}
|
||||
|
||||
@Test
|
||||
@MainActor
|
||||
mutating func classicAppAccountSecretsBundleIsUsed() async throws {
|
||||
// Given an authentication service with an Element Classic account for Alice.
|
||||
try await setup(classicAppAccounts: [.mockAlice])
|
||||
@@ -96,7 +95,6 @@ struct AuthenticationServiceTests {
|
||||
}
|
||||
|
||||
@Test
|
||||
@MainActor
|
||||
mutating func classicAppAccountSecretsBundleIsIgnoredWhenUnavailable() async throws {
|
||||
// Given an authentication service with an Element Classic account for Alice
|
||||
// which isn't configured with any available secrets.
|
||||
@@ -114,7 +112,6 @@ struct AuthenticationServiceTests {
|
||||
}
|
||||
|
||||
@Test
|
||||
@MainActor
|
||||
mutating func classicAppAccountSecretsBundleIsIgnoredForDifferentUser() async throws {
|
||||
// Given an authentication service with an Element Classic account for Dan.
|
||||
try await setup(classicAppAccounts: [.mockDan])
|
||||
@@ -132,13 +129,13 @@ struct AuthenticationServiceTests {
|
||||
|
||||
// MARK: - Helpers
|
||||
|
||||
private mutating func setup(serverAddress: String = "matrix.org",
|
||||
private mutating func setup(serverNameOrBaseURL: String = "matrix.org",
|
||||
classicAppAccounts: [ClassicAppAccount] = [],
|
||||
availableSecrets: ClassicAppAccount.AvailableSecrets = .complete) async throws {
|
||||
let configuration: ClientFactoryMock.Configuration = .init()
|
||||
let clientFactory = ClientFactoryMock(configuration)
|
||||
|
||||
client = configuration.homeserverClients[serverAddress]
|
||||
client = configuration.homeserverClients[serverNameOrBaseURL]
|
||||
encryption = EncryptionSDKMock()
|
||||
client.encryptionReturnValue = encryption
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -226,7 +226,7 @@ struct LoginScreenViewModelTests {
|
||||
|
||||
// MARK: - Helpers
|
||||
|
||||
private mutating func setupViewModel(homeserverAddress: String = "example.com", loginHint: String? = nil) async {
|
||||
private mutating func setupViewModel(serverNameOrBaseURL: String = "example.com", loginHint: String? = nil) async {
|
||||
let appSettings = AppSettings.volatile()
|
||||
|
||||
clientFactory = ClientFactoryMock(.init())
|
||||
@@ -238,7 +238,7 @@ struct LoginScreenViewModelTests {
|
||||
appHooks: AppHooks())
|
||||
|
||||
guard case .success = await service
|
||||
.configure(for: homeserverAddress, flow: .login) else {
|
||||
.configure(for: serverNameOrBaseURL, flow: .login) else {
|
||||
Issue.record("A valid server should be configured for the test.")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -17,14 +17,14 @@ nonisolated struct NotificationContentBuilderTests {
|
||||
var mediaProvider: MediaProviderMock
|
||||
var notificationContent: UNMutableNotificationContent
|
||||
|
||||
init() {
|
||||
init() async {
|
||||
notificationContent = .init()
|
||||
let stringBuilder = RoomMessageEventStringBuilder(attributedStringBuilder: AttributedStringBuilder(mentionBuilder: PlainMentionBuilder()),
|
||||
style: .plain)
|
||||
mediaProvider = MediaProviderMock(.init())
|
||||
notificationContentBuilder = NotificationContentBuilder(messageEventStringBuilder: stringBuilder,
|
||||
notificationSoundName: UNNotificationSoundName("message.caf"),
|
||||
userSession: NSEUserSessionMock(.init()))
|
||||
notificationContentBuilder = await NotificationContentBuilder(messageEventStringBuilder: stringBuilder,
|
||||
notificationSoundName: UNNotificationSoundName("message.caf"),
|
||||
userSession: NSEUserSessionMock(.init()))
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@@ -14,7 +14,7 @@ import Testing
|
||||
@MainActor
|
||||
struct ServerSelectionScreenViewModelTests {
|
||||
private static let passwordOnlyServer = "example.com" // password only, no OAuth
|
||||
private static let pickerProviders = ["matrix.org", "beta.matrix.org", passwordOnlyServer]
|
||||
private static let pickerProviders: [String] = ["matrix.org", "beta.matrix.org", passwordOnlyServer]
|
||||
|
||||
var appSettings: AppSettings!
|
||||
var client: ClientSDKMock!
|
||||
@@ -31,16 +31,16 @@ struct ServerSelectionScreenViewModelTests {
|
||||
// Given a view model for login.
|
||||
try setup(authenticationFlow: .login)
|
||||
#expect(service.homeserver.value.loginMode == .unknown)
|
||||
#expect(clientFactory.makeAuthenticationClientHomeserverAddressSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount == 0)
|
||||
#expect(clientFactory.makeAuthenticationClientServerNameOrBaseURLSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount == 0)
|
||||
|
||||
// When selecting matrix.org.
|
||||
context.homeserverAddress = "matrix.org"
|
||||
context.serverNameOrBaseURL = "matrix.org"
|
||||
let deferred = deferFulfillment(viewModel.actions) { $0.isContinueWithOAuth }
|
||||
context.send(viewAction: .confirm)
|
||||
try await deferred.fulfill()
|
||||
|
||||
// Then selection should succeed.
|
||||
#expect(clientFactory.makeAuthenticationClientHomeserverAddressSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount == 1)
|
||||
#expect(clientFactory.makeAuthenticationClientServerNameOrBaseURLSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount == 1)
|
||||
#expect(service.homeserver.value == .mockMatrixDotOrg)
|
||||
}
|
||||
|
||||
@@ -51,13 +51,13 @@ struct ServerSelectionScreenViewModelTests {
|
||||
#expect(service.homeserver.value.loginMode == .unknown)
|
||||
|
||||
// When entering a Matrix ID instead of an account provider.
|
||||
context.homeserverAddress = "@alice:matrix.org"
|
||||
context.serverNameOrBaseURL = "@alice:matrix.org"
|
||||
let deferred = deferFulfillment(viewModel.actions) { $0.isContinueWithOAuth }
|
||||
context.send(viewAction: .confirm)
|
||||
try await deferred.fulfill()
|
||||
|
||||
// Then the homeserver from the ID should be used.
|
||||
#expect(clientFactory.makeAuthenticationClientHomeserverAddressSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksReceivedArguments?.homeserverAddress == "matrix.org")
|
||||
#expect(clientFactory.makeAuthenticationClientServerNameOrBaseURLSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksReceivedArguments?.serverNameOrBaseURL == "matrix.org")
|
||||
#expect(service.homeserver.value == .mockMatrixDotOrg)
|
||||
}
|
||||
|
||||
@@ -66,17 +66,17 @@ struct ServerSelectionScreenViewModelTests {
|
||||
// Given a view model for login.
|
||||
try setup(authenticationFlow: .login)
|
||||
#expect(service.homeserver.value.loginMode == .unknown)
|
||||
#expect(clientFactory.makeAuthenticationClientHomeserverAddressSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount == 0)
|
||||
#expect(clientFactory.makeAuthenticationClientServerNameOrBaseURLSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount == 0)
|
||||
#expect(context.alertInfo == nil)
|
||||
|
||||
// When selecting a server that doesn't support login.
|
||||
context.homeserverAddress = "server.net"
|
||||
context.serverNameOrBaseURL = "server.net"
|
||||
let deferred = deferFulfillment(context.observe(\.alertInfo)) { $0 != nil }
|
||||
context.send(viewAction: .confirm)
|
||||
try await deferred.fulfill()
|
||||
|
||||
// Then selection should fail with an alert about not supporting registration.
|
||||
#expect(clientFactory.makeAuthenticationClientHomeserverAddressSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount == 1)
|
||||
#expect(clientFactory.makeAuthenticationClientServerNameOrBaseURLSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount == 1)
|
||||
#expect(context.alertInfo?.id == .loginAlert)
|
||||
}
|
||||
|
||||
@@ -85,16 +85,16 @@ struct ServerSelectionScreenViewModelTests {
|
||||
// Given a view model for registration.
|
||||
try setup(authenticationFlow: .register)
|
||||
#expect(service.homeserver.value.loginMode == .unknown)
|
||||
#expect(clientFactory.makeAuthenticationClientHomeserverAddressSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount == 0)
|
||||
#expect(clientFactory.makeAuthenticationClientServerNameOrBaseURLSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount == 0)
|
||||
|
||||
// When selecting matrix.org.
|
||||
context.homeserverAddress = "matrix.org"
|
||||
context.serverNameOrBaseURL = "matrix.org"
|
||||
let deferred = deferFulfillment(viewModel.actions) { $0.isContinueWithOAuth }
|
||||
context.send(viewAction: .confirm)
|
||||
try await deferred.fulfill()
|
||||
|
||||
// Then selection should succeed.
|
||||
#expect(clientFactory.makeAuthenticationClientHomeserverAddressSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount == 1)
|
||||
#expect(clientFactory.makeAuthenticationClientServerNameOrBaseURLSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount == 1)
|
||||
#expect(service.homeserver.value == .mockMatrixDotOrg)
|
||||
}
|
||||
|
||||
@@ -103,17 +103,17 @@ struct ServerSelectionScreenViewModelTests {
|
||||
// Given a view model for registration.
|
||||
try setup(authenticationFlow: .register)
|
||||
#expect(service.homeserver.value.loginMode == .unknown)
|
||||
#expect(clientFactory.makeAuthenticationClientHomeserverAddressSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount == 0)
|
||||
#expect(clientFactory.makeAuthenticationClientServerNameOrBaseURLSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount == 0)
|
||||
#expect(context.alertInfo == nil)
|
||||
|
||||
// When selecting a server that doesn't support registration.
|
||||
context.homeserverAddress = "example.com"
|
||||
context.serverNameOrBaseURL = "example.com"
|
||||
let deferred = deferFulfillment(context.observe(\.alertInfo)) { $0 != nil }
|
||||
context.send(viewAction: .confirm)
|
||||
try await deferred.fulfill()
|
||||
|
||||
// Then selection should fail with an alert about not supporting registration.
|
||||
#expect(clientFactory.makeAuthenticationClientHomeserverAddressSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount == 1)
|
||||
#expect(clientFactory.makeAuthenticationClientServerNameOrBaseURLSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount == 1)
|
||||
#expect(context.alertInfo?.id == .registrationAlert)
|
||||
}
|
||||
|
||||
@@ -122,17 +122,17 @@ struct ServerSelectionScreenViewModelTests {
|
||||
// Given a view model for login.
|
||||
try setup(authenticationFlow: .login)
|
||||
#expect(service.homeserver.value.loginMode == .unknown)
|
||||
#expect(clientFactory.makeAuthenticationClientHomeserverAddressSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount == 0)
|
||||
#expect(clientFactory.makeAuthenticationClientServerNameOrBaseURLSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount == 0)
|
||||
#expect(context.alertInfo == nil)
|
||||
|
||||
// When selecting a server that requires Element Pro
|
||||
context.homeserverAddress = "secure.gov"
|
||||
context.serverNameOrBaseURL = "secure.gov"
|
||||
let deferred = deferFulfillment(context.observe(\.alertInfo)) { $0 != nil }
|
||||
context.send(viewAction: .confirm)
|
||||
try await deferred.fulfill()
|
||||
|
||||
// Then selection should fail with an alert telling the user to download Element Pro.
|
||||
#expect(clientFactory.makeAuthenticationClientHomeserverAddressSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount == 1)
|
||||
#expect(clientFactory.makeAuthenticationClientServerNameOrBaseURLSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount == 1)
|
||||
#expect(context.alertInfo?.id == .elementProAlert)
|
||||
}
|
||||
|
||||
@@ -147,7 +147,7 @@ struct ServerSelectionScreenViewModelTests {
|
||||
|
||||
// When attempting to discover an invalid server
|
||||
var deferred = deferFulfillment(context.observe(\.viewState.isShowingFooterError)) { $0 }
|
||||
context.homeserverAddress = "idontexist"
|
||||
context.serverNameOrBaseURL = "idontexist"
|
||||
context.send(viewAction: .confirm)
|
||||
try await deferred.fulfill()
|
||||
|
||||
@@ -159,7 +159,7 @@ struct ServerSelectionScreenViewModelTests {
|
||||
|
||||
// And when clearing the error.
|
||||
deferred = deferFulfillment(context.observe(\.viewState.isShowingFooterError)) { !$0 }
|
||||
context.homeserverAddress = ""
|
||||
context.serverNameOrBaseURL = ""
|
||||
context.send(viewAction: .clearFooterError)
|
||||
try await deferred.fulfill()
|
||||
|
||||
@@ -175,9 +175,9 @@ struct ServerSelectionScreenViewModelTests {
|
||||
mutating func userInputPasswordLoginWithoutConfiguration() async throws {
|
||||
// Given a view model for login using a service that hasn't been configured against a server that doesn't support OAuth.
|
||||
try setup(authenticationFlow: .login)
|
||||
context.homeserverAddress = Self.passwordOnlyServer
|
||||
context.serverNameOrBaseURL = Self.passwordOnlyServer
|
||||
#expect(service.homeserver.value.loginMode == .unknown)
|
||||
#expect(clientFactory.makeAuthenticationClientHomeserverAddressSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount == 0)
|
||||
#expect(clientFactory.makeAuthenticationClientServerNameOrBaseURLSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount == 0)
|
||||
|
||||
// When confirming from the server selection screen.
|
||||
let deferred = deferFulfillment(viewModel.actions) { $0.isContinueWithPassword }
|
||||
@@ -185,7 +185,7 @@ struct ServerSelectionScreenViewModelTests {
|
||||
try await deferred.fulfill()
|
||||
|
||||
// Then the service should be configured but no OAuth URL fetched.
|
||||
#expect(clientFactory.makeAuthenticationClientHomeserverAddressSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount == 1)
|
||||
#expect(clientFactory.makeAuthenticationClientServerNameOrBaseURLSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount == 1)
|
||||
#expect(service.homeserver.value.loginMode == .password)
|
||||
}
|
||||
|
||||
@@ -193,13 +193,13 @@ struct ServerSelectionScreenViewModelTests {
|
||||
mutating func userInputPasswordLoginAfterConfiguration() async throws {
|
||||
// Given a view model for login using a service that has already been configured against a server that doesn't support OAuth.
|
||||
try setup(authenticationFlow: .login)
|
||||
context.homeserverAddress = Self.passwordOnlyServer
|
||||
guard case .success = await service.configure(for: context.homeserverAddress, flow: .login) else {
|
||||
context.serverNameOrBaseURL = Self.passwordOnlyServer
|
||||
guard case .success = await service.configure(for: context.serverNameOrBaseURL, flow: .login) else {
|
||||
Issue.record("The configuration should succeed.")
|
||||
return
|
||||
}
|
||||
#expect(service.homeserver.value.loginMode == .password)
|
||||
#expect(clientFactory.makeAuthenticationClientHomeserverAddressSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount == 1)
|
||||
#expect(clientFactory.makeAuthenticationClientServerNameOrBaseURLSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount == 1)
|
||||
|
||||
// When confirming from the server selection screen.
|
||||
let deferred = deferFulfillment(viewModel.actions) { $0.isContinueWithPassword }
|
||||
@@ -207,7 +207,7 @@ struct ServerSelectionScreenViewModelTests {
|
||||
try await deferred.fulfill()
|
||||
|
||||
// Then the service should be re-configured but no OAuth URL fetched.
|
||||
#expect(clientFactory.makeAuthenticationClientHomeserverAddressSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount == 2)
|
||||
#expect(clientFactory.makeAuthenticationClientServerNameOrBaseURLSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount == 2)
|
||||
}
|
||||
|
||||
// MARK: - Picker mode
|
||||
@@ -218,7 +218,7 @@ struct ServerSelectionScreenViewModelTests {
|
||||
try setup(authenticationFlow: .login, mode: .picker(Self.pickerProviders))
|
||||
#expect(service.homeserver.value.loginMode == .unknown)
|
||||
#expect(context.viewState.mode == .picker(Self.pickerProviders))
|
||||
#expect(clientFactory.makeAuthenticationClientHomeserverAddressSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount == 0)
|
||||
#expect(clientFactory.makeAuthenticationClientServerNameOrBaseURLSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount == 0)
|
||||
#expect(client.urlForOauthOauthConfigurationPromptLoginHintDeviceIdAdditionalScopesCallsCount == 0)
|
||||
|
||||
// When confirming from the picker.
|
||||
@@ -227,7 +227,7 @@ struct ServerSelectionScreenViewModelTests {
|
||||
try await deferred.fulfill()
|
||||
|
||||
// Then the service should be configured and the OAuth URL fetched.
|
||||
#expect(clientFactory.makeAuthenticationClientHomeserverAddressSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount == 1)
|
||||
#expect(clientFactory.makeAuthenticationClientServerNameOrBaseURLSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount == 1)
|
||||
#expect(client.urlForOauthOauthConfigurationPromptLoginHintDeviceIdAdditionalScopesCallsCount == 1)
|
||||
#expect(client.urlForOauthOauthConfigurationPromptLoginHintDeviceIdAdditionalScopesReceivedArguments?.prompt == .consent)
|
||||
#expect(service.homeserver.value.loginMode == .oAuth(supportsCreatePrompt: true))
|
||||
@@ -237,9 +237,9 @@ struct ServerSelectionScreenViewModelTests {
|
||||
mutating func pickerForPasswordLoginWithoutConfiguration() async throws {
|
||||
// Given a view model for login using a service that hasn't been configured against a server that doesn't support OAuth.
|
||||
try setup(authenticationFlow: .login, mode: .picker(Self.pickerProviders))
|
||||
context.homeserverAddress = Self.passwordOnlyServer
|
||||
context.serverNameOrBaseURL = Self.passwordOnlyServer
|
||||
#expect(service.homeserver.value.loginMode == .unknown)
|
||||
#expect(clientFactory.makeAuthenticationClientHomeserverAddressSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount == 0)
|
||||
#expect(clientFactory.makeAuthenticationClientServerNameOrBaseURLSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount == 0)
|
||||
|
||||
// When confirming from the picker.
|
||||
let deferred = deferFulfillment(viewModel.actions) { $0.isContinueWithPassword }
|
||||
@@ -247,7 +247,7 @@ struct ServerSelectionScreenViewModelTests {
|
||||
try await deferred.fulfill()
|
||||
|
||||
// Then the service should be configured but no OAuth URL fetched.
|
||||
#expect(clientFactory.makeAuthenticationClientHomeserverAddressSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount == 1)
|
||||
#expect(clientFactory.makeAuthenticationClientServerNameOrBaseURLSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount == 1)
|
||||
#expect(service.homeserver.value.loginMode == .password)
|
||||
}
|
||||
|
||||
@@ -258,7 +258,7 @@ struct ServerSelectionScreenViewModelTests {
|
||||
// Given a view model with a previous server in history.
|
||||
try setup(authenticationFlow: .login)
|
||||
appSettings.previousServers = ["myserver.com"]
|
||||
context.homeserverAddress = ""
|
||||
context.serverNameOrBaseURL = ""
|
||||
let textField = UITextField()
|
||||
context.send(viewAction: .updateTextField(textField))
|
||||
|
||||
@@ -268,14 +268,14 @@ struct ServerSelectionScreenViewModelTests {
|
||||
let typed = typedSoFar + nextCharacter
|
||||
let expectedAddress = "myserver.com"
|
||||
textField.text = typedSoFar
|
||||
let deferred = deferFulfillment(context.observe(\.homeserverAddress)) { $0 == expectedAddress }
|
||||
let deferred = deferFulfillment(context.observe(\.serverNameOrBaseURL)) { $0 == expectedAddress }
|
||||
_ = textField.delegate?.textField?(textField, shouldChangeCharactersIn: NSRange(location: typedSoFar.count, length: 0), replacementString: nextCharacter)
|
||||
try await deferred.fulfill()
|
||||
|
||||
// Then the address should be completed and the appended portion selected.
|
||||
let selectionStart = expectedAddress.index(expectedAddress.startIndex, offsetBy: typed.count)
|
||||
#expect(context.homeserverAddress == expectedAddress)
|
||||
#expect(context.homeserverSelection == TextSelection(range: selectionStart..<expectedAddress.endIndex))
|
||||
#expect(context.serverNameOrBaseURL == expectedAddress)
|
||||
#expect(context.serverNameOrBaseURLSelection == TextSelection(range: selectionStart..<expectedAddress.endIndex))
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -283,7 +283,7 @@ struct ServerSelectionScreenViewModelTests {
|
||||
// Given a view model with no previous server history, falling back to the default account providers.
|
||||
try setup(authenticationFlow: .login)
|
||||
#expect(appSettings.previousServers.isEmpty)
|
||||
context.homeserverAddress = ""
|
||||
context.serverNameOrBaseURL = ""
|
||||
let textField = UITextField()
|
||||
context.send(viewAction: .updateTextField(textField))
|
||||
|
||||
@@ -293,14 +293,14 @@ struct ServerSelectionScreenViewModelTests {
|
||||
let typed = typedSoFar + nextCharacter
|
||||
let expectedAddress = "matrix.org"
|
||||
textField.text = typedSoFar
|
||||
let deferred = deferFulfillment(context.observe(\.homeserverAddress)) { $0 == expectedAddress }
|
||||
let deferred = deferFulfillment(context.observe(\.serverNameOrBaseURL)) { $0 == expectedAddress }
|
||||
_ = textField.delegate?.textField?(textField, shouldChangeCharactersIn: NSRange(location: typedSoFar.count, length: 0), replacementString: nextCharacter)
|
||||
try await deferred.fulfill()
|
||||
|
||||
// Then the address should be completed and the appended portion selected.
|
||||
let selectionStart = expectedAddress.index(expectedAddress.startIndex, offsetBy: typed.count)
|
||||
#expect(context.homeserverAddress == expectedAddress)
|
||||
#expect(context.homeserverSelection == TextSelection(range: selectionStart..<expectedAddress.endIndex))
|
||||
#expect(context.serverNameOrBaseURL == expectedAddress)
|
||||
#expect(context.serverNameOrBaseURLSelection == TextSelection(range: selectionStart..<expectedAddress.endIndex))
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -308,7 +308,7 @@ struct ServerSelectionScreenViewModelTests {
|
||||
// Given a view model with a known set of servers.
|
||||
try setup(authenticationFlow: .login)
|
||||
appSettings.previousServers = ["myserver.com"]
|
||||
context.homeserverAddress = ""
|
||||
context.serverNameOrBaseURL = ""
|
||||
let textField = UITextField()
|
||||
context.send(viewAction: .updateTextField(textField))
|
||||
|
||||
@@ -318,7 +318,7 @@ struct ServerSelectionScreenViewModelTests {
|
||||
try await Task.sleep(for: .milliseconds(50))
|
||||
|
||||
// Then the address should remain unchanged.
|
||||
#expect(context.homeserverAddress == "")
|
||||
#expect(context.serverNameOrBaseURL == "")
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -326,30 +326,30 @@ struct ServerSelectionScreenViewModelTests {
|
||||
// Given a view model with an ordered server history.
|
||||
try setup(authenticationFlow: .login)
|
||||
appSettings.previousServers = ["abc.foo", "matrix.org", "mantis.asdf"]
|
||||
context.homeserverAddress = ""
|
||||
context.serverNameOrBaseURL = ""
|
||||
let textField = UITextField()
|
||||
context.send(viewAction: .updateTextField(textField))
|
||||
|
||||
// Step 1: type "m" — first "m" match in history is "matrix.org".
|
||||
textField.text = ""
|
||||
var deferred = deferFulfillment(context.observe(\.homeserverAddress)) { $0 == "matrix.org" }
|
||||
var deferred = deferFulfillment(context.observe(\.serverNameOrBaseURL)) { $0 == "matrix.org" }
|
||||
_ = textField.delegate?.textField?(textField, shouldChangeCharactersIn: NSRange(location: 0, length: 0), replacementString: "m")
|
||||
try await deferred.fulfill()
|
||||
#expect(context.homeserverAddress == "matrix.org")
|
||||
#expect(context.serverNameOrBaseURL == "matrix.org")
|
||||
|
||||
// Step 2: type "a" replacing the highlighted suffix — still matches "matrix.org".
|
||||
// textField.text = "matrix.org", "atrix.org" highlighted (loc: 1, len: 9).
|
||||
deferred = deferFulfillment(context.observe(\.homeserverAddress)) { $0 == "matrix.org" }
|
||||
deferred = deferFulfillment(context.observe(\.serverNameOrBaseURL)) { $0 == "matrix.org" }
|
||||
_ = textField.delegate?.textField?(textField, shouldChangeCharactersIn: NSRange(location: 1, length: 9), replacementString: "a")
|
||||
try await deferred.fulfill()
|
||||
#expect(context.homeserverAddress == "matrix.org")
|
||||
#expect(context.serverNameOrBaseURL == "matrix.org")
|
||||
|
||||
// Step 3: type "n" replacing the highlighted suffix — "man" now matches "mantis.asdf".
|
||||
// textField.text = "matrix.org", "trix.org" highlighted (loc: 2, len: 8).
|
||||
deferred = deferFulfillment(context.observe(\.homeserverAddress)) { $0 == "mantis.asdf" }
|
||||
deferred = deferFulfillment(context.observe(\.serverNameOrBaseURL)) { $0 == "mantis.asdf" }
|
||||
_ = textField.delegate?.textField?(textField, shouldChangeCharactersIn: NSRange(location: 2, length: 8), replacementString: "n")
|
||||
try await deferred.fulfill()
|
||||
#expect(context.homeserverAddress == "mantis.asdf")
|
||||
#expect(context.serverNameOrBaseURL == "mantis.asdf")
|
||||
}
|
||||
|
||||
// MARK: - Helpers
|
||||
|
||||
Reference in New Issue
Block a user