Skip to content

Commit 4a906d5

Browse files
committed
fake_api [nfc]: Add delay option to connection.prepare
This allows simulating a request that takes some time to return and consequently races with something else. For example here: #713 (comment) This is NFC because `Future.delayed(Duration.zero, f)` is exactly equivalent to `Future(f)`. (The docs aren't real clear on this; but reading the implementations confirms they boil down to the very same `Timer` constructor call, and then the docs do seem to be trying to say that when read in light of that information.)
1 parent fb01562 commit 4a906d5

File tree

2 files changed

+54
-7
lines changed

2 files changed

+54
-7
lines changed

test/api/fake_api.dart

Lines changed: 21 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,19 +8,22 @@ import 'package:zulip/model/store.dart';
88
import '../example_data.dart' as eg;
99

1010
sealed class _PreparedResponse {
11+
final Duration delay;
12+
13+
_PreparedResponse({this.delay = Duration.zero});
1114
}
1215

1316
class _PreparedException extends _PreparedResponse {
1417
final Object exception;
1518

16-
_PreparedException({required this.exception});
19+
_PreparedException({super.delay, required this.exception});
1720
}
1821

1922
class _PreparedSuccess extends _PreparedResponse {
2023
final int httpStatus;
2124
final List<int> bytes;
2225

23-
_PreparedSuccess({required this.httpStatus, required this.bytes});
26+
_PreparedSuccess({super.delay, required this.httpStatus, required this.bytes});
2427
}
2528

2629
/// An [http.Client] that accepts and replays canned responses, for testing.
@@ -53,12 +56,13 @@ class FakeHttpClient extends http.BaseClient {
5356
int? httpStatus,
5457
Map<String, dynamic>? json,
5558
String? body,
59+
Duration delay = Duration.zero,
5660
}) {
5761
assert(_nextResponse == null,
5862
'FakeApiConnection.prepare was called while already expecting a request');
5963
if (exception != null) {
6064
assert(httpStatus == null && json == null && body == null);
61-
_nextResponse = _PreparedException(exception: exception);
65+
_nextResponse = _PreparedException(exception: exception, delay: delay);
6266
} else {
6367
assert((json == null) || (body == null));
6468
final String resolvedBody = switch ((body, json)) {
@@ -69,6 +73,7 @@ class FakeHttpClient extends http.BaseClient {
6973
_nextResponse = _PreparedSuccess(
7074
httpStatus: httpStatus ?? 200,
7175
bytes: utf8.encode(resolvedBody),
76+
delay: delay,
7277
);
7378
}
7479
}
@@ -89,14 +94,16 @@ class FakeHttpClient extends http.BaseClient {
8994
final response = _nextResponse!;
9095
_nextResponse = null;
9196

97+
final http.StreamedResponse Function() computation;
9298
switch (response) {
9399
case _PreparedException(:var exception):
94-
return Future(() => throw exception);
100+
computation = () => throw exception;
95101
case _PreparedSuccess(:var bytes, :var httpStatus):
96102
final byteStream = http.ByteStream.fromBytes(bytes);
97-
return Future(() => http.StreamedResponse(
98-
byteStream, httpStatus, request: request));
103+
computation = () => http.StreamedResponse(
104+
byteStream, httpStatus, request: request);
99105
}
106+
return Future.delayed(response.delay, computation);
100107
}
101108
}
102109

@@ -203,13 +210,20 @@ class FakeApiConnection extends ApiConnection {
203210
///
204211
/// If `exception` is non-null, then `httpStatus`, `body`, and `json` must
205212
/// all be null, and the next request will throw the given exception.
213+
///
214+
/// In either case, the next request will complete a duration of `delay`
215+
/// after being started.
206216
void prepare({
207217
Object? exception,
208218
int? httpStatus,
209219
Map<String, dynamic>? json,
210220
String? body,
221+
Duration delay = Duration.zero,
211222
}) {
212223
client.prepare(
213-
exception: exception, httpStatus: httpStatus, json: json, body: body);
224+
exception: exception,
225+
httpStatus: httpStatus, json: json, body: body,
226+
delay: delay,
227+
);
214228
}
215229
}

test/api/fake_api_test.dart

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import 'package:checks/checks.dart';
22
import 'package:test/scaffolding.dart';
33
import 'package:zulip/api/exception.dart';
44

5+
import '../fake_async.dart';
56
import 'exception_checks.dart';
67
import 'fake_api.dart';
78

@@ -21,4 +22,36 @@ void main() {
2122
..asString.contains('no response was prepared')
2223
..asString.contains('FakeApiConnection.prepare'));
2324
});
25+
26+
test('delay success', () => awaitFakeAsync((async) async {
27+
final connection = FakeApiConnection();
28+
connection.prepare(delay: const Duration(seconds: 2),
29+
json: {'a': 3});
30+
31+
Map<String, dynamic>? result;
32+
connection.get('aRoute', (json) => json, '/', null)
33+
.then((r) { result = r; });
34+
35+
async.elapse(const Duration(seconds: 1));
36+
check(result).isNull();
37+
38+
async.elapse(const Duration(seconds: 1));
39+
check(result).isNotNull().deepEquals({'a': 3});
40+
}));
41+
42+
test('delay exception', () => awaitFakeAsync((async) async {
43+
final connection = FakeApiConnection();
44+
connection.prepare(delay: const Duration(seconds: 2),
45+
exception: Exception("oops"));
46+
47+
Object? error;
48+
connection.get('aRoute', (json) => null, '/', null)
49+
.catchError((Object e) { error = e; });
50+
51+
async.elapse(const Duration(seconds: 1));
52+
check(error).isNull();
53+
54+
async.elapse(const Duration(seconds: 1));
55+
check(error).isA<NetworkException>().asString.contains("oops");
56+
}));
2457
}

0 commit comments

Comments
 (0)