Skip to content

Commit ad7d628

Browse files
committed
store test: Add and use UpdateMachineTestGlobalStore, for less mocking
1 parent 1c7a2f8 commit ad7d628

File tree

3 files changed

+126
-38
lines changed

3 files changed

+126
-38
lines changed

test/api/fake_api.dart

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import 'dart:collection';
22
import 'dart:convert';
33

4+
import 'package:checks/checks.dart';
45
import 'package:flutter/foundation.dart';
56
import 'package:http/http.dart' as http;
67
import 'package:zulip/api/core.dart';
@@ -278,3 +279,7 @@ class FakeApiConnection extends ApiConnection {
278279
);
279280
}
280281
}
282+
283+
extension FakeApiConnectionChecks on Subject<FakeApiConnection> {
284+
Subject<bool> get isOpen => has((x) => x.isOpen, 'isOpen');
285+
}

test/model/store_test.dart

Lines changed: 61 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -131,17 +131,25 @@ void main() {
131131
}));
132132

133133
test('GlobalStore.perAccount account is logged out while loading; then fails with HTTP status code 401', () => awaitFakeAsync((async) async {
134-
final globalStore = LoadingTestGlobalStore(accounts: [eg.selfAccount]);
134+
final globalStore = UpdateMachineTestGlobalStore(accounts: [eg.selfAccount]);
135+
globalStore.prepareRegisterQueueResponse = (connection) =>
136+
connection.prepare(
137+
delay: TestGlobalStore.removeAccountDuration + Duration(seconds: 1),
138+
apiException: eg.apiExceptionUnauthorized());
139+
final connection = globalStore.apiConnectionFromAccount(eg.selfAccount) as FakeApiConnection;
135140
final future = globalStore.perAccount(eg.selfAccount.id);
141+
check(connection.takeRequests()).length.equals(1); // register request
136142

137143
await logOutAccount(globalStore, eg.selfAccount.id);
138144
check(globalStore.takeDoRemoveAccountCalls())
139145
.single.equals(eg.selfAccount.id);
140146

141-
globalStore.completers[eg.selfAccount.id]!
142-
.single.completeError(eg.apiExceptionUnauthorized());
143147
await check(future).throws<AccountNotFoundException>();
144148
check(globalStore.takeDoRemoveAccountCalls()).isEmpty();
149+
// no poll, server-emoji-data, or register-token requests
150+
check(connection.takeRequests()).isEmpty();
151+
// TODO(#1354) uncomment
152+
// check(connection).isOpen.isFalse();
145153
}));
146154

147155
// TODO test insertAccount
@@ -239,11 +247,20 @@ void main() {
239247
});
240248

241249
test('when store loading', () async {
242-
final globalStore = LoadingTestGlobalStore(accounts: [eg.selfAccount]);
250+
final globalStore = UpdateMachineTestGlobalStore(accounts: [eg.selfAccount]);
243251
checkGlobalStore(globalStore, eg.selfAccount.id,
244252
expectAccount: true, expectStore: false);
245253

246-
// don't await; we'll complete/await it manually after removeAccount
254+
// assert(globalStore.useCachedApiConnections);
255+
// Cache a connection and get this reference to it,
256+
// so we can check later that it gets closed.
257+
// final connection = globalStore.apiConnectionFromAccount(eg.selfAccount) as FakeApiConnection;
258+
259+
globalStore.prepareRegisterQueueResponse = (connection) {
260+
connection.prepare(
261+
delay: TestGlobalStore.removeAccountDuration + Duration(seconds: 1),
262+
json: eg.initialSnapshot().toJson());
263+
};
247264
final loadingFuture = globalStore.perAccount(eg.selfAccount.id);
248265

249266
checkGlobalStore(globalStore, eg.selfAccount.id,
@@ -257,13 +274,14 @@ void main() {
257274
expectAccount: false, expectStore: false);
258275
check(notifyCount).equals(1);
259276

260-
globalStore.completers[eg.selfAccount.id]!.single
261-
.complete(eg.store(account: eg.selfAccount, initialSnapshot: eg.initialSnapshot()));
262-
// TODO test that the never-used store got disposed and its connection closed
263-
await check(loadingFuture).throws<AccountNotFoundException>();
277+
// Actually throws a null-check error; that's the bug #1354.
278+
// TODO(#1354) should specifically throw AccountNotFoundException
279+
await check(loadingFuture).throws();
264280
checkGlobalStore(globalStore, eg.selfAccount.id,
265281
expectAccount: false, expectStore: false);
266282
check(notifyCount).equals(1); // no extra notify
283+
// TODO(#1354) uncomment
284+
// check(connection).isOpen.isFalse();
267285

268286
check(globalStore.debugNumPerAccountStoresLoading).equals(0);
269287
});
@@ -966,41 +984,42 @@ void main() {
966984
});
967985

