200 lines
6.3 KiB
Dart
200 lines
6.3 KiB
Dart
import 'dart:async';
|
|
|
|
import 'package:flutter_test/flutter_test.dart';
|
|
import 'package:http/http.dart' as http;
|
|
import 'package:plezy/utils/abortable_http_request.dart';
|
|
import 'package:plezy/utils/managed_http_client.dart';
|
|
|
|
void main() {
|
|
group('ManagedHttpClient', () {
|
|
test('preserves headers added by request finalization', () async {
|
|
final inner = _CapturingClient();
|
|
final client = ManagedHttpClient(inner, debugLabel: 'test');
|
|
addTearDown(() => client.closeGracefully(drainTimeout: Duration.zero));
|
|
|
|
final request = http.MultipartRequest('POST', Uri.parse('https://example.test/upload'))
|
|
..fields['title'] = 'Movie';
|
|
final response = await client.send(request);
|
|
await response.stream.drain<void>();
|
|
|
|
expect(inner.headers?['content-type'], startsWith('multipart/form-data; boundary='));
|
|
expect(inner.body, isNotEmpty);
|
|
});
|
|
|
|
test('closeGracefully aborts and cancels an active streamed response before closing inner client', () async {
|
|
final inner = _StreamingClient();
|
|
final client = ManagedHttpClient(inner, debugLabel: 'test');
|
|
addTearDown(() async {
|
|
await client.closeGracefully(drainTimeout: Duration.zero);
|
|
await inner.dispose();
|
|
});
|
|
|
|
final response = await client.send(http.Request('GET', Uri.parse('https://example.test/slow')));
|
|
|
|
await client.closeGracefully(drainTimeout: const Duration(milliseconds: 100));
|
|
|
|
expect(inner.closeCount, 1);
|
|
expect(inner.responseCancelled, isTrue);
|
|
await expectLater(inner.abortTrigger, completes);
|
|
await expectLater(response.stream.toList(), completion(isEmpty));
|
|
});
|
|
|
|
test('closeGracefully can retry after a drain timeout', () async {
|
|
final inner = _DeferredSendClient();
|
|
final client = ManagedHttpClient(inner, debugLabel: 'test');
|
|
addTearDown(() => client.closeGracefully(drainTimeout: Duration.zero));
|
|
|
|
final responseFuture = client.send(http.Request('GET', Uri.parse('https://example.test/slow')));
|
|
await Future<void>.delayed(Duration.zero);
|
|
|
|
await client.closeGracefully(drainTimeout: const Duration(milliseconds: 1));
|
|
expect(inner.closeCount, 0);
|
|
|
|
var retryCompleted = false;
|
|
final retryClose = client.closeGracefully(drainTimeout: const Duration(seconds: 1));
|
|
unawaited(retryClose.whenComplete(() => retryCompleted = true));
|
|
await Future<void>.delayed(const Duration(milliseconds: 10));
|
|
expect(retryCompleted, isFalse);
|
|
|
|
inner.completeWithEmptyResponse();
|
|
final response = await responseFuture;
|
|
await response.stream.drain<void>();
|
|
await retryClose;
|
|
|
|
expect(inner.closeCount, 1);
|
|
});
|
|
|
|
test('preserves final response URL metadata', () async {
|
|
final finalUrl = Uri.parse('https://example.test/final');
|
|
final inner = _UrlResponseClient(finalUrl);
|
|
final client = ManagedHttpClient(inner, debugLabel: 'test');
|
|
addTearDown(() => client.closeGracefully(drainTimeout: Duration.zero));
|
|
|
|
final response = await client.send(http.Request('GET', Uri.parse('https://example.test/start')));
|
|
|
|
expect(response, isA<http.BaseResponseWithUrl>().having((r) => r.url, 'url', finalUrl));
|
|
await response.stream.drain<void>();
|
|
});
|
|
});
|
|
|
|
group('sendAbortableHttpRequest', () {
|
|
test('aborts the underlying request when its timeout fires', () async {
|
|
final inner = _HangingClient();
|
|
addTearDown(inner.close);
|
|
|
|
await expectLater(
|
|
sendAbortableHttpRequest(
|
|
inner,
|
|
'GET',
|
|
Uri.parse('https://example.test/slow'),
|
|
timeout: const Duration(milliseconds: 1),
|
|
operation: 'slow request',
|
|
),
|
|
throwsA(isA<TimeoutException>().having((e) => e.message, 'message', 'slow request timed out')),
|
|
);
|
|
|
|
await expectLater(inner.abortTrigger, completes);
|
|
});
|
|
});
|
|
}
|
|
|
|
class _CapturingClient extends http.BaseClient {
|
|
Map<String, String>? headers;
|
|
List<int>? body;
|
|
|
|
@override
|
|
Future<http.StreamedResponse> send(http.BaseRequest request) async {
|
|
headers = Map.of(request.headers);
|
|
body = await request.finalize().toBytes();
|
|
return http.StreamedResponse(const Stream<List<int>>.empty(), 200, request: request);
|
|
}
|
|
}
|
|
|
|
class _StreamingClient extends http.BaseClient {
|
|
_StreamingClient() {
|
|
responseController = StreamController<List<int>>(
|
|
onCancel: () {
|
|
responseCancelled = true;
|
|
},
|
|
);
|
|
}
|
|
|
|
late final StreamController<List<int>> responseController;
|
|
Future<void>? abortTrigger;
|
|
var closeCount = 0;
|
|
var responseCancelled = false;
|
|
|
|
@override
|
|
Future<http.StreamedResponse> send(http.BaseRequest request) async {
|
|
abortTrigger = (request as http.Abortable).abortTrigger;
|
|
return http.StreamedResponse(responseController.stream, 200, request: request);
|
|
}
|
|
|
|
@override
|
|
void close() {
|
|
closeCount += 1;
|
|
}
|
|
|
|
Future<void> dispose() async {
|
|
await responseController.close();
|
|
}
|
|
}
|
|
|
|
class _DeferredSendClient extends http.BaseClient {
|
|
final _response = Completer<http.StreamedResponse>();
|
|
http.BaseRequest? request;
|
|
Future<void>? abortTrigger;
|
|
var closeCount = 0;
|
|
|
|
@override
|
|
Future<http.StreamedResponse> send(http.BaseRequest request) {
|
|
this.request = request;
|
|
abortTrigger = (request as http.Abortable).abortTrigger;
|
|
return _response.future;
|
|
}
|
|
|
|
void completeWithEmptyResponse() {
|
|
_response.complete(http.StreamedResponse(const Stream<List<int>>.empty(), 200, request: request));
|
|
}
|
|
|
|
@override
|
|
void close() {
|
|
closeCount += 1;
|
|
}
|
|
}
|
|
|
|
class _UrlResponseClient extends http.BaseClient {
|
|
_UrlResponseClient(this.url);
|
|
|
|
final Uri url;
|
|
|
|
@override
|
|
Future<http.StreamedResponse> send(http.BaseRequest request) async {
|
|
return _ResponseWithUrl(const Stream<List<int>>.empty(), 200, url: url, request: request);
|
|
}
|
|
}
|
|
|
|
class _ResponseWithUrl extends http.StreamedResponse implements http.BaseResponseWithUrl {
|
|
_ResponseWithUrl(super.stream, super.statusCode, {required this.url, super.request});
|
|
|
|
@override
|
|
final Uri url;
|
|
}
|
|
|
|
class _HangingClient extends http.BaseClient {
|
|
final _response = Completer<http.StreamedResponse>();
|
|
Future<void>? abortTrigger;
|
|
var closed = false;
|
|
|
|
@override
|
|
Future<http.StreamedResponse> send(http.BaseRequest request) {
|
|
abortTrigger = (request as http.Abortable).abortTrigger;
|
|
return _response.future;
|
|
}
|
|
|
|
@override
|
|
void close() {
|
|
closed = true;
|
|
}
|
|
}
|