Verify Element X with an existing Element Classic account. (#5374)

* Read and import the secrets from ClassicAppAccounts.

* Record snapshots.

* Add some documentation, tidy up tests and fix the dismissal of the backup instructions.

* Workaround flakey tests (the fulfilments weren't always firing).

* Allow a custom Classic App deep link URL to be configured.
This commit is contained in:
Doug
2026-04-13 15:30:09 +01:00
committed by GitHub
parent f146ba835d
commit 252e2f75df
40 changed files with 947 additions and 103 deletions

View File

@@ -14,6 +14,7 @@ extension AuthenticationClientFactoryMock {
struct Configuration {
var homeserverClients = [
"matrix.org": ClientSDKMock(configuration: .init()),
"https://matrix-client.matrix.org": ClientSDKMock(configuration: .init()),
"example.com": ClientSDKMock(configuration: .init(serverAddress: "example.com",
homeserverURL: "https://matrix.example.com",
slidingSyncVersion: .native,
@@ -51,5 +52,12 @@ extension AuthenticationClientFactoryMock {
}
return client
}
makeInMemoryClientHomeserverAddressClientSessionDelegateAppSettingsAppHooksClosure = { address, _, _, _ in
guard let client = configuration.homeserverClients[address] else {
throw ClientBuildError.ServerUnreachable(message: "Not a known homeserver.")
}
return client
}
}
}

View File

@@ -9,10 +9,18 @@ import Foundation
import MatrixRustSDK
extension ClassicAppManagerMock {
struct Configuration { }
struct Configuration {
var accounts: [ClassicAppAccount]
var availableSecrets: ClassicAppAccount.AvailableSecrets = .complete
var secretsBundle: SecretsBundleWithUserId?
}
convenience init(_ configuration: Configuration) {
self.init()
loadAccountsClosure = { configuration.accounts }
availableSecretsForReturnValue = configuration.availableSecrets
secretsBundleForReturnValue = configuration.secretsBundle
}
}
@@ -22,7 +30,7 @@ extension ClassicAppAccount {
displayName: "Alice",
avatarURL: nil,
serverName: "matrix.org",
homeserverURL: "https://matrix-client.matrix.org/",
homeserverURL: "https://matrix-client.matrix.org",
cryptoStoreURL: .cachesDirectory,
cryptoStorePassphrase: "1234567890",
accessToken: "accessToken")
@@ -33,7 +41,7 @@ extension ClassicAppAccount {
displayName: "Dan",
avatarURL: .mockMXCUserAvatar,
serverName: "matrix.org",
homeserverURL: "https://matrix-client.matrix.org/",
homeserverURL: "https://matrix-client.matrix.org",
cryptoStoreURL: .cachesDirectory,
cryptoStorePassphrase: "1234567890",
accessToken: "accessToken")

View File

@@ -1946,6 +1946,80 @@ class AuthenticationClientFactoryMock: AuthenticationClientFactoryProtocol, @unc
return makeClientHomeserverAddressSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksReturnValue
}
}
//MARK: - makeInMemoryClient
var makeInMemoryClientHomeserverAddressClientSessionDelegateAppSettingsAppHooksThrowableError: Error?
var makeInMemoryClientHomeserverAddressClientSessionDelegateAppSettingsAppHooksUnderlyingCallsCount = 0
var makeInMemoryClientHomeserverAddressClientSessionDelegateAppSettingsAppHooksCallsCount: Int {
get {
if Thread.isMainThread {
return makeInMemoryClientHomeserverAddressClientSessionDelegateAppSettingsAppHooksUnderlyingCallsCount
} else {
var returnValue: Int? = nil
DispatchQueue.main.sync {
returnValue = makeInMemoryClientHomeserverAddressClientSessionDelegateAppSettingsAppHooksUnderlyingCallsCount
}
return returnValue!
}
}
set {
if Thread.isMainThread {
makeInMemoryClientHomeserverAddressClientSessionDelegateAppSettingsAppHooksUnderlyingCallsCount = newValue
} else {
DispatchQueue.main.sync {
makeInMemoryClientHomeserverAddressClientSessionDelegateAppSettingsAppHooksUnderlyingCallsCount = newValue
}
}
}
}
var makeInMemoryClientHomeserverAddressClientSessionDelegateAppSettingsAppHooksCalled: Bool {
return makeInMemoryClientHomeserverAddressClientSessionDelegateAppSettingsAppHooksCallsCount > 0
}
var makeInMemoryClientHomeserverAddressClientSessionDelegateAppSettingsAppHooksReceivedArguments: (homeserverAddress: String, clientSessionDelegate: ClientSessionDelegate, appSettings: AppSettings, appHooks: AppHooks)?
var makeInMemoryClientHomeserverAddressClientSessionDelegateAppSettingsAppHooksReceivedInvocations: [(homeserverAddress: String, clientSessionDelegate: ClientSessionDelegate, appSettings: AppSettings, appHooks: AppHooks)] = []
var makeInMemoryClientHomeserverAddressClientSessionDelegateAppSettingsAppHooksUnderlyingReturnValue: ClientProtocol!
var makeInMemoryClientHomeserverAddressClientSessionDelegateAppSettingsAppHooksReturnValue: ClientProtocol! {
get {
if Thread.isMainThread {
return makeInMemoryClientHomeserverAddressClientSessionDelegateAppSettingsAppHooksUnderlyingReturnValue
} else {
var returnValue: ClientProtocol? = nil
DispatchQueue.main.sync {
returnValue = makeInMemoryClientHomeserverAddressClientSessionDelegateAppSettingsAppHooksUnderlyingReturnValue
}
return returnValue!
}
}
set {
if Thread.isMainThread {
makeInMemoryClientHomeserverAddressClientSessionDelegateAppSettingsAppHooksUnderlyingReturnValue = newValue
} else {
DispatchQueue.main.sync {
makeInMemoryClientHomeserverAddressClientSessionDelegateAppSettingsAppHooksUnderlyingReturnValue = newValue
}
}
}
}
var makeInMemoryClientHomeserverAddressClientSessionDelegateAppSettingsAppHooksClosure: ((String, ClientSessionDelegate, AppSettings, AppHooks) async throws -> ClientProtocol)?
func makeInMemoryClient(homeserverAddress: String, clientSessionDelegate: ClientSessionDelegate, appSettings: AppSettings, appHooks: AppHooks) async throws -> ClientProtocol {
if let error = makeInMemoryClientHomeserverAddressClientSessionDelegateAppSettingsAppHooksThrowableError {
throw error
}
makeInMemoryClientHomeserverAddressClientSessionDelegateAppSettingsAppHooksCallsCount += 1
makeInMemoryClientHomeserverAddressClientSessionDelegateAppSettingsAppHooksReceivedArguments = (homeserverAddress: homeserverAddress, clientSessionDelegate: clientSessionDelegate, appSettings: appSettings, appHooks: appHooks)
DispatchQueue.main.async {
self.makeInMemoryClientHomeserverAddressClientSessionDelegateAppSettingsAppHooksReceivedInvocations.append((homeserverAddress: homeserverAddress, clientSessionDelegate: clientSessionDelegate, appSettings: appSettings, appHooks: appHooks))
}
if let makeInMemoryClientHomeserverAddressClientSessionDelegateAppSettingsAppHooksClosure = makeInMemoryClientHomeserverAddressClientSessionDelegateAppSettingsAppHooksClosure {
return try await makeInMemoryClientHomeserverAddressClientSessionDelegateAppSettingsAppHooksClosure(homeserverAddress, clientSessionDelegate, appSettings, appHooks)
} else {
return makeInMemoryClientHomeserverAddressClientSessionDelegateAppSettingsAppHooksReturnValue
}
}
}
class BannedRoomProxyMock: BannedRoomProxyProtocol, @unchecked Sendable {
var info: BaseRoomInfoProxyProtocol {
@@ -2309,6 +2383,154 @@ class ClassicAppManagerMock: ClassicAppManagerProtocol, @unchecked Sendable {
return loadAccountsReturnValue
}
}
//MARK: - availableSecrets
var availableSecretsForThrowableError: Error?
var availableSecretsForUnderlyingCallsCount = 0
var availableSecretsForCallsCount: Int {
get {
if Thread.isMainThread {
return availableSecretsForUnderlyingCallsCount
} else {
var returnValue: Int? = nil
DispatchQueue.main.sync {
returnValue = availableSecretsForUnderlyingCallsCount
}
return returnValue!
}
}
set {
if Thread.isMainThread {
availableSecretsForUnderlyingCallsCount = newValue
} else {
DispatchQueue.main.sync {
availableSecretsForUnderlyingCallsCount = newValue
}
}
}
}
var availableSecretsForCalled: Bool {
return availableSecretsForCallsCount > 0
}
var availableSecretsForReceivedAccount: ClassicAppAccount?
var availableSecretsForReceivedInvocations: [ClassicAppAccount] = []
var availableSecretsForUnderlyingReturnValue: ClassicAppAccount.AvailableSecrets!
var availableSecretsForReturnValue: ClassicAppAccount.AvailableSecrets! {
get {
if Thread.isMainThread {
return availableSecretsForUnderlyingReturnValue
} else {
var returnValue: ClassicAppAccount.AvailableSecrets? = nil
DispatchQueue.main.sync {
returnValue = availableSecretsForUnderlyingReturnValue
}
return returnValue!
}
}
set {
if Thread.isMainThread {
availableSecretsForUnderlyingReturnValue = newValue
} else {
DispatchQueue.main.sync {
availableSecretsForUnderlyingReturnValue = newValue
}
}
}
}
var availableSecretsForClosure: ((ClassicAppAccount) async throws -> ClassicAppAccount.AvailableSecrets)?
func availableSecrets(for account: ClassicAppAccount) async throws -> ClassicAppAccount.AvailableSecrets {
if let error = availableSecretsForThrowableError {
throw error
}
availableSecretsForCallsCount += 1
availableSecretsForReceivedAccount = account
DispatchQueue.main.async {
self.availableSecretsForReceivedInvocations.append(account)
}
if let availableSecretsForClosure = availableSecretsForClosure {
return try await availableSecretsForClosure(account)
} else {
return availableSecretsForReturnValue
}
}
//MARK: - secretsBundle
var secretsBundleForThrowableError: Error?
var secretsBundleForUnderlyingCallsCount = 0
var secretsBundleForCallsCount: Int {
get {
if Thread.isMainThread {
return secretsBundleForUnderlyingCallsCount
} else {
var returnValue: Int? = nil
DispatchQueue.main.sync {
returnValue = secretsBundleForUnderlyingCallsCount
}
return returnValue!
}
}
set {
if Thread.isMainThread {
secretsBundleForUnderlyingCallsCount = newValue
} else {
DispatchQueue.main.sync {
secretsBundleForUnderlyingCallsCount = newValue
}
}
}
}
var secretsBundleForCalled: Bool {
return secretsBundleForCallsCount > 0
}
var secretsBundleForReceivedAccount: ClassicAppAccount?
var secretsBundleForReceivedInvocations: [ClassicAppAccount] = []
var secretsBundleForUnderlyingReturnValue: SecretsBundleWithUserId!
var secretsBundleForReturnValue: SecretsBundleWithUserId! {
get {
if Thread.isMainThread {
return secretsBundleForUnderlyingReturnValue
} else {
var returnValue: SecretsBundleWithUserId? = nil
DispatchQueue.main.sync {
returnValue = secretsBundleForUnderlyingReturnValue
}
return returnValue!
}
}
set {
if Thread.isMainThread {
secretsBundleForUnderlyingReturnValue = newValue
} else {
DispatchQueue.main.sync {
secretsBundleForUnderlyingReturnValue = newValue
}
}
}
}
var secretsBundleForClosure: ((ClassicAppAccount) async throws -> SecretsBundleWithUserId)?
func secretsBundle(for account: ClassicAppAccount) async throws -> SecretsBundleWithUserId {
if let error = secretsBundleForThrowableError {
throw error
}
secretsBundleForCallsCount += 1
secretsBundleForReceivedAccount = account
DispatchQueue.main.async {
self.secretsBundleForReceivedInvocations.append(account)
}
if let secretsBundleForClosure = secretsBundleForClosure {
return try await secretsBundleForClosure(account)
} else {
return secretsBundleForReturnValue
}
}
}
class ClientProxyMock: ClientProxyProtocol, @unchecked Sendable {
var actionsPublisher: AnyPublisher<ClientProxyAction, Never> {

View File

@@ -46,11 +46,16 @@ extension ClientSDKMock {
serverReturnValue = "https://\(configuration.serverAddress)"
homeserverReturnValue = configuration.homeserverURL
urlForOidcOidcConfigurationPromptLoginHintDeviceIdAdditionalScopesReturnValue = OAuthAuthorizationDataSDKMock(configuration: configuration)
loginUsernamePasswordInitialDeviceNameDeviceIdClosure = { username, password, _, _ in
loginUsernamePasswordInitialDeviceNameDeviceIdClosure = { [weak self] username, password, _, _ in
guard username == configuration.validCredentials.username,
password == configuration.validCredentials.password else {
throw MockError.generic // use the matrix error
}
if username.hasPrefix("@"), username.contains(":") {
self?.userIdReturnValue = username
} else {
self?.userIdReturnValue = "@\(username):\(configuration.serverAddress)"
}
}
userIdReturnValue = configuration.userID