mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-11-03 06:55:59 +00:00
Introduce a user session manager (#6295)
This commit is contained in:
@@ -38,6 +38,7 @@ struct ChatsTabFlowCoordinatorTests {
|
||||
|
||||
let userSession = UserSessionMock(.init(clientProxy: clientProxy))
|
||||
let flowParameters = CommonFlowParameters(userSession: userSession,
|
||||
availableSessionsPublisher: .init([.init(userSession: userSession)]),
|
||||
bugReportService: BugReportServiceMock(.init()),
|
||||
elementCallService: ElementCallServiceMock(.init()),
|
||||
timelineControllerFactory: timelineControllerFactory,
|
||||
|
||||
@@ -460,6 +460,19 @@ final class HomeScreenViewModelTests {
|
||||
try await deferred.fulfill()
|
||||
}
|
||||
|
||||
@Test
|
||||
func multiAccountAnnouncementIsMarkedSeenWithSeveralAccounts() async throws {
|
||||
userSettings.app.multiAccountEnabled = true
|
||||
userSettings.app.hasSeenMultiAccountAnnouncement = false
|
||||
setupViewModel(otherAccountUserIDs: ["@other:client.com"])
|
||||
|
||||
let deferred = deferFailure(context.$viewState, timeout: .seconds(1)) { $0.bindings.isPresentingMultiAccountAnnouncement }
|
||||
context.send(viewAction: .screenAppeared)
|
||||
try await deferred.fulfill()
|
||||
|
||||
#expect(userSettings.app.hasSeenMultiAccountAnnouncement)
|
||||
}
|
||||
|
||||
// MARK: - Helpers
|
||||
|
||||
enum InviteType { case rooms, spaces }
|
||||
@@ -551,7 +564,7 @@ final class HomeScreenViewModelTests {
|
||||
return (roomListSubject, stateSubject)
|
||||
}
|
||||
|
||||
private func setupViewModel(securityStatePublisher: CurrentValuePublisher<SessionSecurityState, Never>? = nil, invites: InviteType? = nil, roomSummaryProvider: RoomSummaryProviderMock? = nil) {
|
||||
private func setupViewModel(otherAccountUserIDs: [String] = [], securityStatePublisher: CurrentValuePublisher<SessionSecurityState, Never>? = nil, invites: InviteType? = nil, roomSummaryProvider: RoomSummaryProviderMock? = nil) {
|
||||
cancellables.removeAll()
|
||||
|
||||
var rooms: [RoomSummary] = .mockRooms
|
||||
@@ -595,7 +608,10 @@ final class HomeScreenViewModelTests {
|
||||
|
||||
notificationManager = NotificationManagerMock()
|
||||
|
||||
let sessionDetails = [UserSessionDetails(userSession: userSession)] + otherAccountUserIDs.map { UserSessionDetails(userID: $0) }
|
||||
|
||||
viewModel = HomeScreenViewModel(userSession: userSession,
|
||||
availableSessionsPublisher: .init(sessionDetails),
|
||||
selectedRoomPublisher: CurrentValueSubject<String?, Never>(nil).asCurrentValuePublisher(),
|
||||
analyticsService: AnalyticsServiceMock(.init()),
|
||||
bugReportService: BugReportServiceMock(.init()),
|
||||
|
||||
@@ -522,7 +522,9 @@ final class RoomFlowCoordinatorTests {
|
||||
"1"
|
||||
}
|
||||
|
||||
let flowParameters = CommonFlowParameters(userSession: UserSessionMock(.init(clientProxy: clientProxy, userSettings: userSettings)),
|
||||
let userSession = UserSessionMock(.init(clientProxy: clientProxy, userSettings: userSettings))
|
||||
let flowParameters = CommonFlowParameters(userSession: userSession,
|
||||
availableSessionsPublisher: .init([.init(userSession: userSession)]),
|
||||
bugReportService: BugReportServiceMock(.init()),
|
||||
elementCallService: ElementCallServiceMock(.init()),
|
||||
timelineControllerFactory: timelineControllerFactory,
|
||||
|
||||
@@ -54,6 +54,7 @@ struct RoomMembersFlowCoordinatorTests {
|
||||
let userSession = UserSessionMock(.init(clientProxy: clientProxy))
|
||||
|
||||
let flowParameters = CommonFlowParameters(userSession: userSession,
|
||||
availableSessionsPublisher: .init([.init(userSession: userSession)]),
|
||||
bugReportService: BugReportServiceMock(.init()),
|
||||
elementCallService: ElementCallServiceMock(.init()),
|
||||
timelineControllerFactory: TimelineControllerFactoryMock(.init()),
|
||||
|
||||
@@ -51,6 +51,7 @@ struct UserSessionFlowCoordinatorTests {
|
||||
let userSession = UserSessionMock(.init(clientProxy: clientProxy))
|
||||
|
||||
let flowParameters = CommonFlowParameters(userSession: userSession,
|
||||
availableSessionsPublisher: .init([.init(userSession: userSession)]),
|
||||
bugReportService: BugReportServiceMock(.init()),
|
||||
elementCallService: ElementCallServiceMock(.init()),
|
||||
timelineControllerFactory: TimelineControllerFactoryMock(.init()),
|
||||
|
||||
@@ -0,0 +1,119 @@
|
||||
//
|
||||
// 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
|
||||
|
||||
@MainActor
|
||||
struct UserSessionManagerTests {
|
||||
let userSessionStore = UserSessionStoreMock()
|
||||
let appSettings = AppSettings.volatile()
|
||||
|
||||
@Test
|
||||
func ordersAccountsByWhenTheyWereLastSelected() {
|
||||
appSettings.userSettings(for: "@alice:matrix.org").account.lastSelectedDate = .now.addingTimeInterval(-60)
|
||||
appSettings.userSettings(for: "@bob:matrix.org").account.lastSelectedDate = .now
|
||||
|
||||
// Accounts that were never selected (e.g. after upgrading from a single account) come last.
|
||||
let manager = makeManager(userIDs: ["@dave:matrix.org", "@alice:matrix.org", "@carol:matrix.org", "@bob:matrix.org"])
|
||||
|
||||
#expect(manager.userIDs == ["@bob:matrix.org", "@alice:matrix.org", "@carol:matrix.org", "@dave:matrix.org"])
|
||||
}
|
||||
|
||||
@Test
|
||||
func restoreFallsBackToTheNextAccount() async {
|
||||
let manager = makeManager(userIDs: ["@alice:matrix.org", "@bob:matrix.org"])
|
||||
// Alice already has a live session, e.g. when restoring again after clearing the cache.
|
||||
manager.add(makeUserSession(userID: "@alice:matrix.org"))
|
||||
let bob = makeUserSession(userID: "@bob:matrix.org")
|
||||
userSessionStore.restoreUserSessionUserIDClosure = { userID in
|
||||
userID == "@bob:matrix.org" ? .success(bob) : .failure(.failedRestoringLogin)
|
||||
}
|
||||
|
||||
guard case .success(let userSession) = await manager.restoreActiveSession() else {
|
||||
Issue.record("Restoring should fall back to Bob.")
|
||||
return
|
||||
}
|
||||
|
||||
#expect(userSession.clientProxy.userID == "@bob:matrix.org")
|
||||
#expect(manager.userIDs == ["@bob:matrix.org"])
|
||||
#expect(manager.session(for: "@alice:matrix.org") == nil)
|
||||
// Nothing can use the session until it's added, e.g. while migrations run.
|
||||
#expect(manager.activeSession == nil)
|
||||
}
|
||||
|
||||
@Test
|
||||
func restoreFailsWhenNoAccountCanBeRestored() async {
|
||||
let manager = makeManager(userIDs: ["@alice:matrix.org", "@bob:matrix.org"])
|
||||
userSessionStore.restoreUserSessionUserIDReturnValue = .failure(.failedRestoringLogin)
|
||||
|
||||
guard case .failure(.failedRestoringSessions) = await manager.restoreActiveSession() else {
|
||||
Issue.record("Restoring should fail once every account has failed.")
|
||||
return
|
||||
}
|
||||
|
||||
#expect(userSessionStore.restoreUserSessionUserIDReceivedInvocations == ["@alice:matrix.org", "@bob:matrix.org"])
|
||||
#expect(manager.userIDs.isEmpty)
|
||||
}
|
||||
|
||||
@Test
|
||||
func addRegistersTheSession() {
|
||||
let manager = makeManager(userIDs: ["@alice:matrix.org", "@bob:matrix.org"])
|
||||
|
||||
// A known account keeps its place, e.g. after clearing the cache.
|
||||
let bob = makeUserSession(userID: "@bob:matrix.org")
|
||||
manager.add(bob)
|
||||
#expect(manager.userIDs == ["@alice:matrix.org", "@bob:matrix.org"])
|
||||
#expect((manager.session(for: "@bob:matrix.org") as? UserSessionMock) === bob)
|
||||
#expect(manager.activeSession == nil)
|
||||
|
||||
// A new account becomes the active one.
|
||||
let carol = makeUserSession(userID: "@carol:matrix.org")
|
||||
manager.add(carol)
|
||||
#expect(manager.userIDs == ["@carol:matrix.org", "@alice:matrix.org", "@bob:matrix.org"])
|
||||
#expect(appSettings.userSettings(for: "@carol:matrix.org").account.lastSelectedDate != nil)
|
||||
#expect((manager.activeSession as? UserSessionMock) === carol)
|
||||
#expect(manager.sessionsPublisher.value.map(\.clientProxy.userID) == ["@carol:matrix.org", "@bob:matrix.org"])
|
||||
}
|
||||
|
||||
@Test
|
||||
func addReplacesTheSessionOfAKnownAccount() {
|
||||
let manager = makeManager(userIDs: ["@alice:matrix.org", "@bob:matrix.org"])
|
||||
manager.add(makeUserSession(userID: "@alice:matrix.org"))
|
||||
|
||||
// E.g. restoring again after clearing the cache, or signing in again after a soft logout.
|
||||
let newAlice = makeUserSession(userID: "@alice:matrix.org")
|
||||
manager.add(newAlice)
|
||||
|
||||
#expect(manager.userIDs == ["@alice:matrix.org", "@bob:matrix.org"])
|
||||
#expect((manager.activeSession as? UserSessionMock) === newAlice)
|
||||
}
|
||||
|
||||
@Test
|
||||
func removeDeletesTheAccount() {
|
||||
let manager = makeManager(userIDs: ["@alice:matrix.org"])
|
||||
let alice = makeUserSession(userID: "@alice:matrix.org")
|
||||
manager.add(alice)
|
||||
|
||||
manager.remove(userID: "@alice:matrix.org")
|
||||
|
||||
#expect((userSessionStore.logoutUserSessionReceivedUserSession as? UserSessionMock) === alice)
|
||||
#expect(manager.userIDs.isEmpty)
|
||||
#expect(manager.activeSession == nil)
|
||||
}
|
||||
|
||||
// MARK: - Helpers
|
||||
|
||||
private func makeManager(userIDs: [String]) -> UserSessionManager {
|
||||
userSessionStore.userIDs = userIDs
|
||||
return UserSessionManager(userSessionStore: userSessionStore, appSettings: appSettings)
|
||||
}
|
||||
|
||||
private func makeUserSession(userID: String) -> UserSessionMock {
|
||||
UserSessionMock(.init(clientProxy: ClientProxyMock(.init(userID: userID))))
|
||||
}
|
||||
}
|
||||
@@ -46,8 +46,8 @@ struct UserSessionStoreTests {
|
||||
// MARK: - Restoration
|
||||
|
||||
@Test
|
||||
func restoreWithoutCredentials() async {
|
||||
guard case .failure(.missingCredentials) = await store.restoreUserSession() else {
|
||||
func restoreUnknownAccount() async {
|
||||
guard case .failure(.unknownAccount) = await store.restoreUserSession(userID: "@alice:matrix.org") else {
|
||||
Issue.record("Restoration should fail when there are no credentials.")
|
||||
return
|
||||
}
|
||||
@@ -60,7 +60,7 @@ struct UserSessionStoreTests {
|
||||
keychainController.restorationTokensReturnValue = [credentials]
|
||||
|
||||
// When restoring the session.
|
||||
guard case .failure(.failedRestoringLogin) = await store.restoreUserSession() else {
|
||||
guard case .failure(.failedRestoringLogin) = await store.restoreUserSession(userID: credentials.userID) else {
|
||||
Issue.record("Restoration should fail when the non-transient user data is missing.")
|
||||
return
|
||||
}
|
||||
@@ -79,7 +79,7 @@ struct UserSessionStoreTests {
|
||||
clientFactory.makeAppClientCredentialsClientSessionDelegateAppSettingsAppHooksThrowableError = TestError.generic
|
||||
|
||||
// When restoring the session.
|
||||
guard case .failure(.failedRestoringLogin) = await store.restoreUserSession() else {
|
||||
guard case .failure(.failedRestoringLogin) = await store.restoreUserSession(userID: credentials.userID) else {
|
||||
Issue.record("Restoration should fail when the client can't be created.")
|
||||
return
|
||||
}
|
||||
@@ -90,18 +90,19 @@ struct UserSessionStoreTests {
|
||||
|
||||
@Test
|
||||
func restoreSucceeds() async throws {
|
||||
// Given valid session data and a client factory that returns a client.
|
||||
// Given valid session data for Alice, stored after another account.
|
||||
let sessionDirectories = try makeValidSessionDirectories()
|
||||
defer { try? sessionDirectories.delete() }
|
||||
keychainController.restorationTokensReturnValue = [makeCredentials(sessionDirectories: sessionDirectories)]
|
||||
keychainController.restorationTokensReturnValue = [makeCredentials(sessionDirectories: .init(), userID: "@bob:matrix.org"),
|
||||
makeCredentials(sessionDirectories: sessionDirectories)]
|
||||
|
||||
// When restoring the session.
|
||||
guard case .success(let userSession) = await store.restoreUserSession() else {
|
||||
// When restoring Alice's session.
|
||||
guard case .success(let userSession) = await store.restoreUserSession(userID: "@alice:matrix.org") else {
|
||||
Issue.record("Restoration should succeed.")
|
||||
return
|
||||
}
|
||||
|
||||
// Then a user session should be built for the restored client.
|
||||
// Then a user session should be built for Alice's client.
|
||||
#expect(userSession.clientProxy.userID == "@alice:matrix.org")
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user