platform/packages/test/lib/src/matchers.dart

332 lines
9.3 KiB
Dart
Raw Normal View History

2018-10-21 08:22:41 +00:00
import 'dart:convert';
2017-03-25 15:26:00 +00:00
import 'dart:io';
2016-12-10 17:05:45 +00:00
import 'package:http/http.dart' as http;
2021-05-15 08:35:27 +00:00
import 'package:angel3_http_exception/angel3_http_exception.dart';
import 'package:angel3_validate/angel3_validate.dart';
2016-12-10 17:05:45 +00:00
2017-03-25 15:26:00 +00:00
/// Expects a response to be a JSON representation of an `AngelHttpException`.
///
/// You can optionally check for a matching [message], [statusCode] and [errors].
Matcher isAngelHttpException(
2021-04-26 01:05:34 +00:00
{String? message,
int? statusCode,
2021-05-18 11:43:58 +00:00
Iterable<String> errors = const []}) =>
2021-05-15 08:35:27 +00:00
_IsAngelHttpException(
2017-03-25 15:26:00 +00:00
message: message, statusCode: statusCode, errors: errors);
2016-12-10 17:05:45 +00:00
/// Expects a given response, when parsed as JSON,
2016-12-10 18:11:27 +00:00
/// to equal a desired value.
2021-05-15 08:35:27 +00:00
Matcher isJson(value) => _IsJson(value);
2016-12-10 18:11:27 +00:00
2017-03-25 15:26:00 +00:00
/// Expects a response to have the given content type, whether a `String` or [ContentType].
2021-05-15 08:35:27 +00:00
Matcher hasContentType(contentType) => _HasContentType(contentType);
2017-03-25 15:26:00 +00:00
/// Expects a response to have the given body.
///
/// If `true` is passed as the value (default), then this matcher will simply assert
/// that the response has a non-empty body.
///
/// If value is a `List<int>`, then it will be matched against `res.bodyBytes`.
/// Otherwise, the string value will be matched against `res.body`.
2021-05-15 08:35:27 +00:00
Matcher hasBody([value]) => _HasBody(value ?? true);
2017-03-25 15:26:00 +00:00
/// Expects a response to have a header named [key] which contains [value]. [value] can be a `String`, or a List of `String`s.
///
/// If `value` is true (default), then this matcher will simply assert that the header is present.
2021-05-15 08:35:27 +00:00
Matcher hasHeader(String key, [value]) => _HasHeader(key, value ?? true);
2017-03-25 15:26:00 +00:00
2016-12-10 18:11:27 +00:00
/// Expects a response to have the given status code.
2021-05-15 08:35:27 +00:00
Matcher hasStatus(int status) => _HasStatus(status);
2016-12-10 18:11:27 +00:00
2017-03-25 15:26:00 +00:00
/// Expects a response to have a JSON body that is a `Map` and satisfies the given [validator] schema.
2021-05-15 08:35:27 +00:00
Matcher hasValidBody(Validator validator) => _HasValidBody(validator);
2017-03-25 15:26:00 +00:00
2023-11-04 02:18:44 +00:00
String notHttpResponse = "expected http.Response but got none\n";
2016-12-10 18:11:27 +00:00
class _IsJson extends Matcher {
2022-02-22 00:07:01 +00:00
dynamic value;
2016-12-10 18:11:27 +00:00
_IsJson(this.value);
@override
Description describe(Description description) {
2017-03-25 15:26:00 +00:00
return description.add('equals the desired JSON response: $value');
2016-12-10 18:11:27 +00:00
}
@override
2018-07-09 16:10:25 +00:00
bool matches(item, Map matchState) =>
item is http.Response &&
equals(value).matches(json.decode(item.body), matchState);
2016-12-10 18:11:27 +00:00
}
2017-03-25 15:26:00 +00:00
class _HasBody extends Matcher {
2022-02-22 00:07:01 +00:00
final dynamic body;
2017-03-25 15:26:00 +00:00
_HasBody(this.body);
@override
Description describe(Description description) =>
description.add('has body $body');
@override
2018-07-09 16:10:25 +00:00
bool matches(item, Map matchState) {
if (item is http.Response) {
if (body == true) return isNotEmpty.matches(item.bodyBytes, matchState);
2021-05-18 11:43:58 +00:00
if (body is List<int>) {
2018-07-09 16:10:25 +00:00
return equals(body).matches(item.bodyBytes, matchState);
2021-05-18 11:43:58 +00:00
} else {
2018-07-09 16:10:25 +00:00
return equals(body.toString()).matches(item.body, matchState);
2021-05-18 11:43:58 +00:00
}
2018-07-09 16:10:25 +00:00
} else {
return false;
}
2017-03-25 15:26:00 +00:00
}
}
class _HasContentType extends Matcher {
2022-02-22 00:07:01 +00:00
dynamic contentType;
2017-03-25 15:26:00 +00:00
_HasContentType(this.contentType);
@override
Description describe(Description description) {
2018-07-09 16:10:25 +00:00
var str = contentType is ContentType
? ((contentType as ContentType).value)
: contentType.toString();
2022-08-27 09:25:39 +00:00
return description.add('has content type $str');
2017-03-25 15:26:00 +00:00
}
@override
2018-07-09 16:10:25 +00:00
bool matches(item, Map matchState) {
if (item is http.Response) {
2023-11-04 02:18:44 +00:00
//if (!item.headers.containsKey('content-type')) return false;
var headerContentType = item.headers['content-type'];
if (headerContentType == null) return false;
2018-07-09 16:10:25 +00:00
if (contentType is ContentType) {
2023-11-04 02:18:44 +00:00
var compare = ContentType.parse(headerContentType);
2018-07-09 16:10:25 +00:00
return equals(contentType.mimeType)
.matches(compare.mimeType, matchState);
} else {
return equals(contentType.toString())
2023-11-04 02:18:44 +00:00
.matches(headerContentType, matchState);
2018-07-09 16:10:25 +00:00
}
2023-11-04 02:18:44 +00:00
}
return false;
}
@override
Description describeMismatch(Object? item, Description mismatchDescription,
Map matchState, bool verbose) {
if (item is http.Response) {
var headerContentType = item.headers['content-type'] ?? 'none';
mismatchDescription
.add("expected '$contentType' but got '$headerContentType'\n");
2017-03-25 15:26:00 +00:00
} else {
2023-11-04 02:18:44 +00:00
mismatchDescription.add(notHttpResponse);
2017-03-25 15:26:00 +00:00
}
2023-11-04 02:18:44 +00:00
return mismatchDescription;
2017-03-25 15:26:00 +00:00
}
}
class _HasHeader extends Matcher {
final String key;
2022-02-22 00:07:01 +00:00
final dynamic value;
2017-03-25 15:26:00 +00:00
_HasHeader(this.key, this.value);
@override
Description describe(Description description) {
2021-05-18 11:43:58 +00:00
if (value == true) {
2017-03-25 15:26:00 +00:00
return description.add('contains header $key');
2021-05-18 11:43:58 +00:00
} else {
2017-03-25 15:26:00 +00:00
return description.add('contains header $key with value(s) $value');
2021-05-18 11:43:58 +00:00
}
2017-03-25 15:26:00 +00:00
}
@override
2018-07-09 16:10:25 +00:00
bool matches(item, Map matchState) {
if (item is http.Response) {
if (value == true) {
return contains(key.toLowerCase())
.matches(item.headers.keys, matchState);
} else {
2023-11-05 09:34:41 +00:00
var headerKey = item.headers[key.toLowerCase()];
if (headerKey == null) return false;
2021-05-18 11:43:58 +00:00
var v = value is Iterable ? (value as Iterable) : [value];
2023-11-05 09:34:41 +00:00
return v.map((x) => x.toString()).every(headerKey.split(',').contains);
2018-07-09 16:10:25 +00:00
}
2017-03-25 15:26:00 +00:00
} else {
2018-07-09 16:10:25 +00:00
return false;
2017-03-25 15:26:00 +00:00
}
}
2023-11-04 02:18:44 +00:00
@override
Description describeMismatch(Object? item, Description mismatchDescription,
Map matchState, bool verbose) {
if (item is http.Response) {
mismatchDescription.add("expected '$key' but got none\n");
} else {
mismatchDescription.add(notHttpResponse);
}
return mismatchDescription;
}
2017-03-25 15:26:00 +00:00
}
2016-12-10 18:11:27 +00:00
class _HasStatus extends Matcher {
int status;
_HasStatus(this.status);
@override
Description describe(Description description) {
2017-03-25 15:26:00 +00:00
return description.add('has status code $status');
2016-12-10 18:11:27 +00:00
}
@override
2018-07-09 16:10:25 +00:00
bool matches(item, Map matchState) =>
item is http.Response &&
2016-12-10 18:11:27 +00:00
equals(status).matches(item.statusCode, matchState);
2023-11-04 02:18:44 +00:00
@override
Description describeMismatch(Object? item, Description mismatchDescription,
Map matchState, bool verbose) {
if (item is http.Response) {
mismatchDescription.add('expected $status but got ${item.statusCode}\n');
} else {
mismatchDescription.add(notHttpResponse);
}
return mismatchDescription;
}
2016-12-10 18:11:27 +00:00
}
2017-03-25 15:26:00 +00:00
class _HasValidBody extends Matcher {
final Validator validator;
2023-11-04 02:18:44 +00:00
final _errors = <String>[];
2017-03-25 15:26:00 +00:00
_HasValidBody(this.validator);
@override
Description describe(Description description) =>
description.add('matches validation schema ${validator.rules}');
@override
2018-07-09 16:10:25 +00:00
bool matches(item, Map matchState) {
if (item is http.Response) {
final jsons = json.decode(item.body);
2023-11-04 02:18:44 +00:00
if (jsons is Map) {
try {
return validator.matches(jsons, matchState);
} catch (e) {
_errors.addAll((e as ValidationException).errors);
}
}
}
return false;
}
@override
Description describeMismatch(Object? item, Description mismatchDescription,
Map matchState, bool verbose) {
if (item is http.Response) {
if (_errors.isEmpty) {
mismatchDescription.add("expected JSON but got invalid JSON\n");
} else {
for (var err in _errors) {
mismatchDescription.add("$err\n");
}
}
2018-07-09 16:10:25 +00:00
} else {
2023-11-04 02:18:44 +00:00
mismatchDescription.add(notHttpResponse);
2018-07-09 16:10:25 +00:00
}
2023-11-04 02:18:44 +00:00
return mismatchDescription;
2017-03-25 15:26:00 +00:00
}
}
class _IsAngelHttpException extends Matcher {
2021-04-26 01:05:34 +00:00
String? message;
int? statusCode;
2017-03-25 15:26:00 +00:00
final List<String> errors = [];
_IsAngelHttpException(
2021-05-18 11:43:58 +00:00
{this.message, this.statusCode, Iterable<String> errors = const []}) {
2021-04-26 01:05:34 +00:00
this.errors.addAll(errors);
2017-03-25 15:26:00 +00:00
}
@override
Description describe(Description description) {
2021-05-15 08:35:27 +00:00
if (message?.isNotEmpty != true && statusCode == null && errors.isEmpty) {
2017-03-25 15:26:00 +00:00
return description.add('is an Angel HTTP Exception');
2021-05-15 08:35:27 +00:00
} else {
var buf = StringBuffer('is an Angel HTTP Exception with');
2017-03-25 15:26:00 +00:00
if (statusCode != null) buf.write(' status code $statusCode');
if (message?.isNotEmpty == true) {
2021-05-15 08:35:27 +00:00
if (statusCode != null && errors.isNotEmpty) {
2017-03-25 15:26:00 +00:00
buf.write(',');
2021-05-15 08:35:27 +00:00
} else if (statusCode != null && errors.isEmpty) {
buf.write(' and');
}
2017-03-25 15:26:00 +00:00
buf.write(' message "$message"');
}
if (errors.isNotEmpty) {
2021-05-15 08:35:27 +00:00
if (statusCode != null || message?.isNotEmpty == true) {
2017-03-25 15:26:00 +00:00
buf.write(' and errors $errors');
2021-05-15 08:35:27 +00:00
} else {
2017-03-25 15:26:00 +00:00
buf.write(' errors $errors');
2021-05-15 08:35:27 +00:00
}
2017-03-25 15:26:00 +00:00
}
return description.add(buf.toString());
}
}
@override
2018-07-09 16:10:25 +00:00
bool matches(item, Map matchState) {
if (item is http.Response) {
final jsons = json.decode(item.body);
if (jsons is Map && jsons['isError'] == true) {
2021-05-15 08:35:27 +00:00
var exc = AngelHttpException.fromMap(jsons);
2018-07-09 16:10:25 +00:00
print(exc.toJson());
2021-05-15 08:35:27 +00:00
if (message?.isNotEmpty != true &&
statusCode == null &&
errors.isEmpty) {
2018-07-09 16:10:25 +00:00
return true;
2021-05-15 08:35:27 +00:00
} else {
2021-05-18 11:43:58 +00:00
if (statusCode != null) {
if (!equals(statusCode).matches(exc.statusCode, matchState)) {
return false;
}
}
2018-07-09 16:10:25 +00:00
2021-05-18 11:43:58 +00:00
if (message?.isNotEmpty == true) {
if (!equals(message).matches(exc.message, matchState)) {
return false;
}
}
2018-07-09 16:10:25 +00:00
if (errors.isNotEmpty) {
2021-05-15 08:35:27 +00:00
if (!errors.every(
(err) => contains(err).matches(exc.errors, matchState))) {
2018-07-09 16:10:25 +00:00
return false;
2021-05-15 08:35:27 +00:00
}
2018-07-09 16:10:25 +00:00
}
return true;
2017-03-25 15:26:00 +00:00
}
2021-05-15 08:35:27 +00:00
} else {
2018-07-09 16:10:25 +00:00
return false;
2021-05-15 08:35:27 +00:00
}
2018-07-09 16:10:25 +00:00
} else {
2017-03-25 15:26:00 +00:00
return false;
2018-07-09 16:10:25 +00:00
}
2017-03-25 15:26:00 +00:00
}
}