968986
group('UpdateMachine.poll reload failure', () {
969-
late LoadingTestGlobalStore globalStore;
987+
late UpdateMachineTestGlobalStore globalStore;
970988

971-
List<Completer<PerAccountStore>> completers() =>
972-
globalStore.completers[eg.selfAccount.id]!;
989+
Future<void> prepareReload(FakeAsync async, {
990+
required void Function(FakeApiConnection) prepareRegisterQueueResponse,
991+
}) async {
992+
globalStore = UpdateMachineTestGlobalStore(accounts: [eg.selfAccount]);
973993

974-
Future<void> prepareReload(FakeAsync async) async {
975-
globalStore = LoadingTestGlobalStore(accounts: [eg.selfAccount]);
976-
final future = globalStore.perAccount(eg.selfAccount.id);
977-
final store = eg.store(globalStore: globalStore, account: eg.selfAccount);
978-
completers().single.complete(store);
979-
await future;
980-
completers().clear();
981-
final updateMachine = globalStore.updateMachines[eg.selfAccount.id] =
982-
UpdateMachine.fromInitialSnapshot(
983-
store: store, initialSnapshot: eg.initialSnapshot());
984-
updateMachine.debugPauseLoop();
994+
final store = await globalStore.perAccount(eg.selfAccount.id);
995+
final updateMachine = store.updateMachine!;
985996
updateMachine.poll();
986997

987-
(store.connection as FakeApiConnection).prepare(
998+
final connection = store.connection as FakeApiConnection;
999+
connection.prepare(
9881000
apiException: eg.apiExceptionBadEventQueueId());
1001+
globalStore.clearCachedApiConnections();
1002+
globalStore.prepareRegisterQueueResponse = (newConnection) {
1003+
// Assert this is actually a new connection, not the one on
1004+
// globalStore._perAccountStores, which at least one test will close
1005+
// via logOutAccount.
1006+
assert(!identical(connection, newConnection));
1007+
prepareRegisterQueueResponse(newConnection);
1008+
};
9891009
updateMachine.debugAdvanceLoop();
9901010
async.elapse(Duration.zero);
9911011
check(store).isLoading.isTrue();
9921012
}
9931013

9941014
test('user logged out before new store is loaded', () => awaitFakeAsync((async) async {
995-
await prepareReload(async);
996-
check(completers()).single.isCompleted.isFalse();
1015+
await prepareReload(async,
1016+
prepareRegisterQueueResponse: (newConnection) {
1017+
newConnection.prepare(
1018+
delay: TestGlobalStore.removeAccountDuration + Duration(seconds: 1),
1019+
json: eg.initialSnapshot().toJson());
1020+
});
9971021

998-
// [PerAccountStore.fromInitialSnapshot] requires the account
999-
// to be in the global store when called; do so before logging out.
1000-
final newStore = eg.store(globalStore: globalStore, account: eg.selfAccount);
10011022
await logOutAccount(globalStore, eg.selfAccount.id);
1002-
completers().single.complete(newStore);
1003-
check(completers()).single.isCompleted.isTrue();
10041023
check(globalStore.takeDoRemoveAccountCalls()).single.equals(eg.selfAccount.id);
10051024

10061025
async.elapse(TestGlobalStore.removeAccountDuration);
@@ -1009,15 +1028,20 @@ void main() {
10091028
async.flushTimers();
10101029
// Reload never succeeds and there are no unhandled errors.
10111030
check(globalStore.perAccountSync(eg.selfAccount.id)).isNull();
1012-
}));
1031+
}),
1032+
// An unhandled error is actually the bug #1354, so skip for now
1033+
// TODO(#1354) unskip
1034+
skip: true);
10131035

10141036
test('new store is not loaded, gets HTTP 401 error instead', () => awaitFakeAsync((async) async {
1015-
await prepareReload(async);
1016-
check(completers()).single.isCompleted.isFalse();
1037+
await prepareReload(async,
1038+
prepareRegisterQueueResponse: (newConnection) {
1039+
newConnection.prepare(
1040+
delay: Duration(seconds: 1),
1041+
apiException: eg.apiExceptionUnauthorized());
1042+
});
10171043

1018-
completers().single.completeError(eg.apiExceptionUnauthorized());
1019-
async.elapse(Duration.zero);
1020-
check(completers()).single.isCompleted.isTrue();
1044+
async.elapse(const Duration(seconds: 1));
10211045
check(globalStore.takeDoRemoveAccountCalls()).single.equals(eg.selfAccount.id);
10221046

10231047
async.elapse(TestGlobalStore.removeAccountDuration);

test/model/test_store.dart

Lines changed: 60 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
import 'package:zulip/api/model/events.dart';
22
import 'package:zulip/api/model/initial_snapshot.dart';
33
import 'package:zulip/api/model/model.dart';
4+
import 'package:zulip/api/route/events.dart';
5+
import 'package:zulip/api/route/realm.dart';
46
import 'package:zulip/model/store.dart';
7+
import 'package:zulip/notifications/receive.dart';
58
import 'package:zulip/widgets/store.dart';
69

710
import '../api/fake_api.dart';
@@ -122,7 +125,10 @@ mixin _DatabaseMixin on GlobalStore {
122125
/// in [updateMachines], which tests can use for invoking that logic
123126
/// explicitly when desired.
124127
///
125-
/// See also [TestZulipBinding.globalStore], which provides one of these.
128+
/// See also:
129+
/// * [TestZulipBinding.globalStore], which provides one of these.
130+
/// * [UpdateMachineTestGlobalStore], which prepares per-account data
131+
/// using [UpdateMachine.load] (like [LiveGlobalStore] does)..
126132
class TestGlobalStore extends GlobalStore with _ApiConnectionsMixin, _DatabaseMixin {
127133
TestGlobalStore({required super.accounts});
128134

@@ -171,6 +177,59 @@ class TestGlobalStore extends GlobalStore with _ApiConnectionsMixin, _DatabaseMi
171177
}
172178
}
173179

180+
/// A [GlobalStore] that causes no database queries,
181+
/// and loads per-account data from API responses prepared by callers.
182+
///
183+
/// The per-account stores will use [FakeApiConnection].
184+
///
185+
/// Like [LiveGlobalStore] and unlike [TestGlobalStore],
186+
/// account data is loaded via [UpdateMachine.load].
187+
/// Callers can set [prepareRegisterQueueResponse]
188+
/// to prepare a register-queue payload or an exception.
189+
/// The implementation pauses the event-polling loop
190+
/// to avoid being a nuisance and does a boring
191+
/// [FakeApiConnection.prepare] for the register-token request.
192+
///
193+
/// See also:
194+
/// * [TestGlobalStore], which prepares per-account data
195+
/// without using [UpdateMachine.load].
196+
class UpdateMachineTestGlobalStore extends GlobalStore with _ApiConnectionsMixin, _DatabaseMixin {
197+
UpdateMachineTestGlobalStore({required super.accounts});
198+
199+
@override bool get useCachedApiConnections => true;
200+
@override set useCachedApiConnections(bool value) =>
201+
throw UnsupportedError(
202+
'Setting UpdateMachineTestGlobalStore.useCachedApiConnections '
203+
'is not supported.');
204+
205+
void Function(FakeApiConnection)? prepareRegisterQueueResponse;
206+
207+
void _prepareRegisterQueueSuccess(FakeApiConnection connection) {
208+
connection.prepare(json: eg.initialSnapshot().toJson());
209+
}
210+
211+
@override
212+
Future<PerAccountStore> doLoadPerAccount(int accountId) async {
213+
final account = getAccount(accountId);
214+
215+
// UpdateMachine.load should pick up the connection
216+
// with the network-request responses that we've prepared.
217+
assert(useCachedApiConnections);
218+
219+
final connection = apiConnectionFromAccount(account!) as FakeApiConnection;
220+
(prepareRegisterQueueResponse ?? _prepareRegisterQueueSuccess)(connection);
221+
connection
222+
..prepare(json: GetEventsResult(events: [HeartbeatEvent(id: 2)], queueId: null).toJson())
223+
..prepare(json: ServerEmojiData(codeToNames: {}).toJson());
224+
if (NotificationService.instance.token.value != null) {
225+
connection.prepare(json: {}); // register-token
226+
}
227+
final updateMachine = await UpdateMachine.load(this, accountId);
228+
updateMachine.debugPauseLoop();
229+
return updateMachine.store;
230+
}
231+
}
232+
174233
extension PerAccountStoreTestExtension on PerAccountStore {
175234
Future<void> addUser(User user) async {
176235
await handleEvent(RealmUserAddEvent(id: 1, person: user));

0 commit comments

Comments
 (0)