Initial implementation of AccountSettings. (#6275)

* Make the UserPreference macro also generate an accessor for the key name.

* Initial implementation of AccountSettings.

- Add an AccountUserDefaults storage with a userID prefix on the key.
- Migrate the 2 values reset by AppSettings.resetSessionSpecificSettings to AccountSettings.

* Rename UserSettings.volatile() to UserSettings.mock()

Given it now has a userID parameter with a default value..
This commit is contained in:
Doug
2026-10-06 17:52:24 +01:00
committed by GitHub
parent acd18f3aa9
commit 93825c01ad
96 changed files with 623 additions and 248 deletions
@@ -0,0 +1,93 @@
//
// 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 AccountUserDefaultsTests {
private let store = VolatileUserDefaults()
private let aliceDefaults: AccountUserDefaults
private let bobDefaults: AccountUserDefaults
init() {
aliceDefaults = AccountUserDefaults(userID: "@alice:example.com", store: store)
bobDefaults = AccountUserDefaults(userID: "@bob:example.com", store: store)
}
@Test
func dictionaryRepresentation() {
// Given a store containing values for Alice, Bob and the app, all sharing a key.
aliceDefaults.set("one", forKey: "first")
aliceDefaults.set(2, forKey: "second")
bobDefaults.set("bob", forKey: "first")
store.set("global", forKey: "first")
#expect(aliceDefaults.object(forKey: "first") as? String == "one")
#expect(aliceDefaults.object(forKey: "second") as? Int == 2)
#expect(bobDefaults.object(forKey: "first") as? String == "bob")
#expect(store.object(forKey: "first") as? String == "global")
// When getting the dictionary representation of Alice's defaults.
let representation = aliceDefaults.dictionaryRepresentation()
// Then it should only contain Alice's values.
#expect(representation.count == 2)
#expect(representation["first"] as? String == "one")
#expect(representation["second"] as? Int == 2)
}
@Test
func reset() {
// Given a store containing values for Alice, Bob and the app, all sharing a key.
aliceDefaults.set("one", forKey: "first")
aliceDefaults.set(2, forKey: "second")
bobDefaults.set("bob", forKey: "first")
store.set("global", forKey: "first")
#expect(aliceDefaults.object(forKey: "first") as? String == "one")
#expect(aliceDefaults.object(forKey: "second") as? Int == 2)
#expect(bobDefaults.object(forKey: "first") as? String == "bob")
#expect(store.object(forKey: "first") as? String == "global")
// When resetting Alice's defaults.
aliceDefaults.reset()
// Then only Alice's values should be removed.
#expect(aliceDefaults.object(forKey: "first") == nil)
#expect(aliceDefaults.object(forKey: "second") == nil)
#expect(bobDefaults.object(forKey: "first") as? String == "bob")
#expect(store.object(forKey: "first") as? String == "global")
}
@Test
func migrateAppSettingsValue() {
// Given a store containing an app value.
store.set("global", forKey: "first")
#expect(store.object(forKey: "first") as? String == "global")
#expect(aliceDefaults.object(forKey: "first") == nil)
#expect(bobDefaults.object(forKey: "first") == nil)
// When migrating that value into Alice's defaults.
aliceDefaults.migrateAppSettingsValue(forKey: "first")
// Then the value should be moved to Alice's defaults only.
#expect(aliceDefaults.object(forKey: "first") as? String == "global")
#expect(store.object(forKey: "first") == nil)
#expect(bobDefaults.object(forKey: "first") == nil)
}
@Test
func migrateUnsetAppSettingsValue() {
// Given an empty store.
#expect(store.storage.isEmpty)
// When migrating a value that isn't set into Alice's defaults.
aliceDefaults.migrateAppSettingsValue(forKey: "first")
// Then nothing should be written to the store.
#expect(aliceDefaults.object(forKey: "first") == nil)
#expect(store.storage.isEmpty)
}
}
@@ -23,7 +23,7 @@ final class AnalyticsSettingsScreenViewModelTests {
client.isRunning = false
analytics = AnalyticsService(client: client, appSettings: appSettings)
viewModel = AnalyticsSettingsScreenViewModel(userSettings: UserSettings(appSettings: appSettings, accountSettings: .init()),
viewModel = AnalyticsSettingsScreenViewModel(userSettings: appSettings.userSettings(for: "@me:matrix.org"),
analytics: analytics)
context = viewModel.context
}
@@ -17,7 +17,7 @@ final class BugReportServiceTests {
var bugReportService: BugReportServiceProtocol!
init() throws {
userSettings = UserSettings.volatile()
userSettings = UserSettings.mock()
userSettings.bugReportRageshakeURL.reset()
let bugReportServiceMock = BugReportServiceMock()
@@ -29,7 +29,7 @@ struct ChatsTabFlowCoordinatorTests {
}
init() async throws {
let userSettings = UserSettings.volatile()
let userSettings = UserSettings.mock()
clientProxy = ClientProxyMock(.init(userID: "hi@bob", roomSummaryProvider: RoomSummaryProviderMock(.init(state: .loaded(.mockRooms)))))
timelineControllerFactory = TimelineControllerFactoryMock(.init())
@@ -84,7 +84,7 @@ final class ComposerToolbarViewModelTests {
.init(suggestionType: .user(.init(id: "@user_mention_2:matrix.org", displayName: "User 2", avatarURL: nil)), range: .init(), rawSuggestionText: "")]
let mockCompletionSuggestionService = CompletionSuggestionServiceMock(configuration: .init(suggestions: suggestions))
let userSettings = UserSettings.volatile()
let userSettings = UserSettings.mock()
viewModel = ComposerToolbarViewModel(roomProxy: JoinedRoomProxyMock(.init()),
wysiwygViewModel: wysiwygViewModel,
@@ -803,7 +803,7 @@ final class ComposerToolbarViewModelTests {
let mockSubject = CurrentValueSubject<[IdentityStatusChange], Never>([])
roomProxyMock.identityStatusChangesPublisher = mockSubject.asCurrentValuePublisher()
let userSettings = UserSettings.volatile()
let userSettings = UserSettings.mock()
viewModel = ComposerToolbarViewModel(roomProxy: roomProxyMock,
wysiwygViewModel: wysiwygViewModel,
@@ -846,7 +846,7 @@ final class ComposerToolbarViewModelTests {
roomProxyMock.identityStatusChangesPublisher = mockSubject.asCurrentValuePublisher()
let userSettings = UserSettings.volatile()
let userSettings = UserSettings.mock()
viewModel = ComposerToolbarViewModel(roomProxy: roomProxyMock,
wysiwygViewModel: wysiwygViewModel,
@@ -886,7 +886,7 @@ final class ComposerToolbarViewModelTests {
}
roomProxyMock.identityStatusChangesPublisher = CurrentValueSubject([IdentityStatusChange(userId: "@alice:localhost", changedTo: .pinViolation)]).asCurrentValuePublisher()
let userSettings = UserSettings.volatile()
let userSettings = UserSettings.mock()
viewModel = ComposerToolbarViewModel(roomProxy: roomProxyMock,
wysiwygViewModel: wysiwygViewModel,
@@ -923,7 +923,7 @@ final class ComposerToolbarViewModelTests {
draftServiceMock.loadDraftClosure = loadDraftClosure
}
let userSettings = UserSettings.volatile()
let userSettings = UserSettings.mock()
viewModel = ComposerToolbarViewModel(initialText: initialText,
roomProxy: JoinedRoomProxyMock(.init()),
@@ -24,7 +24,7 @@ final class CreateRoomScreenViewModelTests {
}
init() {
userSettings = UserSettings.volatile()
userSettings = UserSettings.mock()
}
isolated deinit {
@@ -55,7 +55,7 @@ struct EmojiPickerScreenViewModelTests {
emojiPickerStream = stream
viewModel = EmojiPickerScreenViewModel(selectedEmojis: selectedEmojis,
emojiProvider: EmojiProvider(userSettings: .volatile()),
emojiProvider: EmojiProvider(userSettings: .mock()),
continuation: continuation)
}
}
+4 -4
View File
@@ -19,7 +19,7 @@ struct EmojiProviderTests {
let emojiLoaderMock = EmojiLoaderMock()
emojiLoaderMock.categories = [category]
let emojiProvider = EmojiProvider(loader: emojiLoaderMock, userSettings: .volatile())
let emojiProvider = EmojiProvider(loader: emojiLoaderMock, userSettings: .mock())
let categories = await emojiProvider.categories()
#expect(emojiLoaderMock.categories == categories)
@@ -33,7 +33,7 @@ struct EmojiProviderTests {
let emojiLoaderMock = EmojiLoaderMock()
emojiLoaderMock.categories = [category]
let emojiProvider = EmojiProvider(loader: emojiLoaderMock, userSettings: .volatile())
let emojiProvider = EmojiProvider(loader: emojiLoaderMock, userSettings: .mock())
let categories = await emojiProvider.categories(searchString: "")
#expect(emojiLoaderMock.categories == categories)
@@ -51,7 +51,7 @@ struct EmojiProviderTests {
let emojiLoaderMock = EmojiLoaderMock()
emojiLoaderMock.categories = categoriesForFirstLoad
let emojiProvider = EmojiProvider(loader: emojiLoaderMock, userSettings: .volatile())
let emojiProvider = EmojiProvider(loader: emojiLoaderMock, userSettings: .mock())
_ = await emojiProvider.categories()
emojiLoaderMock.categories = categoriesForSecondLoad
@@ -82,7 +82,7 @@ struct EmojiProviderTests {
let emojiLoaderMock = EmojiLoaderMock()
emojiLoaderMock.categories = categories
let emojiProvider = EmojiProvider(loader: emojiLoaderMock, userSettings: .volatile())
let emojiProvider = EmojiProvider(loader: emojiLoaderMock, userSettings: .mock())
_ = await emojiProvider.categories()
let result = await emojiProvider.categories(searchString: searchString)
@@ -25,7 +25,7 @@ final class HomeScreenViewModelTests {
var cancellables = Set<AnyCancellable>()
init() {
userSettings = UserSettings.volatile()
userSettings = UserSettings.mock()
}
@Test
@@ -109,7 +109,7 @@ struct IdentityConfirmationScreenViewModelTests {
userSession.sessionSecurityStatePublisher = securityStateSubject.asCurrentValuePublisher()
viewModel = IdentityConfirmationScreenViewModel(userSession: userSession,
userSettings: .volatile(),
userSettings: .mock(),
userIndicatorController: UserIndicatorControllerMock())
}
}
@@ -34,7 +34,7 @@ final class JoinRoomScreenViewModelTests {
private let userSettings: UserSettings
init() {
userSettings = UserSettings.volatile()
userSettings = UserSettings.mock()
}
isolated deinit {
@@ -228,7 +228,7 @@ final class LiveLocationManagerTests {
}
private func setUp(accuracyAuthorization: CLAccuracyAuthorization = .fullAccuracy) {
userSettings = UserSettings.volatile()
userSettings = UserSettings.mock()
clientProxy = ClientProxyMock(.init())
beaconInfoSubject = PassthroughSubject<LiveLocationOwnInfoUpdate, Never>()
clientProxy.liveLocationOwnInfoUpdatesPublisher = beaconInfoSubject.eraseToAnyPublisher()
@@ -417,7 +417,7 @@ struct LocationSharingScreenViewModelTests {
let roomProxyMock = JoinedRoomProxyMock(.init(members: .allMembers))
roomProxyMock.makeLiveLocationServiceReturnValue = liveLocationServiceMock
let userSettings = UserSettings.volatile()
let userSettings = UserSettings.mock()
viewModel = LocationSharingScreenViewModel(interactionMode: .viewLive(sender: nil, initialLiveLocationShare: nil),
mapURLBuilder: userSettings.mapTilerConfiguration.publisher.value,
@@ -630,7 +630,7 @@ struct LocationSharingScreenViewModelTests {
private mutating func setupViewModel(liveLocationManagerMock: LiveLocationManagerMock,
members: [RoomMemberProxyMock] = .allMembersAsAdmin) {
userSettings = UserSettings.volatile()
userSettings = UserSettings.mock()
timelineProxy = TimelineProxyMock(.init())
viewModel = LocationSharingScreenViewModel(interactionMode: .picker(shouldShowLiveLocationOption: true),
mapURLBuilder: userSettings.mapTilerConfiguration.publisher.value,
@@ -649,7 +649,7 @@ struct LocationSharingScreenViewModelTests {
liveLocationsSubject: CurrentValueSubject<[LiveLocationShare], Never>,
members: [RoomMemberProxyMock] = .allMembers,
isSharingLiveLocationFromThisDevice: Bool = false) {
userSettings = UserSettings.volatile()
userSettings = UserSettings.mock()
if isSharingLiveLocationFromThisDevice {
userSettings.liveLocationSharingSessionsByRoomID[Self.roomID] = .init(eventID: "$event:matrix.org", expirationDate: .distantFuture)
}
@@ -28,7 +28,7 @@ final class MediaUploadPreviewScreenViewModelTests {
}
init() {
userSettings = UserSettings.volatile()
userSettings = UserSettings.mock()
userSettings.optimizeMediaUploads = false
userIndicatorController = UserIndicatorControllerMock()
@@ -17,7 +17,7 @@ final class MediaUploadingPreprocessorTests {
var mediaUploadingPreprocessor: MediaUploadingPreprocessor!
init() {
userSettings = UserSettings.volatile()
userSettings = UserSettings.mock()
userSettings.optimizeMediaUploads = false
mediaUploadingPreprocessor = MediaUploadingPreprocessor(userSettings: userSettings)
}
@@ -23,7 +23,7 @@ struct NotificationSettingsScreenViewModelTests {
init() throws {
userNotificationCenter = UserNotificationCenterMock()
userNotificationCenter.authorizationStatusReturnValue = .authorized
userSettings = UserSettings.volatile()
userSettings = UserSettings.mock()
notificationSettingsProxy = NotificationSettingsProxyMock(with: NotificationSettingsProxyMockConfiguration())
notificationSettingsProxy.getDefaultRoomNotificationModeIsEncryptedIsOneToOneReturnValue = .allMessages
notificationSettingsProxy.isRoomMentionEnabledReturnValue = true
@@ -18,7 +18,7 @@ struct NotificationToneManagerTests {
private let manager: NotificationToneManager
init() {
manager = NotificationToneManager(userSettings: .volatile())
manager = NotificationToneManager(userSettings: .mock())
}
// MARK: - Deletion
+11 -11
View File
@@ -28,7 +28,7 @@ struct PillContextTests {
userIndicatorController: userIndicatorController,
appMediator: AppMediatorMock(.init()),
analyticsService: AnalyticsServiceMock(.init()),
emojiProvider: EmojiProvider(userSettings: .volatile()),
emojiProvider: EmojiProvider(userSettings: .mock()),
linkMetadataProvider: LinkMetadataProvider(),
timelineControllerFactory: TimelineControllerFactoryMock(.init()))
let context = PillContext(timelineContext: mock.context, data: PillTextAttachmentData(type: .user(userID: id), font: .preferredFont(forTextStyle: .body)))
@@ -60,7 +60,7 @@ struct PillContextTests {
userIndicatorController: userIndicatorController,
appMediator: AppMediatorMock(.init()),
analyticsService: AnalyticsServiceMock(.init()),
emojiProvider: EmojiProvider(userSettings: .volatile()),
emojiProvider: EmojiProvider(userSettings: .mock()),
linkMetadataProvider: LinkMetadataProvider(),
timelineControllerFactory: TimelineControllerFactoryMock(.init()))
let context = PillContext(timelineContext: mock.context, data: PillTextAttachmentData(type: .user(userID: id), font: .preferredFont(forTextStyle: .body)))
@@ -84,7 +84,7 @@ struct PillContextTests {
userIndicatorController: userIndicatorController,
appMediator: AppMediatorMock(.init()),
analyticsService: AnalyticsServiceMock(.init()),
emojiProvider: EmojiProvider(userSettings: .volatile()),
emojiProvider: EmojiProvider(userSettings: .mock()),
linkMetadataProvider: LinkMetadataProvider(),
timelineControllerFactory: TimelineControllerFactoryMock(.init()))
let context = PillContext(timelineContext: mock.context, data: PillTextAttachmentData(type: .allUsers, font: .preferredFont(forTextStyle: .body)))
@@ -108,7 +108,7 @@ struct PillContextTests {
userIndicatorController: userIndicatorController,
appMediator: AppMediatorMock(.init()),
analyticsService: AnalyticsServiceMock(.init()),
emojiProvider: EmojiProvider(userSettings: .volatile()),
emojiProvider: EmojiProvider(userSettings: .mock()),
linkMetadataProvider: LinkMetadataProvider(),
timelineControllerFactory: TimelineControllerFactoryMock(.init()))
let context = PillContext(timelineContext: mock.context, data: PillTextAttachmentData(type: .roomID("1"), font: .preferredFont(forTextStyle: .body)))
@@ -131,7 +131,7 @@ struct PillContextTests {
userIndicatorController: userIndicatorController,
appMediator: AppMediatorMock(.init()),
analyticsService: AnalyticsServiceMock(.init()),
emojiProvider: EmojiProvider(userSettings: .volatile()),
emojiProvider: EmojiProvider(userSettings: .mock()),
linkMetadataProvider: LinkMetadataProvider(),
timelineControllerFactory: TimelineControllerFactoryMock(.init()))
let context = PillContext(timelineContext: mock.context, data: PillTextAttachmentData(type: .roomID("1"), font: .preferredFont(forTextStyle: .body)))
@@ -158,7 +158,7 @@ struct PillContextTests {
userIndicatorController: userIndicatorController,
appMediator: AppMediatorMock(.init()),
analyticsService: AnalyticsServiceMock(.init()),
emojiProvider: EmojiProvider(userSettings: .volatile()),
emojiProvider: EmojiProvider(userSettings: .mock()),
linkMetadataProvider: LinkMetadataProvider(),
timelineControllerFactory: TimelineControllerFactoryMock(.init()))
let context = PillContext(timelineContext: mock.context, data: PillTextAttachmentData(type: .roomAlias("#foundation-and-empire:matrix.org"), font: .preferredFont(forTextStyle: .body)))
@@ -181,7 +181,7 @@ struct PillContextTests {
userIndicatorController: userIndicatorController,
appMediator: AppMediatorMock(.init()),
analyticsService: AnalyticsServiceMock(.init()),
emojiProvider: EmojiProvider(userSettings: .volatile()),
emojiProvider: EmojiProvider(userSettings: .mock()),
linkMetadataProvider: LinkMetadataProvider(),
timelineControllerFactory: TimelineControllerFactoryMock(.init()))
let context = PillContext(timelineContext: mock.context, data: PillTextAttachmentData(type: .roomAlias("#foundation-and-empire:matrix.org"), font: .preferredFont(forTextStyle: .body)))
@@ -206,7 +206,7 @@ struct PillContextTests {
userIndicatorController: userIndicatorController,
appMediator: AppMediatorMock(.init()),
analyticsService: AnalyticsServiceMock(.init()),
emojiProvider: EmojiProvider(userSettings: .volatile()),
emojiProvider: EmojiProvider(userSettings: .mock()),
linkMetadataProvider: LinkMetadataProvider(),
timelineControllerFactory: TimelineControllerFactoryMock(.init()))
let context = PillContext(timelineContext: mock.context, data: PillTextAttachmentData(type: .event(room: .roomID("1")), font: .preferredFont(forTextStyle: .body)))
@@ -229,7 +229,7 @@ struct PillContextTests {
userIndicatorController: userIndicatorController,
appMediator: AppMediatorMock(.init()),
analyticsService: AnalyticsServiceMock(.init()),
emojiProvider: EmojiProvider(userSettings: .volatile()),
emojiProvider: EmojiProvider(userSettings: .mock()),
linkMetadataProvider: LinkMetadataProvider(),
timelineControllerFactory: TimelineControllerFactoryMock(.init()))
let context = PillContext(timelineContext: mock.context, data: PillTextAttachmentData(type: .event(room: .roomID("1")), font: .preferredFont(forTextStyle: .body)))
@@ -256,7 +256,7 @@ struct PillContextTests {
userIndicatorController: userIndicatorController,
appMediator: AppMediatorMock(.init()),
analyticsService: AnalyticsServiceMock(.init()),
emojiProvider: EmojiProvider(userSettings: .volatile()),
emojiProvider: EmojiProvider(userSettings: .mock()),
linkMetadataProvider: LinkMetadataProvider(),
timelineControllerFactory: TimelineControllerFactoryMock(.init()))
let context = PillContext(timelineContext: mock.context, data: PillTextAttachmentData(type: .event(room: .roomAlias("#foundation-and-empire:matrix.org")), font: .preferredFont(forTextStyle: .body)))
@@ -279,7 +279,7 @@ struct PillContextTests {
userIndicatorController: userIndicatorController,
appMediator: AppMediatorMock(.init()),
analyticsService: AnalyticsServiceMock(.init()),
emojiProvider: EmojiProvider(userSettings: .volatile()),
emojiProvider: EmojiProvider(userSettings: .mock()),
linkMetadataProvider: LinkMetadataProvider(),
timelineControllerFactory: TimelineControllerFactoryMock(.init()))
let context = PillContext(timelineContext: mock.context, data: PillTextAttachmentData(type: .event(room: .roomAlias("#foundation-and-empire:matrix.org")), font: .preferredFont(forTextStyle: .body)))
+1 -1
View File
@@ -21,7 +21,7 @@ struct PresenceServiceTests {
init() {
clientProxy = ClientProxyMock(.init())
userSettings = UserSettings.volatile()
userSettings = UserSettings.mock()
notificationCenter = NotificationCenter()
clientProxy.configurePresenceSendImmediatelyClosure = { [sendsSubject] presence, sendImmediately in
@@ -167,7 +167,7 @@ struct RoomDetailsEditScreenViewModelTests {
userIndicatorController = UserIndicatorControllerMock()
viewModel = .init(roomProxy: JoinedRoomProxyMock(roomProxyConfiguration),
userSession: UserSessionMock(.init()),
mediaUploadingPreprocessor: MediaUploadingPreprocessor(userSettings: .volatile()),
mediaUploadingPreprocessor: MediaUploadingPreprocessor(userSettings: .mock()),
userIndicatorController: userIndicatorController)
}
}
@@ -30,7 +30,7 @@ struct RoomDetailsScreenViewModelTests {
notificationSettingsProxyMock = NotificationSettingsProxyMock(with: NotificationSettingsProxyMockConfiguration())
viewModel = RoomDetailsScreenViewModel(roomProxy: roomProxyMock,
userSession: UserSessionMock(.init()),
userSettings: UserSettings.volatile(),
userSettings: UserSettings.mock(),
appHooks: AppHooks(),
analyticsService: AnalyticsServiceMock(.init()),
userIndicatorController: UserIndicatorControllerMock(),
@@ -44,7 +44,7 @@ struct RoomDetailsScreenViewModelTests {
roomProxyMock = JoinedRoomProxyMock(.init(name: "Test", members: mockedMembers, joinRule: .public))
viewModel = RoomDetailsScreenViewModel(roomProxy: roomProxyMock,
userSession: UserSessionMock(.init()),
userSettings: UserSettings.volatile(),
userSettings: UserSettings.mock(),
appHooks: AppHooks(),
analyticsService: AnalyticsServiceMock(.init()),
userIndicatorController: UserIndicatorControllerMock(),
@@ -65,7 +65,7 @@ struct RoomDetailsScreenViewModelTests {
roomProxyMock = JoinedRoomProxyMock(.init(name: "Test", members: mockedMembers))
viewModel = RoomDetailsScreenViewModel(roomProxy: roomProxyMock,
userSession: UserSessionMock(.init()),
userSettings: UserSettings.volatile(),
userSettings: UserSettings.mock(),
appHooks: AppHooks(),
analyticsService: AnalyticsServiceMock(.init()),
userIndicatorController: UserIndicatorControllerMock(),
@@ -87,7 +87,7 @@ struct RoomDetailsScreenViewModelTests {
roomProxyMock = JoinedRoomProxyMock(.init(name: "Test", members: mockedMembers))
viewModel = RoomDetailsScreenViewModel(roomProxy: roomProxyMock,
userSession: UserSessionMock(.init()),
userSettings: UserSettings.volatile(),
userSettings: UserSettings.mock(),
appHooks: AppHooks(),
analyticsService: AnalyticsServiceMock(.init()),
userIndicatorController: UserIndicatorControllerMock(),
@@ -146,7 +146,7 @@ struct RoomDetailsScreenViewModelTests {
roomProxyMock = JoinedRoomProxyMock(.init(name: "Test", isDirect: true, isEncrypted: true, members: mockedMembers))
viewModel = RoomDetailsScreenViewModel(roomProxy: roomProxyMock,
userSession: UserSessionMock(.init()),
userSettings: UserSettings.volatile(),
userSettings: UserSettings.mock(),
appHooks: AppHooks(),
analyticsService: AnalyticsServiceMock(.init()),
userIndicatorController: UserIndicatorControllerMock(),
@@ -168,7 +168,7 @@ struct RoomDetailsScreenViewModelTests {
roomProxyMock = JoinedRoomProxyMock(.init(name: "Test", isDirect: true, isEncrypted: true, members: mockedMembers))
viewModel = RoomDetailsScreenViewModel(roomProxy: roomProxyMock,
userSession: UserSessionMock(.init()),
userSettings: UserSettings.volatile(),
userSettings: UserSettings.mock(),
appHooks: AppHooks(),
analyticsService: AnalyticsServiceMock(.init()),
userIndicatorController: UserIndicatorControllerMock(),
@@ -201,7 +201,7 @@ struct RoomDetailsScreenViewModelTests {
roomProxyMock = JoinedRoomProxyMock(.init(name: "Test", isDirect: true, isEncrypted: true, members: mockedMembers))
viewModel = RoomDetailsScreenViewModel(roomProxy: roomProxyMock,
userSession: UserSessionMock(.init(clientProxy: clientProxy)),
userSettings: UserSettings.volatile(),
userSettings: UserSettings.mock(),
appHooks: AppHooks(),
analyticsService: AnalyticsServiceMock(.init()),
userIndicatorController: UserIndicatorControllerMock(),
@@ -233,7 +233,7 @@ struct RoomDetailsScreenViewModelTests {
roomProxyMock = JoinedRoomProxyMock(.init(name: "Test", isDirect: true, isEncrypted: true, members: mockedMembers))
viewModel = RoomDetailsScreenViewModel(roomProxy: roomProxyMock,
userSession: UserSessionMock(.init()),
userSettings: UserSettings.volatile(),
userSettings: UserSettings.mock(),
appHooks: AppHooks(),
analyticsService: AnalyticsServiceMock(.init()),
userIndicatorController: UserIndicatorControllerMock(),
@@ -266,7 +266,7 @@ struct RoomDetailsScreenViewModelTests {
roomProxyMock = JoinedRoomProxyMock(.init(name: "Test", isDirect: true, isEncrypted: true, members: mockedMembers))
viewModel = RoomDetailsScreenViewModel(roomProxy: roomProxyMock,
userSession: UserSessionMock(.init(clientProxy: clientProxy)),
userSettings: UserSettings.volatile(),
userSettings: UserSettings.mock(),
appHooks: AppHooks(),
analyticsService: AnalyticsServiceMock(.init()),
userIndicatorController: UserIndicatorControllerMock(),
@@ -300,7 +300,7 @@ struct RoomDetailsScreenViewModelTests {
powerLevelsConfiguration: .init(canUserInvite: false)))
viewModel = RoomDetailsScreenViewModel(roomProxy: roomProxyMock,
userSession: UserSessionMock(.init()),
userSettings: UserSettings.volatile(),
userSettings: UserSettings.mock(),
appHooks: AppHooks(),
analyticsService: AnalyticsServiceMock(.init()),
userIndicatorController: UserIndicatorControllerMock(),
@@ -318,7 +318,7 @@ struct RoomDetailsScreenViewModelTests {
roomProxyMock = JoinedRoomProxyMock(.init(name: "Test", members: mockedMembers, joinRule: .public))
viewModel = RoomDetailsScreenViewModel(roomProxy: roomProxyMock,
userSession: UserSessionMock(.init()),
userSettings: UserSettings.volatile(),
userSettings: UserSettings.mock(),
appHooks: AppHooks(),
analyticsService: AnalyticsServiceMock(.init()),
userIndicatorController: UserIndicatorControllerMock(),
@@ -367,7 +367,7 @@ struct RoomDetailsScreenViewModelTests {
viewModel = RoomDetailsScreenViewModel(roomProxy: roomProxyMock,
userSession: UserSessionMock(.init()),
userSettings: UserSettings.volatile(),
userSettings: UserSettings.mock(),
appHooks: AppHooks(),
analyticsService: AnalyticsServiceMock(.init()),
userIndicatorController: UserIndicatorControllerMock(),
@@ -403,7 +403,7 @@ struct RoomDetailsScreenViewModelTests {
viewModel = RoomDetailsScreenViewModel(roomProxy: roomProxyMock,
userSession: UserSessionMock(.init()),
userSettings: UserSettings.volatile(),
userSettings: UserSettings.mock(),
appHooks: AppHooks(),
analyticsService: AnalyticsServiceMock(.init()),
userIndicatorController: UserIndicatorControllerMock(),
@@ -439,7 +439,7 @@ struct RoomDetailsScreenViewModelTests {
viewModel = RoomDetailsScreenViewModel(roomProxy: roomProxyMock,
userSession: UserSessionMock(.init()),
userSettings: UserSettings.volatile(),
userSettings: UserSettings.mock(),
appHooks: AppHooks(),
analyticsService: AnalyticsServiceMock(.init()),
userIndicatorController: UserIndicatorControllerMock(),
@@ -460,7 +460,7 @@ struct RoomDetailsScreenViewModelTests {
roomProxyMock = JoinedRoomProxyMock(.init(name: "Test", isDirect: false, members: mockedMembers))
viewModel = RoomDetailsScreenViewModel(roomProxy: roomProxyMock,
userSession: UserSessionMock(.init()),
userSettings: UserSettings.volatile(),
userSettings: UserSettings.mock(),
appHooks: AppHooks(),
analyticsService: AnalyticsServiceMock(.init()),
userIndicatorController: UserIndicatorControllerMock(),
@@ -481,7 +481,7 @@ struct RoomDetailsScreenViewModelTests {
roomProxyMock = JoinedRoomProxyMock(.init(name: "Test", isDirect: true, members: mockedMembers))
viewModel = RoomDetailsScreenViewModel(roomProxy: roomProxyMock,
userSession: UserSessionMock(.init()),
userSettings: UserSettings.volatile(),
userSettings: UserSettings.mock(),
appHooks: AppHooks(),
analyticsService: AnalyticsServiceMock(.init()),
userIndicatorController: UserIndicatorControllerMock(),
@@ -500,7 +500,7 @@ struct RoomDetailsScreenViewModelTests {
notificationSettingsProxyMock.getNotificationSettingsRoomIdIsEncryptedIsOneToOneThrowableError = NotificationSettingsError.Generic(msg: "error")
viewModel = RoomDetailsScreenViewModel(roomProxy: roomProxyMock,
userSession: UserSessionMock(.init()),
userSettings: UserSettings.volatile(),
userSettings: UserSettings.mock(),
appHooks: AppHooks(),
analyticsService: AnalyticsServiceMock(.init()),
userIndicatorController: UserIndicatorControllerMock(),
@@ -709,7 +709,7 @@ struct RoomDetailsScreenViewModelTests {
roomProxyMock = JoinedRoomProxyMock(.init(name: "Test", isDirect: false, knockRequestsState: .loaded(mockedRequests), joinRule: .knock))
viewModel = RoomDetailsScreenViewModel(roomProxy: roomProxyMock,
userSession: UserSessionMock(.init()),
userSettings: UserSettings.volatile(),
userSettings: UserSettings.mock(),
appHooks: AppHooks(),
analyticsService: AnalyticsServiceMock(.init()),
userIndicatorController: UserIndicatorControllerMock(),
@@ -731,7 +731,7 @@ struct RoomDetailsScreenViewModelTests {
roomProxyMock = JoinedRoomProxyMock(.init(name: "Test", isDirect: false, knockRequestsState: .loading, joinRule: .knock))
viewModel = RoomDetailsScreenViewModel(roomProxy: roomProxyMock,
userSession: UserSessionMock(.init()),
userSettings: UserSettings.volatile(),
userSettings: UserSettings.mock(),
appHooks: AppHooks(),
analyticsService: AnalyticsServiceMock(.init()),
userIndicatorController: UserIndicatorControllerMock(),
@@ -755,7 +755,7 @@ struct RoomDetailsScreenViewModelTests {
powerLevelsConfiguration: .init(canUserInvite: false)))
viewModel = RoomDetailsScreenViewModel(roomProxy: roomProxyMock,
userSession: UserSessionMock(.init()),
userSettings: UserSettings.volatile(),
userSettings: UserSettings.mock(),
appHooks: AppHooks(),
analyticsService: AnalyticsServiceMock(.init()),
userIndicatorController: UserIndicatorControllerMock(),
@@ -779,7 +779,7 @@ struct RoomDetailsScreenViewModelTests {
roomProxyMock = JoinedRoomProxyMock(.init(name: "Test", isDirect: true, members: mockedMembers, knockRequestsState: .loaded(mockedRequests), joinRule: .knock))
viewModel = RoomDetailsScreenViewModel(roomProxy: roomProxyMock,
userSession: UserSessionMock(.init()),
userSettings: UserSettings.volatile(),
userSettings: UserSettings.mock(),
appHooks: AppHooks(),
analyticsService: AnalyticsServiceMock(.init()),
userIndicatorController: UserIndicatorControllerMock(),
@@ -807,7 +807,7 @@ struct RoomDetailsScreenViewModelTests {
viewModel = RoomDetailsScreenViewModel(roomProxy: roomProxyMock,
userSession: UserSessionMock(.init()),
userSettings: UserSettings.volatile(),
userSettings: UserSettings.mock(),
appHooks: AppHooks(),
analyticsService: AnalyticsServiceMock(.init()),
userIndicatorController: UserIndicatorControllerMock(),
@@ -22,7 +22,7 @@ final class RoomFlowCoordinatorTests {
private let userSettings: UserSettings
init() {
userSettings = UserSettings.volatile()
userSettings = UserSettings.mock()
}
@Test
@@ -17,7 +17,7 @@ final class RoomListFiltersStateTests {
let defaultFilters = RoomListFilter.allCases.filter { $0 != .mentions && $0 != .lowPriority }
init() {
userSettings = UserSettings.volatile()
userSettings = UserSettings.mock()
state = RoomListFiltersState(userSettings: userSettings)
}
@@ -51,7 +51,7 @@ struct RoomMembersFlowCoordinatorTests {
let clientProxy = ClientProxyMock(.init())
clientProxy.directRoomForUserIDReturnValue = .success(nil)
let userSettings = UserSettings.volatile()
let userSettings = UserSettings.mock()
let flowParameters = CommonFlowParameters(userSession: UserSessionMock(.init(clientProxy: clientProxy)),
bugReportService: BugReportServiceMock(.init()),
@@ -153,7 +153,7 @@ final class RoomScreenViewModelTests {
@Test
func pinnedEventsBannerThreadedSelection() async throws {
let userSettings = UserSettings.volatile()
let userSettings = UserSettings.mock()
userSettings.threadsEnabled = true
let userSession = UserSessionMock(.init(userSettings: userSettings))
@@ -122,7 +122,7 @@ final class RoomSummaryProviderTests {
// MARK: - Helpers
private func setup(isLowPriorityFilterEnabled: Bool = false, includesOnlyJoinedRooms: Bool = false, setsRoomList: Bool = true) {
userSettings = UserSettings.volatile()
userSettings = UserSettings.mock()
userSettings.lowPriorityFilterEnabled = isLowPriorityFilterEnabled
let stateEventStringBuilder = RoomStateEventStringBuilder(userID: "@me:matrix.org")
@@ -54,7 +54,7 @@ struct SearchScreenViewModelTests {
staticRoomListSubject.value.first { $0.id == identifier }
}
userSettings = UserSettings.volatile()
userSettings = UserSettings.mock()
viewModel = SearchScreenViewModel(roomSummaryProvider: RoomSummaryProviderMock(.init(state: .loaded(.mockRooms))),
clientProxy: clientProxy,
@@ -457,7 +457,7 @@ final class SecurityAndPrivacyScreenViewModelTests {
topLevelSpaces: [SpaceServiceRoom] = [],
joinRule: ElementX.JoinRule,
forceDisableE2EE: Bool = false) {
let userSettings = UserSettings.volatile()
let userSettings = UserSettings.mock()
userSettings.knockingEnabled = true
userSettings.forceDisableE2EE.applyRemoteValue(forceDisableE2EE)
roomProxy = JoinedRoomProxyMock(.init(isEncrypted: false,
@@ -21,7 +21,7 @@ struct SessionVerificationViewModelTests {
sessionVerificationController = SessionVerificationControllerProxyMock.configureMock()
viewModel = SessionVerificationScreenViewModel(sessionVerificationControllerProxy: sessionVerificationController,
flow: .deviceInitiator,
userSettings: .volatile(),
userSettings: .mock(),
mediaProvider: MediaProviderMock(.init()))
context = viewModel.context
}
@@ -176,7 +176,7 @@ struct SettingsScreenViewModelTests {
@Test
mutating func addAccountButtonFollowsTheFeatureFlag() async throws {
let userSettings = UserSettings.volatile()
let userSettings = UserSettings.mock()
setupViewModel(userSettings: userSettings)
#expect(!context.viewState.showAddAccountButton)
@@ -200,7 +200,7 @@ struct SettingsScreenViewModelTests {
// MARK: - Helpers
private mutating func setupViewModel(status: UserStatus = .init(), userSettings: UserSettings = .volatile()) {
private mutating func setupViewModel(status: UserStatus = .init(), userSettings: UserSettings = .mock()) {
clientProxy = ClientProxyMock(.init(userID: "", status: status))
viewModel = SettingsScreenViewModel(userSession: UserSessionMock(.init(clientProxy: clientProxy)),
userSettings: userSettings,
@@ -55,7 +55,7 @@ struct TimelineControllerTests {
attributedStringBuilder: AttributedStringBuilder(mentionBuilder: MentionBuilder()),
stateEventStringBuilder: RoomStateEventStringBuilder(userID: "@alice:matrix.org")),
mediaProvider: MediaProviderMock(.init()),
userSettings: .volatile())
userSettings: .mock())
}
/// The live timeline's items are only published once any initial focus attempt has finished.
@@ -218,7 +218,7 @@ struct TimelineInteractionHandlerTests {
userIndicatorController: UserIndicatorControllerMock(),
appMediator: AppMediatorMock(.init()),
analyticsService: AnalyticsServiceMock(.init()),
emojiProvider: EmojiProvider(userSettings: .volatile()),
emojiProvider: EmojiProvider(userSettings: .mock()),
linkMetadataProvider: LinkMetadataProvider(),
timelineControllerFactory: TimelineControllerFactoryMock(.init()))
}
@@ -123,7 +123,7 @@ struct TimelineItemMenuActionProviderTests {
isViewSourceEnabled: true,
areThreadsEnabled: true,
timelineKind: timelineKind,
emojiProvider: EmojiProvider(userSettings: .volatile()))
emojiProvider: EmojiProvider(userSettings: .mock()))
.makeActions()
}
}
+11 -11
View File
@@ -301,7 +301,7 @@ final class TimelineViewModelTests {
userIndicatorController: UserIndicatorControllerMock(),
appMediator: AppMediatorMock(.init()),
analyticsService: AnalyticsServiceMock(.init()),
emojiProvider: EmojiProvider(userSettings: .volatile()),
emojiProvider: EmojiProvider(userSettings: .mock()),
linkMetadataProvider: LinkMetadataProvider(),
timelineControllerFactory: TimelineControllerFactoryMock(.init()))
return (viewModel, roomProxy, timelineProxy, timelineController)
@@ -326,7 +326,7 @@ final class TimelineViewModelTests {
userIndicatorController: UserIndicatorControllerMock(),
appMediator: AppMediatorMock(.init()),
analyticsService: AnalyticsServiceMock(.init()),
emojiProvider: EmojiProvider(userSettings: .volatile()),
emojiProvider: EmojiProvider(userSettings: .mock()),
linkMetadataProvider: LinkMetadataProvider(),
timelineControllerFactory: TimelineControllerFactoryMock(.init()))
@@ -350,7 +350,7 @@ final class TimelineViewModelTests {
userIndicatorController: UserIndicatorControllerMock(),
appMediator: AppMediatorMock(.init()),
analyticsService: AnalyticsServiceMock(.init()),
emojiProvider: EmojiProvider(userSettings: .volatile()),
emojiProvider: EmojiProvider(userSettings: .mock()),
linkMetadataProvider: LinkMetadataProvider(),
timelineControllerFactory: TimelineControllerFactoryMock(.init()))
@@ -386,7 +386,7 @@ final class TimelineViewModelTests {
userIndicatorController: UserIndicatorControllerMock(),
appMediator: AppMediatorMock(.init()),
analyticsService: AnalyticsServiceMock(.init()),
emojiProvider: EmojiProvider(userSettings: .volatile()),
emojiProvider: EmojiProvider(userSettings: .mock()),
linkMetadataProvider: LinkMetadataProvider(),
timelineControllerFactory: TimelineControllerFactoryMock(.init()))
@@ -422,7 +422,7 @@ final class TimelineViewModelTests {
userIndicatorController: UserIndicatorControllerMock(),
appMediator: AppMediatorMock(.init()),
analyticsService: AnalyticsServiceMock(.init()),
emojiProvider: EmojiProvider(userSettings: .volatile()),
emojiProvider: EmojiProvider(userSettings: .mock()),
linkMetadataProvider: LinkMetadataProvider(),
timelineControllerFactory: TimelineControllerFactoryMock(.init()))
@@ -464,7 +464,7 @@ final class TimelineViewModelTests {
userIndicatorController: UserIndicatorControllerMock(),
appMediator: AppMediatorMock(.init()),
analyticsService: AnalyticsServiceMock(.init()),
emojiProvider: EmojiProvider(userSettings: .volatile()),
emojiProvider: EmojiProvider(userSettings: .mock()),
linkMetadataProvider: LinkMetadataProvider(),
timelineControllerFactory: TimelineControllerFactoryMock(.init()))
#expect(configuration.pinnedEventIDs == viewModel.context.viewState.pinnedEventIDs)
@@ -492,7 +492,7 @@ final class TimelineViewModelTests {
userIndicatorController: UserIndicatorControllerMock(),
appMediator: AppMediatorMock(.init()),
analyticsService: AnalyticsServiceMock(.init()),
emojiProvider: EmojiProvider(userSettings: .volatile()),
emojiProvider: EmojiProvider(userSettings: .mock()),
linkMetadataProvider: LinkMetadataProvider(),
timelineControllerFactory: TimelineControllerFactoryMock(.init()))
@@ -899,7 +899,7 @@ final class TimelineViewModelTests {
@Test
func disablingTheFlagClearsTheSelection() async throws {
let items = [TextRoomTimelineItem(eventID: "$1")]
let userSettings = UserSettings.volatile()
let userSettings = UserSettings.mock()
userSettings.messageMultiSelectEnabled = true
let viewModel = makeViewModel(timelineController: TimelineControllerMock(.init(timelineItems: items)), userSettings: userSettings)
@@ -917,7 +917,7 @@ final class TimelineViewModelTests {
focussedEventID: String? = nil,
timelineController: TimelineControllerProtocol,
userIndicatorController: UserIndicatorControllerProtocol = UserIndicatorControllerMock(),
userSettings: UserSettings = .volatile()) -> TimelineViewModel {
userSettings: UserSettings = .mock()) -> TimelineViewModel {
TimelineViewModel(roomProxy: roomProxy ?? JoinedRoomProxyMock(.init(name: "")),
focussedEventID: focussedEventID,
timelineController: timelineController,
@@ -926,7 +926,7 @@ final class TimelineViewModelTests {
userIndicatorController: userIndicatorController,
appMediator: AppMediatorMock(.init()),
analyticsService: AnalyticsServiceMock(.init()),
emojiProvider: EmojiProvider(userSettings: .volatile()),
emojiProvider: EmojiProvider(userSettings: .mock()),
linkMetadataProvider: LinkMetadataProvider(),
timelineControllerFactory: TimelineControllerFactoryMock(.init()))
}
@@ -944,7 +944,7 @@ final class TimelineViewModelTests {
private func makeSelectionViewModel(timelineController: TimelineControllerProtocol,
userIndicatorController: UserIndicatorControllerProtocol = UserIndicatorControllerMock()) -> TimelineViewModel {
let userSettings = UserSettings.volatile()
let userSettings = UserSettings.mock()
userSettings.messageMultiSelectEnabled = true
return makeViewModel(timelineController: timelineController,
userIndicatorController: userIndicatorController,
@@ -20,7 +20,7 @@ struct UserDetailsEditScreenViewModelTests {
init() {
viewModel = .init(userSession: UserSessionMock(.init()),
mediaUploadingPreprocessor: MediaUploadingPreprocessor(userSettings: .volatile()),
mediaUploadingPreprocessor: MediaUploadingPreprocessor(userSettings: .mock()),
userIndicatorController: UserIndicatorControllerMock())
}
@@ -48,7 +48,7 @@ struct UserSessionFlowCoordinatorTests {
appMediator.networkMonitor = networkMonitor
userIndicatorController = UserIndicatorControllerMock()
let userSettings = UserSettings.volatile()
let userSettings = UserSettings.mock()
let flowParameters = CommonFlowParameters(userSession: UserSessionMock(.init(clientProxy: clientProxy)),
bugReportService: BugReportServiceMock(.init()),
+102
View File
@@ -0,0 +1,102 @@
//
// 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 UserSettingsTests {
private let store = VolatileUserDefaults()
private let appSettings: AppSettings
private let aliceSettings: UserSettings
private let bobSettings: UserSettings
init() {
appSettings = AppSettings(store: store)
aliceSettings = appSettings.userSettings(for: "@alice:example.com")
bobSettings = appSettings.userSettings(for: "@bob:example.com")
}
@Test
func accountSettingsAreScopedToAccount() {
// Given settings for two different accounts.
#expect(aliceSettings.searchBreadcrumbs.isEmpty)
#expect(bobSettings.searchBreadcrumbs.isEmpty)
// When updating an account setting for Alice.
aliceSettings.searchBreadcrumbs = [.query("Alice")]
// Then only Alice should see the change.
#expect(aliceSettings.searchBreadcrumbs == [.query("Alice")])
#expect(bobSettings.searchBreadcrumbs.isEmpty)
}
@Test
func appSettingsAreShared() {
// Given settings for two different accounts.
#expect(!appSettings.hasSignedInBefore)
#expect(!aliceSettings.hasSignedInBefore)
#expect(!bobSettings.hasSignedInBefore)
// When updating an app setting through Alice's settings.
aliceSettings.hasSignedInBefore = true
// Then the change should be seen by the app and every account.
#expect(appSettings.hasSignedInBefore)
#expect(aliceSettings.hasSignedInBefore)
#expect(bobSettings.hasSignedInBefore)
}
@Test
func migrateAppSettingsValueToAccountSettings() throws {
// Given a store containing values saved under the keys previously used by the app settings.
// These keys are hardcoded so that renaming a property breaks this test instead of the migration.
store.set(true, forKey: "hasRunIdentityConfirmationOnboarding")
try store.set(JSONEncoder().encode([SearchBreadcrumb.query("Alice")]), forKey: "searchBreadcrumbs")
#expect(store.object(forKey: "hasRunIdentityConfirmationOnboarding") != nil)
#expect(store.object(forKey: "searchBreadcrumbs") != nil)
#expect(!aliceSettings.hasRunIdentityConfirmationOnboarding)
#expect(aliceSettings.searchBreadcrumbs.isEmpty)
// When migrating those values into Alice's account settings.
aliceSettings.migrateAppSettingsValueToAccountSettings(\.hasRunIdentityConfirmationOnboardingKey)
aliceSettings.migrateAppSettingsValueToAccountSettings(\.searchBreadcrumbsKey)
// Then the values should belong to Alice only.
#expect(aliceSettings.hasRunIdentityConfirmationOnboarding)
#expect(aliceSettings.searchBreadcrumbs == [.query("Alice")])
#expect(!bobSettings.hasRunIdentityConfirmationOnboarding)
#expect(bobSettings.searchBreadcrumbs.isEmpty)
#expect(store.object(forKey: "hasRunIdentityConfirmationOnboarding") == nil)
#expect(store.object(forKey: "searchBreadcrumbs") == nil)
}
@Test
func resetSessionSpecificSettings() {
// Given app settings and account settings for two different accounts.
appSettings.hasSignedInBefore = true
aliceSettings.hasRunIdentityConfirmationOnboarding = true
aliceSettings.searchBreadcrumbs = [.query("Alice")]
bobSettings.hasRunIdentityConfirmationOnboarding = true
bobSettings.searchBreadcrumbs = [.query("Bob")]
#expect(appSettings.hasSignedInBefore)
#expect(aliceSettings.hasRunIdentityConfirmationOnboarding)
#expect(aliceSettings.searchBreadcrumbs == [.query("Alice")])
#expect(bobSettings.hasRunIdentityConfirmationOnboarding)
#expect(bobSettings.searchBreadcrumbs == [.query("Bob")])
// When resetting Alice's session specific settings.
aliceSettings.resetSessionSpecificSettings()
// Then only Alice's account settings should be reset.
#expect(!aliceSettings.hasRunIdentityConfirmationOnboarding)
#expect(aliceSettings.searchBreadcrumbs.isEmpty)
#expect(bobSettings.hasRunIdentityConfirmationOnboarding)
#expect(bobSettings.searchBreadcrumbs == [.query("Bob")])
#expect(appSettings.hasSignedInBefore)
}
}