Skip to content

Commit

Permalink
fake_api [nfc]: Add delay option to connection.prepare
Browse files Browse the repository at this point in the history
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.)
  • Loading branch information
gnprice committed Jun 28, 2024
1 parent fb01562 commit 4a906d5
Show file tree
Hide file tree
Showing 2 changed files with 54 additions and 7 deletions.
28 changes: 21 additions & 7 deletions test/api/fake_api.dart
Original file line number Diff line number Diff line change
Expand Up @@ -8,19 +8,22 @@ import 'package:zulip/model/store.dart';
import '../example_data.dart' as eg;

sealed class _PreparedResponse {
final Duration delay;

_PreparedResponse({this.delay = Duration.zero});
}

class _PreparedException extends _PreparedResponse {
final Object exception;

_PreparedException({required this.exception});
_PreparedException({super.delay, required this.exception});
}

class _PreparedSuccess extends _PreparedResponse {
final int httpStatus;
final List<int> bytes;

_PreparedSuccess({required this.httpStatus, required this.bytes});
_PreparedSuccess({super.delay, required this.httpStatus, required this.bytes});
}

/// An [http.Client] that accepts and replays canned responses, for testing.
Expand Down Expand Up @@ -53,12 +56,13 @@ class FakeHttpClient extends http.BaseClient {
int? httpStatus,
Map<String, dynamic>? json,
String? body,
Duration delay = Duration.zero,
}) {
assert(_nextResponse == null,
'FakeApiConnection.prepare was called while already expecting a request');
if (exception != null) {
assert(httpStatus == null && json == null && body == null);
_nextResponse = _PreparedException(exception: exception);
_nextResponse = _PreparedException(exception: exception, delay: delay);
} else {
assert((json == null) || (body == null));
final String resolvedBody = switch ((body, json)) {
Expand All @@ -69,6 +73,7 @@ class FakeHttpClient extends http.BaseClient {
_nextResponse = _PreparedSuccess(
httpStatus: httpStatus ?? 200,
bytes: utf8.encode(resolvedBody),
delay: delay,
);
}
}
Expand All @@ -89,14 +94,16 @@ class FakeHttpClient extends http.BaseClient {
final response = _nextResponse!;
_nextResponse = null;

final http.StreamedResponse Function() computation;
switch (response) {
case _PreparedException(:var exception):
return Future(() => throw exception);
computation = () => throw exception;
case _PreparedSuccess(:var bytes, :var httpStatus):
final byteStream = http.ByteStream.fromBytes(bytes);
return Future(() => http.StreamedResponse(
byteStream, httpStatus, request: request));
computation = () => http.StreamedResponse(
byteStream, httpStatus, request: request);
}
return Future.delayed(response.delay, computation);
}
}

Expand Down Expand Up @@ -203,13 +210,20 @@ class FakeApiConnection extends ApiConnection {
///
/// If `exception` is non-null, then `httpStatus`, `body`, and `json` must
/// all be null, and the next request will throw the given exception.
///
/// In either case, the next request will complete a duration of `delay`
/// after being started.
void prepare({
Object? exception,
int? httpStatus,
Map<String, dynamic>? json,
String? body,
Duration delay = Duration.zero,
}) {
client.prepare(
exception: exception, httpStatus: httpStatus, json: json, body: body);
exception: exception,
httpStatus: httpStatus, json: json, body: body,
delay: delay,
);
}
}
33 changes: 33 additions & 0 deletions test/api/fake_api_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import 'package:checks/checks.dart';
import 'package:test/scaffolding.dart';
import 'package:zulip/api/exception.dart';

import '../fake_async.dart';
import 'exception_checks.dart';
import 'fake_api.dart';

Expand All @@ -21,4 +22,36 @@ void main() {
..asString.contains('no response was prepared')
..asString.contains('FakeApiConnection.prepare'));
});

test('delay success', () => awaitFakeAsync((async) async {
final connection = FakeApiConnection();
connection.prepare(delay: const Duration(seconds: 2),
json: {'a': 3});

Map<String, dynamic>? result;
connection.get('aRoute', (json) => json, '/', null)
.then((r) { result = r; });

async.elapse(const Duration(seconds: 1));
check(result).isNull();

async.elapse(const Duration(seconds: 1));
check(result).isNotNull().deepEquals({'a': 3});
}));

test('delay exception', () => awaitFakeAsync((async) async {
final connection = FakeApiConnection();
connection.prepare(delay: const Duration(seconds: 2),
exception: Exception("oops"));

Object? error;
connection.get('aRoute', (json) => null, '/', null)
.catchError((Object e) { error = e; });

async.elapse(const Duration(seconds: 1));
check(error).isNull();

async.elapse(const Duration(seconds: 1));
check(error).isA<NetworkException>().asString.contains("oops");
}));
}

0 comments on commit 4a906d5

Please sign in to comment.