Files
plezy/test/utils/http_lifecycle_test.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;
}
}