Skip to content

Commit da56401

Browse files
committed
store test: Add and use UpdateMachineTestGlobalStore, for less mocking
1 parent 9a9d08b commit da56401

File tree

3 files changed

+132
-45
lines changed

3 files changed

+132
-45
lines changed

test/api/fake_api.dart

+5
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

+65-44
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
});
@@ -965,44 +983,42 @@ void main() {
965983
});
966984

967985
group('UpdateMachine.poll reload failure', () {
968-
late LoadingTestGlobalStore globalStore;
969-
970-
List<Completer<PerAccountStore>> completers() =>
971-
globalStore.completers[eg.selfAccount.id]!;
972-
973-
Future<void> prepareReload(FakeAsync async) async {
974-
globalStore = LoadingTestGlobalStore(accounts: [eg.selfAccount]);
975-
976-
// Simulate the setup that [TestGlobalStore.doLoadPerAccount] would do.
977-
// (These tests use [LoadingTestGlobalStore] for greater control in
978-
// later steps; that requires this setup step to be finer-grained too.)
979-
final updateMachine = eg.updateMachine(
980-
globalStore: globalStore, account: eg.selfAccount);
981-
final store = updateMachine.store;
982-
final future = globalStore.perAccount(eg.selfAccount.id);
983-
completers().single.complete(store);
984-
await future;
985-
completers().clear();
986+
late UpdateMachineTestGlobalStore globalStore;
986987

987-
updateMachine.debugPauseLoop();
988-
updateMachine.poll();
989-
(store.connection as FakeApiConnection).prepare(
988+
Future<void> prepareReload(FakeAsync async, {
989+
required void Function(FakeApiConnection) prepareRegisterQueueResponse,
990+
}) async {
991+
globalStore = UpdateMachineTestGlobalStore(accounts: [eg.selfAccount]);
992+
993+
final store = await globalStore.perAccount(eg.selfAccount.id);
994+
final updateMachine = store.updateMachine!;
995+
996+
final connection = store.connection as FakeApiConnection;
997+
connection.prepare(
990998
apiException: eg.apiExceptionBadEventQueueId());
999+
globalStore.prepareRegisterQueueResponse = (newConnection) {
1000+
prepareRegisterQueueResponse(newConnection);
1001+
};
1002+
// When we reload, we should get a new connection,
1003+
// just like when the app runs live. This is more realistic,
1004+
// and we don't want a glitch where we try to double-close a connection
1005+
// just because of the test infrastructure. (One of the tests
1006+
// logs out the account, and the connection shouldn't be used after that.)
1007+
globalStore.clearCachedApiConnections();
9911008
updateMachine.debugAdvanceLoop();
992-
async.elapse(Duration.zero);
1009+
async.elapse(Duration.zero); // the bad-event-queue error arrives
9931010
check(store).isLoading.isTrue();
9941011
}
9951012

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

1000-
// [PerAccountStore.fromInitialSnapshot] requires the account
1001-
// to be in the global store when called; do so before logging out.
1002-
final newStore = eg.store(globalStore: globalStore, account: eg.selfAccount);
10031021
await logOutAccount(globalStore, eg.selfAccount.id);
1004-
completers().single.complete(newStore);
1005-
check(completers()).single.isCompleted.isTrue();
10061022
check(globalStore.takeDoRemoveAccountCalls()).single.equals(eg.selfAccount.id);
10071023

10081024
async.elapse(TestGlobalStore.removeAccountDuration);
@@ -1011,15 +1027,20 @@ void main() {
10111027
async.flushTimers();
10121028
// Reload never succeeds and there are no unhandled errors.
10131029
check(globalStore.perAccountSync(eg.selfAccount.id)).isNull();
1014-
}));
1030+
}),
1031+
// An unhandled error is actually the bug #1354, so skip for now
1032+
// TODO(#1354) unskip
1033+
skip: true);
10151034

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

1020-
completers().single.completeError(eg.apiExceptionUnauthorized());
1021-
async.elapse(Duration.zero);
1022-
check(completers()).single.isCompleted.isTrue();
1043+
async.elapse(const Duration(seconds: 1));
10231044
check(globalStore.takeDoRemoveAccountCalls()).single.equals(eg.selfAccount.id);
10241045

10251046
async.elapse(TestGlobalStore.removeAccountDuration);

test/model/test_store.dart

+62-1
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';
@@ -121,7 +124,10 @@ mixin _DatabaseMixin on GlobalStore {
121124
/// Tests can use [PerAccountStore.updateMachine] in order to invoke that logic
122125
/// explicitly when desired.
123126
///
124-
/// See also [TestZulipBinding.globalStore], which provides one of these.
127+
/// See also:
128+
/// * [TestZulipBinding.globalStore], which provides one of these.
129+
/// * [UpdateMachineTestGlobalStore], which prepares per-account data
130+
/// using [UpdateMachine.load] (like [LiveGlobalStore] does).
125131
class TestGlobalStore extends GlobalStore with _ApiConnectionsMixin, _DatabaseMixin {
126132
TestGlobalStore({required super.accounts});
127133

@@ -167,6 +173,61 @@ class TestGlobalStore extends GlobalStore with _ApiConnectionsMixin, _DatabaseMi
167173
}
168174
}
169175

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

0 commit comments

Comments
 (0)