cyberhybridhub/server/test/helpers/mock_http_client.dart
2026-05-31 11:17:12 -05:00

176 lines
5.1 KiB
Dart

import 'dart:convert';
import 'package:http/http.dart' as http;
/// Records requests and returns canned responses for Alpaca client unit tests.
class MockHttpClient extends http.BaseClient {
MockHttpClient({Map<String, _MatchedResponse>? responses})
: _responses = responses ?? <String, _MatchedResponse>{};
final Map<String, _MatchedResponse> _responses;
final List<_QueuedGet> _getQueue = <_QueuedGet>[];
final List<http.BaseRequest> requests = <http.BaseRequest>[];
/// Captured request bodies indexed by request order in [requests].
final List<String> capturedBodies = <String>[];
void whenGet(String pathSuffix, http.Response response) {
_responses[pathSuffix] = _MatchedResponse(method: 'GET', response: response);
}
void whenGetJson(String pathSuffix, Map<String, dynamic> body,
{int statusCode = 200}) {
whenGet(
pathSuffix,
http.Response(jsonEncode(body), statusCode, headers: <String, String>{
'content-type': 'application/json',
}),
);
}
/// Returns [response] for the next GET whose path ends with [pathSuffix].
///
/// Useful for paginated endpoints where the same path is hit multiple
/// times with different `page_token` query values.
void whenGetQueued(String pathSuffix, http.Response response) {
_getQueue.add(_QueuedGet(pathSuffix, response));
}
void whenGetQueuedJson(String pathSuffix, Map<String, dynamic> body,
{int statusCode = 200}) {
whenGetQueued(
pathSuffix,
http.Response(jsonEncode(body), statusCode, headers: <String, String>{
'content-type': 'application/json',
}),
);
}
/// Returns [response] when [predicate] matches the request URI.
void whenGetWhere(
String pathSuffix,
bool Function(Uri uri) predicate,
http.Response response,
) {
_responses['GET:$pathSuffix:${_responses.length}'] = _MatchedResponse(
method: 'GET',
response: response,
pathSuffix: pathSuffix,
uriPredicate: predicate,
);
}
void whenGetWhereJson(
String pathSuffix,
bool Function(Uri uri) predicate,
Map<String, dynamic> body, {
int statusCode = 200,
}) {
whenGetWhere(
pathSuffix,
predicate,
http.Response(jsonEncode(body), statusCode, headers: <String, String>{
'content-type': 'application/json',
}),
);
}
void whenPost(String pathSuffix, http.Response response) {
_responses['POST:$pathSuffix'] =
_MatchedResponse(method: 'POST', response: response);
}
void whenPostJson(String pathSuffix, Map<String, dynamic> body,
{int statusCode = 200}) {
whenPost(
pathSuffix,
http.Response(jsonEncode(body), statusCode, headers: <String, String>{
'content-type': 'application/json',
}),
);
}
@override
Future<http.StreamedResponse> send(http.BaseRequest request) async {
requests.add(request);
final String body = await _readBody(request);
capturedBodies.add(body);
final String path = request.url.path;
final String method = request.method.toUpperCase();
if (method == 'GET' && _getQueue.isNotEmpty) {
final int idx = _getQueue.indexWhere(
(_QueuedGet q) => path.endsWith(q.pathSuffix),
);
if (idx >= 0) {
final _QueuedGet queued = _getQueue.removeAt(idx);
return http.StreamedResponse(
Stream<List<int>>.value(utf8.encode(queued.response.body)),
queued.response.statusCode,
headers: queued.response.headers,
request: request,
);
}
}
for (final MapEntry<String, _MatchedResponse> entry in _responses.entries) {
final _MatchedResponse match = entry.value;
if (match.method != method) continue;
final String suffix = match.pathSuffix ??
(entry.key.startsWith('$method:')
? entry.key.substring(method.length + 1).split(':').first
: entry.key);
final bool pathOk =
path.endsWith(suffix) || request.url.toString().contains(suffix);
if (!pathOk) continue;
if (match.uriPredicate != null && !match.uriPredicate!(request.url)) {
continue;
}
return http.StreamedResponse(
Stream<List<int>>.value(utf8.encode(match.response.body)),
match.response.statusCode,
headers: match.response.headers,
request: request,
);
}
return http.StreamedResponse(
Stream<List<int>>.value(utf8.encode('{}')),
404,
request: request,
);
}
Future<String> _readBody(http.BaseRequest request) async {
if (request is http.Request) {
return request.body;
}
final List<int> bytes = await request.finalize().toBytes();
return utf8.decode(bytes);
}
@override
void close() {}
}
class _QueuedGet {
_QueuedGet(this.pathSuffix, this.response);
final String pathSuffix;
final http.Response response;
}
class _MatchedResponse {
_MatchedResponse({
required this.method,
required this.response,
this.pathSuffix,
this.uriPredicate,
});
final String method;
final http.Response response;
final String? pathSuffix;
final bool Function(Uri uri)? uriPredicate;
}