From a87d74288ed67ff8928f908bea6ed1733c37ac67 Mon Sep 17 00:00:00 2001 From: Greg Price Date: Thu, 14 Aug 2025 19:55:20 -0700 Subject: [PATCH 1/8] test: Include eg.selfUser by default in eg.initialSnapshot Over the next few commits, we'll ensure that all tests' data put the self-user in the users list of the initial snapshot. This will help by making that happen automatically for the majority of tests. --- test/example_data.dart | 2 +- test/model/autocomplete_test.dart | 2 +- test/widgets/new_dm_sheet_test.dart | 5 +++-- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/test/example_data.dart b/test/example_data.dart index 9ad6e8ca13..bb5aa800b7 100644 --- a/test/example_data.dart +++ b/test/example_data.dart @@ -1276,7 +1276,7 @@ InitialSnapshot initialSnapshot({ serverEmojiDataUrl: serverEmojiDataUrl ?? realmUrl.replace(path: '/static/emoji.json'), realmEmptyTopicDisplayName: realmEmptyTopicDisplayName ?? defaultRealmEmptyTopicDisplayName, - realmUsers: realmUsers ?? [], + realmUsers: realmUsers ?? [selfUser], realmNonActiveUsers: realmNonActiveUsers ?? [], crossRealmBots: crossRealmBots ?? [], ); diff --git a/test/model/autocomplete_test.dart b/test/model/autocomplete_test.dart index 92e5a81d9a..ee9d7da6b0 100644 --- a/test/model/autocomplete_test.dart +++ b/test/model/autocomplete_test.dart @@ -446,8 +446,8 @@ void main() { List messages = const [], }) async { store = eg.store(initialSnapshot: eg.initialSnapshot( + realmUsers: users, recentPrivateConversations: dmConversations)); - await store.addUsers(users); await store.addUserGroups(userGroups); await store.addMessages(messages); } diff --git a/test/widgets/new_dm_sheet_test.dart b/test/widgets/new_dm_sheet_test.dart index a95d42dc1a..7b956831fa 100644 --- a/test/widgets/new_dm_sheet_test.dart +++ b/test/widgets/new_dm_sheet_test.dart @@ -34,9 +34,10 @@ Future setupSheet(WidgetTester tester, { final testNavObserver = TestNavigatorObserver() ..onPushed = (route, _) => lastPushedRoute = route; - await testBinding.globalStore.add(eg.selfAccount, eg.initialSnapshot()); + await testBinding.globalStore.add(eg.selfAccount, eg.initialSnapshot( + realmUsers: users, + )); store = await testBinding.globalStore.perAccount(eg.selfAccount.id); - await store.addUsers(users); if (mutedUserIds != null) { await store.setMutedUsers(mutedUserIds); } From 5c24a08680fced7728435f2b2407b42d62242ca1 Mon Sep 17 00:00:00 2001 From: Greg Price Date: Thu, 14 Aug 2025 20:22:19 -0700 Subject: [PATCH 2/8] test: Take a selfUser parameter in eg.store --- test/example_data.dart | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/test/example_data.dart b/test/example_data.dart index bb5aa800b7..3e4da34b96 100644 --- a/test/example_data.dart +++ b/test/example_data.dart @@ -313,6 +313,7 @@ Account account({ ackedPushToken: ackedPushToken, ); } +const _account = account; /// A [User] which throws on attempting to mutate any of its fields. /// @@ -1285,10 +1286,13 @@ const _initialSnapshot = initialSnapshot; PerAccountStore store({ GlobalStore? globalStore, + User? selfUser, Account? account, InitialSnapshot? initialSnapshot, }) { - final effectiveAccount = account ?? selfAccount; + assert(!(account != null && selfUser != null)); + final effectiveAccount = account + ?? (selfUser != null ? _account(user: selfUser) : selfAccount); return PerAccountStore.fromInitialSnapshot( globalStore: globalStore ?? _globalStore(accounts: [effectiveAccount]), accountId: effectiveAccount.id, From a183be2ac4f796c5a3b76f418af6abde40393f95 Mon Sep 17 00:00:00 2001 From: Greg Price Date: Mon, 18 Aug 2025 17:53:20 -0700 Subject: [PATCH 3/8] new-dms test: Include self-user in users list --- test/widgets/new_dm_sheet_test.dart | 27 +++++++++++++++------------ 1 file changed, 15 insertions(+), 12 deletions(-) diff --git a/test/widgets/new_dm_sheet_test.dart b/test/widgets/new_dm_sheet_test.dart index 7b956831fa..a11e862b48 100644 --- a/test/widgets/new_dm_sheet_test.dart +++ b/test/widgets/new_dm_sheet_test.dart @@ -25,6 +25,7 @@ import 'test_app.dart'; late PerAccountStore store; Future setupSheet(WidgetTester tester, { + User? selfUser, required List users, List? mutedUserIds, }) async { @@ -34,17 +35,18 @@ Future setupSheet(WidgetTester tester, { final testNavObserver = TestNavigatorObserver() ..onPushed = (route, _) => lastPushedRoute = route; - await testBinding.globalStore.add(eg.selfAccount, eg.initialSnapshot( - realmUsers: users, - )); - store = await testBinding.globalStore.perAccount(eg.selfAccount.id); + selfUser ??= eg.selfUser; + final account = eg.account(user: selfUser); + await testBinding.globalStore.add(account, eg.initialSnapshot( + realmUsers: [selfUser, ...users])); + store = await testBinding.globalStore.perAccount(account.id); if (mutedUserIds != null) { await store.setMutedUsers(mutedUserIds); } await tester.pumpWidget(TestZulipApp( navigatorObservers: [testNavObserver], - accountId: eg.selfAccount.id, + accountId: account.id, child: const HomePage())); await tester.pumpAndSettle(); @@ -124,7 +126,7 @@ void main() { ]; testWidgets('shows full list initially', (tester) async { - await setupSheet(tester, users: testUsers); + await setupSheet(tester, selfUser: testUsers[0], users: testUsers); check(findText(includePlaceholders: false, 'Alice Anderson')).findsOne(); check(findText(includePlaceholders: false, 'Bob Brown')).findsOne(); check(findText(includePlaceholders: false, 'Charlie Carter')).findsOne(); @@ -144,7 +146,8 @@ void main() { testWidgets('deactivated users excluded', (tester) async { // Omit a deactivated user both before there's a query… final deactivatedUser = eg.user(fullName: 'Impostor Charlie', isActive: false); - await setupSheet(tester, users: [...testUsers, deactivatedUser]); + await setupSheet(tester, selfUser: testUsers[0], + users: [...testUsers, deactivatedUser]); check(findText(includePlaceholders: false, 'Impostor Charlie')).findsNothing(); check(findText(includePlaceholders: false, 'Charlie Carter')).findsOne(); check(find.byIcon(ZulipIcons.check_circle_unchecked)).findsExactly(3); @@ -160,7 +163,7 @@ void main() { testWidgets('muted users excluded', (tester) async { // Omit muted users both before there's a query… final mutedUser = eg.user(fullName: 'Someone Muted'); - await setupSheet(tester, + await setupSheet(tester, selfUser: testUsers[0], users: [...testUsers, mutedUser], mutedUserIds: [mutedUser.userId]); check(findText(includePlaceholders: false, 'Someone Muted')).findsNothing(); check(findText(includePlaceholders: false, 'Muted user')).findsNothing(); @@ -255,7 +258,7 @@ void main() { } testWidgets('tapping user chip deselects the user', (tester) async { - await setupSheet(tester, users: [eg.selfUser, eg.otherUser, eg.thirdUser]); + await setupSheet(tester, users: [eg.otherUser, eg.thirdUser]); await tester.tap(findUserTile(eg.otherUser)); await tester.pump(); @@ -267,7 +270,7 @@ void main() { testWidgets('selecting and deselecting a user', (tester) async { final user = eg.user(fullName: 'Test User'); - await setupSheet(tester, users: [eg.selfUser, user]); + await setupSheet(tester, users: [user]); checkUserSelected(tester, user, false); checkUserSelected(tester, eg.selfUser, false); @@ -286,7 +289,7 @@ void main() { testWidgets('other user selection deselects self user', (tester) async { final otherUser = eg.user(fullName: 'Other User'); - await setupSheet(tester, users: [eg.selfUser, otherUser]); + await setupSheet(tester, users: [otherUser]); await tester.tap(findUserTile(eg.selfUser)); await tester.pump(); @@ -301,7 +304,7 @@ void main() { testWidgets('other user selection hides self user', (tester) async { final otherUser = eg.user(fullName: 'Other User'); - await setupSheet(tester, users: [eg.selfUser, otherUser]); + await setupSheet(tester, users: [otherUser]); check(findText(includePlaceholders: false, eg.selfUser.fullName)).findsOne(); From c6c9238cb488046580acd0bf293530765c291d62 Mon Sep 17 00:00:00 2001 From: Greg Price Date: Thu, 14 Aug 2025 20:16:49 -0700 Subject: [PATCH 4/8] autocomplete test: Ensure self-user in users list --- test/model/autocomplete_test.dart | 23 +++++++++++++++-------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/test/model/autocomplete_test.dart b/test/model/autocomplete_test.dart index ee9d7da6b0..20f8adb6be 100644 --- a/test/model/autocomplete_test.dart +++ b/test/model/autocomplete_test.dart @@ -440,12 +440,17 @@ void main() { late PerAccountStore store; Future prepare({ + User? selfUser, List users = const [], List userGroups = const [], List dmConversations = const [], List messages = const [], }) async { - store = eg.store(initialSnapshot: eg.initialSnapshot( + selfUser ??= eg.selfUser; + if (!users.contains(selfUser)) { + users = [...users, selfUser]; + } + store = eg.store(selfUser: selfUser, initialSnapshot: eg.initialSnapshot( realmUsers: users, recentPrivateConversations: dmConversations)); await store.addUserGroups(userGroups); @@ -817,6 +822,7 @@ void main() { eg.user(userId: 6, fullName: 'User Six', isBot: true), eg.user(userId: 7, fullName: 'User Seven'), ]; + final selfUser = users.last; final userGroups = [ eg.userGroup(id: 1, name: 'User Group One'), @@ -825,13 +831,14 @@ void main() { eg.userGroup(id: 4, name: 'User Group Four'), ]; - await prepare(users: users, userGroups: userGroups, messages: [ - eg.streamMessage(sender: users[1-1], stream: stream, topic: topic), - eg.streamMessage(sender: users[5-1], stream: stream, topic: 'other $topic'), - eg.dmMessage(from: users[1-1], to: [users[2-1], eg.selfUser]), - eg.dmMessage(from: users[1-1], to: [eg.selfUser]), - eg.dmMessage(from: users[4-1], to: [eg.selfUser]), - ]); + await prepare(users: users, selfUser: selfUser, userGroups: userGroups, + messages: [ + eg.streamMessage(sender: users[1-1], stream: stream, topic: topic), + eg.streamMessage(sender: users[5-1], stream: stream, topic: 'other $topic'), + eg.dmMessage(from: users[1-1], to: [users[2-1], selfUser]), + eg.dmMessage(from: users[1-1], to: [selfUser]), + eg.dmMessage(from: users[4-1], to: [selfUser]), + ]); // Check the ranking of the full list of mentions, // i.e. the results for an empty query. From f5ad25f0d9549606763544db47733b9e2f80ae8a Mon Sep 17 00:00:00 2001 From: Greg Price Date: Thu, 14 Aug 2025 20:43:16 -0700 Subject: [PATCH 5/8] test: Include self-user in users lists This completes the sweep through the tests to ensure the self-user is always found in the list of users in the initial snapshot, like it would be in a real-life initial snapshot from a Zulip server. Upcoming commits will start checking that expectation in the model code. --- test/model/store_test.dart | 8 ++++---- test/model/user_test.dart | 2 +- test/notifications/open_test.dart | 14 ++++++++++---- test/widgets/action_sheet_test.dart | 1 + test/widgets/app_test.dart | 3 ++- test/widgets/home_test.dart | 6 ++++-- test/widgets/recent_dm_conversations_test.dart | 7 ++----- test/widgets/store_test.dart | 15 ++++++++++----- 8 files changed, 34 insertions(+), 22 deletions(-) diff --git a/test/model/store_test.dart b/test/model/store_test.dart index 8697647cf6..d3c3e9c4e4 100644 --- a/test/model/store_test.dart +++ b/test/model/store_test.dart @@ -47,7 +47,7 @@ void main() { final store1 = PerAccountStore.fromInitialSnapshot( globalStore: globalStore, accountId: 1, - initialSnapshot: eg.initialSnapshot(), + initialSnapshot: eg.initialSnapshot(realmUsers: [eg.selfUser]), ); completers(1).single.complete(store1); check(await future1).identicalTo(store1); @@ -58,7 +58,7 @@ void main() { final store2 = PerAccountStore.fromInitialSnapshot( globalStore: globalStore, accountId: 2, - initialSnapshot: eg.initialSnapshot(), + initialSnapshot: eg.initialSnapshot(realmUsers: [eg.otherUser]), ); completers(2).single.complete(store2); check(await future2).identicalTo(store2); @@ -85,12 +85,12 @@ void main() { final store1 = PerAccountStore.fromInitialSnapshot( globalStore: globalStore, accountId: 1, - initialSnapshot: eg.initialSnapshot(), + initialSnapshot: eg.initialSnapshot(realmUsers: [eg.selfUser]), ); final store2 = PerAccountStore.fromInitialSnapshot( globalStore: globalStore, accountId: 2, - initialSnapshot: eg.initialSnapshot(), + initialSnapshot: eg.initialSnapshot(realmUsers: [eg.otherUser]), ); completers(1).single.complete(store1); completers(2).single.complete(store2); diff --git a/test/model/user_test.dart b/test/model/user_test.dart index 3638e3c214..6d5e5c2519 100644 --- a/test/model/user_test.dart +++ b/test/model/user_test.dart @@ -201,7 +201,7 @@ void main() { final user3 = eg.user(userId: 3); store = eg.store(initialSnapshot: eg.initialSnapshot( - realmUsers: [user1, user2, user3], + realmUsers: [user1, user2, user3, eg.selfUser], mutedUsers: [MutedUserItem(id: 2), MutedUserItem(id: 1)])); check(store.isUserMuted(1)).isTrue(); check(store.isUserMuted(2)).isTrue(); diff --git a/test/notifications/open_test.dart b/test/notifications/open_test.dart index cdfd8ef361..2db54079c3 100644 --- a/test/notifications/open_test.dart +++ b/test/notifications/open_test.dart @@ -251,9 +251,14 @@ void main() { eg.account(id: 1003, realmUrl: realmUrlB, user: user1), eg.account(id: 1004, realmUrl: realmUrlB, user: user2), ]; - for (final account in accounts) { - await testBinding.globalStore.add(account, eg.initialSnapshot()); - } + await testBinding.globalStore.add( + accounts[0], eg.initialSnapshot(realmUsers: [user1])); + await testBinding.globalStore.add( + accounts[1], eg.initialSnapshot(realmUsers: [user2])); + await testBinding.globalStore.add( + accounts[2], eg.initialSnapshot(realmUsers: [user1])); + await testBinding.globalStore.add( + accounts[3], eg.initialSnapshot(realmUsers: [user2])); await prepare(tester); await checkOpenNotification(tester, accounts[0], eg.streamMessage()); @@ -306,7 +311,8 @@ void main() { final accountB = eg.otherAccount; final message = eg.streamMessage(); await testBinding.globalStore.add(accountA, eg.initialSnapshot()); - await testBinding.globalStore.add(accountB, eg.initialSnapshot()); + await testBinding.globalStore.add(accountB, eg.initialSnapshot( + realmUsers: [eg.otherUser])); setupNotificationDataForLaunch(tester, accountB, message); await prepare(tester, early: true); diff --git a/test/widgets/action_sheet_test.dart b/test/widgets/action_sheet_test.dart index 33c02dbcbd..83574786a7 100644 --- a/test/widgets/action_sheet_test.dart +++ b/test/widgets/action_sheet_test.dart @@ -77,6 +77,7 @@ Future setupToMessageActionSheet(WidgetTester tester, { await testBinding.globalStore.add( selfAccount, eg.initialSnapshot( + realmUsers: [selfUser], realmAllowMessageEditing: realmAllowMessageEditing, realmMessageContentEditLimitSeconds: realmMessageContentEditLimitSeconds, realmEnableReadReceipts: realmEnableReadReceipts, diff --git a/test/widgets/app_test.dart b/test/widgets/app_test.dart index eca2f95cab..022cd52ed3 100644 --- a/test/widgets/app_test.dart +++ b/test/widgets/app_test.dart @@ -281,7 +281,8 @@ void main() { testWidgets('choosing an account clears the navigator stack', (tester) async { addTearDown(testBinding.reset); await testBinding.globalStore.add(eg.selfAccount, eg.initialSnapshot()); - await testBinding.globalStore.add(eg.otherAccount, eg.initialSnapshot()); + await testBinding.globalStore.add(eg.otherAccount, eg.initialSnapshot( + realmUsers: [eg.otherUser])); final pushedRoutes = >[]; final poppedRoutes = >[]; diff --git a/test/widgets/home_test.dart b/test/widgets/home_test.dart index 5a8d3cca33..5422a8973b 100644 --- a/test/widgets/home_test.dart +++ b/test/widgets/home_test.dart @@ -333,7 +333,8 @@ void main () { pushedRoutes = []; lastPoppedRoute = null; await testBinding.globalStore.add(eg.selfAccount, eg.initialSnapshot()); - await testBinding.globalStore.add(eg.otherAccount, eg.initialSnapshot()); + await testBinding.globalStore.add(eg.otherAccount, eg.initialSnapshot( + realmUsers: [eg.otherUser])); await tester.pumpWidget(ZulipApp(navigatorObservers: [testNavObserver])); await tester.pump(Duration.zero); // wait for the loading page checkOnLoadingPage(); @@ -445,7 +446,8 @@ void main () { testWidgets('while loading, go to nested levels of ChooseAccountPage', (tester) async { testBinding.globalStore.loadPerAccountDuration = loadPerAccountDuration; final thirdAccount = eg.account(user: eg.thirdUser); - await testBinding.globalStore.add(thirdAccount, eg.initialSnapshot()); + await testBinding.globalStore.add(thirdAccount, eg.initialSnapshot( + realmUsers: [eg.thirdUser])); await prepare(tester); await tester.pump(kTryAnotherAccountWaitPeriod); diff --git a/test/widgets/recent_dm_conversations_test.dart b/test/widgets/recent_dm_conversations_test.dart index a947420dad..32c1d3f28d 100644 --- a/test/widgets/recent_dm_conversations_test.dart +++ b/test/widgets/recent_dm_conversations_test.dart @@ -38,13 +38,10 @@ Future setupPage(WidgetTester tester, { selfUser ??= eg.selfUser; final selfAccount = eg.account(user: selfUser); - await testBinding.globalStore.add(selfAccount, eg.initialSnapshot()); + await testBinding.globalStore.add(selfAccount, eg.initialSnapshot( + realmUsers: [selfUser, ...users])); store = await testBinding.globalStore.perAccount(selfAccount.id); - await store.addUser(selfUser); - for (final user in users) { - await store.addUser(user); - } if (mutedUserIds != null) { await store.setMutedUsers(mutedUserIds); } diff --git a/test/widgets/store_test.dart b/test/widgets/store_test.dart index e2c9be2d18..fe3ade165d 100644 --- a/test/widgets/store_test.dart +++ b/test/widgets/store_test.dart @@ -281,10 +281,14 @@ void main() { addTearDown(testBinding.reset); - final account1 = eg.account(id: 1, user: eg.user()); - final account2 = eg.account(id: 2, user: eg.user()); - await testBinding.globalStore.add(account1, eg.initialSnapshot()); - await testBinding.globalStore.add(account2, eg.initialSnapshot()); + final user1 = eg.user(); + final user2 = eg.user(); + final account1 = eg.account(id: 1, user: user1); + final account2 = eg.account(id: 2, user: user2); + await testBinding.globalStore.add(account1, eg.initialSnapshot( + realmUsers: [user1])); + await testBinding.globalStore.add(account2, eg.initialSnapshot( + realmUsers: [user2])); final testNavObserver = TestNavigatorObserver(); await tester.pumpWidget(ZulipApp(navigatorObservers: [testNavObserver])); @@ -374,7 +378,8 @@ void main() { // production code, where we could reasonably add an assert against it. // If forced, we could let this test code proceed despite such an assert…) // hack; the snapshot probably corresponds to selfAccount, not otherAccount. - await testBinding.globalStore.add(eg.otherAccount, eg.initialSnapshot()); + await testBinding.globalStore.add(eg.otherAccount, eg.initialSnapshot( + realmUsers: [eg.otherUser])); await pumpWithParams(light: false, accountId: eg.otherAccount.id); // Nudge PerAccountStoreWidget to send its updated store to MyWidgetWithMixin. // From 2ac9bbcb8399a996b7ceb979504508e6159cabfb Mon Sep 17 00:00:00 2001 From: Greg Price Date: Thu, 14 Aug 2025 20:48:59 -0700 Subject: [PATCH 6/8] user [nfc]: Expose UserStoreImpl.userMapFromInitialSnapshot We'll need to pass RealmStoreImpl some details of the self-user, in order to correctly interpret (for #814) the permissions that live on RealmStore. But because UserStore already depends on RealmStore, we don't want to introduce a dependency in RealmStoreImpl on UserStore as a whole. So separate out the step that processes the user lists, and do that step in advance before constructing RealmStoreImpl. --- lib/model/store.dart | 4 +++- lib/model/user.dart | 19 ++++++++++++++----- 2 files changed, 17 insertions(+), 6 deletions(-) diff --git a/lib/model/store.dart b/lib/model/store.dart index 070c20fb27..be370a9fd8 100644 --- a/lib/model/store.dart +++ b/lib/model/store.dart @@ -482,7 +482,9 @@ class PerAccountStore extends PerAccountStoreBase with selfUserId: account.userId, ); final realm = RealmStoreImpl(core: core, initialSnapshot: initialSnapshot); - final users = UserStoreImpl(realm: realm, initialSnapshot: initialSnapshot); + final userMap = UserStoreImpl.userMapFromInitialSnapshot(initialSnapshot); + final users = UserStoreImpl(realm: realm, initialSnapshot: initialSnapshot, + userMap: userMap); final channels = ChannelStoreImpl(users: users, initialSnapshot: initialSnapshot); return PerAccountStore._( diff --git a/lib/model/user.dart b/lib/model/user.dart index da6b828014..7d4188786b 100644 --- a/lib/model/user.dart +++ b/lib/model/user.dart @@ -196,18 +196,27 @@ abstract class HasUserStore extends HasRealmStore with UserStore, ProxyUserStore /// itself. Other code accesses this functionality through [PerAccountStore], /// or through the mixin [UserStore] which describes its interface. class UserStoreImpl extends HasRealmStore with UserStore { + /// Construct an implementation of [UserStore] that does the work itself. + /// + /// The `userMap` parameter should be the result of + /// [UserStoreImpl.userMapFromInitialSnapshot] applied to `initialSnapshot`. UserStoreImpl({ required super.realm, required InitialSnapshot initialSnapshot, - }) : _users = Map.fromEntries( - initialSnapshot.realmUsers - .followedBy(initialSnapshot.realmNonActiveUsers) - .followedBy(initialSnapshot.crossRealmBots) - .map((user) => MapEntry(user.userId, user))), + required Map userMap, + }) : _users = userMap, _mutedUsers = Set.from(initialSnapshot.mutedUsers.map((item) => item.id)), _userStatuses = initialSnapshot.userStatuses.map((userId, change) => MapEntry(userId, change.apply(UserStatus.zero))); + static Map userMapFromInitialSnapshot(InitialSnapshot initialSnapshot) { + return Map.fromEntries( + initialSnapshot.realmUsers + .followedBy(initialSnapshot.realmNonActiveUsers) + .followedBy(initialSnapshot.crossRealmBots) + .map((user) => MapEntry(user.userId, user))); + } + final Map _users; @override From ee7f1870b59fb6bc168c0a4d611720a9e683cd0a Mon Sep 17 00:00:00 2001 From: Greg Price Date: Thu, 14 Aug 2025 20:52:19 -0700 Subject: [PATCH 7/8] store: Extract self-user up front; require to be present We'll want to pass this to RealmStoreImpl soon, in order to interpret permissions correctly. In the series of commits leading up to here, we've already adapted all the tests' data to satisfy this invariant. --- lib/model/store.dart | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/lib/model/store.dart b/lib/model/store.dart index be370a9fd8..e74a22c623 100644 --- a/lib/model/store.dart +++ b/lib/model/store.dart @@ -481,8 +481,22 @@ class PerAccountStore extends PerAccountStoreBase with accountId: accountId, selfUserId: account.userId, ); - final realm = RealmStoreImpl(core: core, initialSnapshot: initialSnapshot); + final userMap = UserStoreImpl.userMapFromInitialSnapshot(initialSnapshot); + final selfUser = userMap[core.selfUserId]; + if (selfUser == null) { + final zulipLocalizations = GlobalLocalizations.zulipLocalizations; + reportErrorToUserModally( + zulipLocalizations.errorCouldNotConnectTitle, + message: zulipLocalizations.errorMalformedResponseWithCause(200, + // skip-i18n: This would be an unlikely bug (in the server?). We're + // showing the user these details at all only because it would be a + // very nasty bug (so, important to resolve ASAP) if it ever did happen. + 'self-user missing from user list')); + throw Exception("bad initial snapshot: self-user missing from user list"); + } + + final realm = RealmStoreImpl(core: core, initialSnapshot: initialSnapshot); final users = UserStoreImpl(realm: realm, initialSnapshot: initialSnapshot, userMap: userMap); final channels = ChannelStoreImpl(users: users, From bd9ace3cd13fdee361c7aa1f4ac6581e6704216b Mon Sep 17 00:00:00 2001 From: Greg Price Date: Thu, 14 Aug 2025 20:16:33 -0700 Subject: [PATCH 8/8] user [nfc]: Assert up front that self-user is present This is already guaranteed by PerAccountStore.fromInitialSnapshot, the only caller of this constructor. --- lib/model/user.dart | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/lib/model/user.dart b/lib/model/user.dart index 7d4188786b..4983cf48ca 100644 --- a/lib/model/user.dart +++ b/lib/model/user.dart @@ -207,7 +207,10 @@ class UserStoreImpl extends HasRealmStore with UserStore { }) : _users = userMap, _mutedUsers = Set.from(initialSnapshot.mutedUsers.map((item) => item.id)), _userStatuses = initialSnapshot.userStatuses.map((userId, change) => - MapEntry(userId, change.apply(UserStatus.zero))); + MapEntry(userId, change.apply(UserStatus.zero))) { + // Verify that [selfUser] will work. + assert(_users.containsKey(selfUserId)); + } static Map userMapFromInitialSnapshot(InitialSnapshot initialSnapshot) { return Map.fromEntries(