mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-10-31 16:24:11 +00:00
df3bdb992a
The legacy restoration token formats store the passphrase as a base64 string whereas the current one exposes Data, so keep both representations of the same value in one place instead of repeating the conversions in every test.
193 lines
8.2 KiB
Swift
193 lines
8.2 KiB
Swift
//
|
|
// 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 MatrixRustSDK
|
|
import MatrixRustSDKMocks
|
|
import Testing
|
|
|
|
@MainActor
|
|
struct UserSessionStoreTests {
|
|
let keychainController = KeychainControllerMock(.init())
|
|
let clientFactory = ClientFactoryMock(.init())
|
|
let store: UserSessionStore
|
|
|
|
init() {
|
|
store = UserSessionStore(keychainController: keychainController,
|
|
clientFactory: clientFactory,
|
|
appSettings: .volatile(),
|
|
analyticsService: AnalyticsServiceMock(.init()),
|
|
appHooks: AppHooks(),
|
|
networkMonitor: NetworkMonitorMock(.init()))
|
|
}
|
|
|
|
// MARK: - Accessors
|
|
|
|
@Test
|
|
func hasSessions() {
|
|
#expect(!store.hasSessions)
|
|
|
|
keychainController.restorationTokensReturnValue = [makeCredentials(sessionDirectories: .init())]
|
|
#expect(store.hasSessions)
|
|
}
|
|
|
|
@Test
|
|
func userIDs() {
|
|
keychainController.restorationTokensReturnValue = [makeCredentials(sessionDirectories: .init(), userID: "@alice:matrix.org"),
|
|
makeCredentials(sessionDirectories: .init(), userID: "@bob:matrix.org")]
|
|
#expect(store.userIDs == ["@alice:matrix.org", "@bob:matrix.org"])
|
|
}
|
|
|
|
// MARK: - Restoration
|
|
|
|
@Test
|
|
func restoreWithoutCredentials() async {
|
|
guard case .failure(.missingCredentials) = await store.restoreUserSession() else {
|
|
Issue.record("Restoration should fail when there are no credentials.")
|
|
return
|
|
}
|
|
}
|
|
|
|
@Test
|
|
func restoreWithMissingUserData() async {
|
|
// Given credentials whose session data is no longer on disk.
|
|
let credentials = makeCredentials(sessionDirectories: .init())
|
|
keychainController.restorationTokensReturnValue = [credentials]
|
|
|
|
// When restoring the session.
|
|
guard case .failure(.failedRestoringLogin) = await store.restoreUserSession() else {
|
|
Issue.record("Restoration should fail when the non-transient user data is missing.")
|
|
return
|
|
}
|
|
|
|
// Then the credentials should have been discarded.
|
|
#expect(keychainController.removeRestorationTokenForUsernameReceivedInvocations == [credentials.userID])
|
|
}
|
|
|
|
@Test
|
|
func restoreWhenClientCreationFails() async throws {
|
|
// Given valid session data but a client factory that fails.
|
|
let sessionDirectories = try makeValidSessionDirectories()
|
|
defer { sessionDirectories.delete() }
|
|
let credentials = makeCredentials(sessionDirectories: sessionDirectories)
|
|
keychainController.restorationTokensReturnValue = [credentials]
|
|
clientFactory.makeAppClientCredentialsClientSessionDelegateAppSettingsAppHooksThrowableError = TestError.generic
|
|
|
|
// When restoring the session.
|
|
guard case .failure(.failedRestoringLogin) = await store.restoreUserSession() else {
|
|
Issue.record("Restoration should fail when the client can't be created.")
|
|
return
|
|
}
|
|
|
|
// Then the credentials should have been discarded.
|
|
#expect(keychainController.removeRestorationTokenForUsernameReceivedInvocations == [credentials.userID])
|
|
}
|
|
|
|
@Test
|
|
func restoreSucceeds() async throws {
|
|
// Given valid session data and a client factory that returns a client.
|
|
let sessionDirectories = try makeValidSessionDirectories()
|
|
defer { sessionDirectories.delete() }
|
|
keychainController.restorationTokensReturnValue = [makeCredentials(sessionDirectories: sessionDirectories)]
|
|
|
|
// When restoring the session.
|
|
guard case .success(let userSession) = await store.restoreUserSession() else {
|
|
Issue.record("Restoration should succeed.")
|
|
return
|
|
}
|
|
|
|
// Then a user session should be built for the restored client.
|
|
#expect(userSession.clientProxy.userID == "@alice:matrix.org")
|
|
}
|
|
|
|
// MARK: - Session creation
|
|
|
|
@Test
|
|
func userSessionForClientSucceeds() async {
|
|
// Given a freshly authenticated client from the SDK.
|
|
let client = ClientSDKMock(.init(userID: "@alice:matrix.org"))
|
|
let sessionDirectories = SessionDirectories()
|
|
|
|
// When creating a user session for it.
|
|
guard case .success(let userSession) = await store.userSession(for: client, sessionDirectories: sessionDirectories, passphrase: RestorationTokenFixtures.passphrase) else {
|
|
Issue.record("Creating the session should succeed.")
|
|
return
|
|
}
|
|
|
|
// Then the session should be built and its restoration token persisted.
|
|
#expect(userSession.clientProxy.userID == "@alice:matrix.org")
|
|
#expect(keychainController.setRestorationTokenForUsernameReceivedArguments?.forUsername == "@alice:matrix.org")
|
|
#expect(keychainController.setRestorationTokenForUsernameReceivedArguments?.restorationToken.passphrase == RestorationTokenFixtures.passphrase)
|
|
}
|
|
|
|
@Test
|
|
func userSessionForClientFails() async {
|
|
// Given a freshly authenticated client that fails to provide its session.
|
|
let client = ClientSDKMock(.init(userID: "@alice:matrix.org"))
|
|
client.sessionThrowableError = TestError.generic
|
|
|
|
// When creating a user session for it.
|
|
guard case .failure(.failedSettingUpSession) = await store.userSession(for: client, sessionDirectories: .init(), passphrase: RestorationTokenFixtures.passphrase) else {
|
|
Issue.record("Creating the session should fail.")
|
|
return
|
|
}
|
|
|
|
// Then nothing should have been persisted.
|
|
#expect(!keychainController.setRestorationTokenForUsernameCalled)
|
|
}
|
|
|
|
// MARK: - Logout/Reset
|
|
|
|
@Test
|
|
func logout() {
|
|
// Given a stored session.
|
|
let userID = "@alice:matrix.org"
|
|
keychainController.restorationTokensReturnValue = [makeCredentials(sessionDirectories: .init(), userID: userID)]
|
|
let userSession = UserSessionMock(.init(clientProxy: ClientProxyMock(.init(userID: userID))))
|
|
|
|
// When logging out.
|
|
store.logout(userSession: userSession)
|
|
|
|
// Then the stored credentials should be removed.
|
|
#expect(keychainController.removeRestorationTokenForUsernameReceivedInvocations == [userID])
|
|
}
|
|
|
|
@Test
|
|
func reset() {
|
|
store.reset()
|
|
#expect(keychainController.removeAllRestorationTokensCalled)
|
|
}
|
|
|
|
// MARK: - Helpers
|
|
|
|
private enum TestError: Error { case generic }
|
|
|
|
private func makeCredentials(sessionDirectories: SessionDirectories, userID: String = "@alice:matrix.org") -> KeychainCredentials {
|
|
let session = Session(accessToken: "accessToken",
|
|
refreshToken: nil,
|
|
userId: userID,
|
|
deviceId: "deviceID",
|
|
homeserverUrl: "https://matrix.org",
|
|
oauthData: nil,
|
|
slidingSyncVersion: .native)
|
|
let restorationToken = RestorationToken(session: session,
|
|
sessionDirectories: sessionDirectories,
|
|
passphrase: RestorationTokenFixtures.passphrase,
|
|
pusherNotificationClientIdentifier: nil)
|
|
return KeychainCredentials(userID: userID, restorationToken: restorationToken)
|
|
}
|
|
|
|
private func makeValidSessionDirectories() throws -> SessionDirectories {
|
|
let sessionDirectories = SessionDirectories()
|
|
try FileManager.default.createDirectory(at: sessionDirectories.dataDirectory, withIntermediateDirectories: true)
|
|
try FileManager.default.createDirectory(at: sessionDirectories.cacheDirectory, withIntermediateDirectories: true)
|
|
sessionDirectories.generateMockData()
|
|
return sessionDirectories
|
|
}
|
|
}
|