Skip to content

Commit

Permalink
feature/add exact body check option for FullHttpRequestMatcher (#158)
Browse files Browse the repository at this point in the history
* add exact body check option for FullHttpRequestMatcher

* let tests use dedicated endpoints
  • Loading branch information
sebastianbuechler authored Aug 25, 2023
1 parent a03d15b commit feb1af1
Show file tree
Hide file tree
Showing 3 changed files with 118 additions and 7 deletions.
19 changes: 15 additions & 4 deletions lib/src/extensions/matches_request.dart
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,10 @@ import 'package:http_mock_adapter/src/types.dart';
extension MatchesRequest on RequestOptions {
/// Check values against matchers.
/// [request] is the configured [Request] which would contain the matchers if used.
bool matchesRequest(Request request) {
bool matchesRequest(Request request, bool needsExactBody) {
final routeMatched = doesRouteMatch(path, request.route);
final requestBodyMatched = matches(data, request.data);
final requestBodyMatched =
matches(data, request.data, exactMaps: needsExactBody);
final queryParametersMatched =
matches(queryParameters, request.queryParameters ?? {});
final headersMatched = matches(headers, request.headers ?? {});
Expand Down Expand Up @@ -46,7 +47,7 @@ extension MatchesRequest on RequestOptions {
}

/// Check the map keys and values determined by the definition.
bool matches(dynamic actual, dynamic expected) {
bool matches(dynamic actual, dynamic expected, {bool exactMaps = false}) {
if (actual == null && expected == null) {
return true;
}
Expand All @@ -60,6 +61,11 @@ extension MatchesRequest on RequestOptions {
return false;
}
} else if (actual is Map && expected is Map) {
// If exactMap is true, ensure that actual and expected have the same length.
if (exactMaps && actual.length != expected.length) {
return false;
}

for (final key in expected.keys.toList()) {
if (!actual.containsKey(key)) {
return false;
Expand All @@ -71,7 +77,7 @@ extension MatchesRequest on RequestOptions {
} else if (expected[key] != actual[key]) {
// Exact match unless map.
if (expected[key] is Map && actual[key] is Map) {
if (!matches(actual[key], expected[key])) {
if (!matches(actual[key], expected[key], exactMaps: exactMaps)) {
// Allow maps to use matchers.
return false;
}
Expand All @@ -82,6 +88,11 @@ extension MatchesRequest on RequestOptions {
}
}
}

// If exactMap is true, check that there are no keys in actual that are not in expected.
if (exactMaps && actual.keys.any((key) => !expected.containsKey(key))) {
return false;
}
} else if (actual is List && expected is List) {
for (var index in Iterable.generate(actual.length)) {
if (!matches(actual[index], expected[index])) {
Expand Down
5 changes: 3 additions & 2 deletions lib/src/matchers/http_matcher.dart
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,12 @@ abstract class HttpRequestMatcher {
/// class.
///
class FullHttpRequestMatcher extends HttpRequestMatcher {
const FullHttpRequestMatcher();
final bool needsExactBody;
const FullHttpRequestMatcher({this.needsExactBody = false});

@override
bool matches(RequestOptions ongoingRequest, Request matcher) {
return ongoingRequest.matchesRequest(matcher);
return ongoingRequest.matchesRequest(matcher, needsExactBody);
}
}

Expand Down
101 changes: 100 additions & 1 deletion test/extensions/matches_request_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ void main() {
queryParameters: <String, dynamic>{},
);

expect(options.matchesRequest(request), true);
expect(options.matchesRequest(request, false), true);
});

group('matches', () {
Expand Down Expand Up @@ -251,6 +251,105 @@ void main() {
e.error is AssertionError)));
});
});

group('Exact body matches', () {
late Dio dio;
late DioAdapter dioAdapter;

setUpAll(() {
dio = Dio(BaseOptions(contentType: Headers.jsonContentType));
dioAdapter = DioAdapter(
dio: dio,
matcher: const FullHttpRequestMatcher(needsExactBody: true),
);
});

test(
'does not match requests via onPost() when expected body is subset of actual body',
() async {
dioAdapter.onPost(
'/too-many-fields',
(server) => server.reply(200, 'OK'),
data: {
'expected': {
'nestedExpected': 'value',
},
},
);

final data = {
'expected': {
'nestedExpected': 'value',
'nestedUnexpected': 'value',
},
'unexepected': 'value'
};
expect(
() => dio.post('/too-many-fields', data: data),
throwsA(
predicate((e) =>
e is DioException &&
e.type == DioExceptionType.unknown &&
e.error is AssertionError),
),
);
});
test(
'does not match requests via onPost() when actual body is subset of expected body',
() async {
dioAdapter.onPost(
'/not-enough-fields',
(server) => server.reply(200, 'OK'),
data: {
'expected': {
'nestedExpected': 'value',
'nestedUnexpected': 'value',
},
'unexepected': 'value'
},
);

final data = {
'expected': {
'nestedExpected': 'value',
},
};
expect(
() => dio.post('/not-enough-fields', data: data),
throwsA(
predicate((e) =>
e is DioException &&
e.type == DioExceptionType.unknown &&
e.error is AssertionError),
),
);
});
test(
'does match requests via onPost() when expected body is equal to actual body',
() async {
dioAdapter.onPost(
'/post-exact-data',
(server) => server.reply(200, 'OK'),
data: {
'expected': {
'nestedExpected': 'value',
'nestedUnexpected': 'value',
},
'unexepected': 'value'
},
);

var response = await dio.post('/post-exact-data', data: {
'expected': {
'nestedExpected': 'value',
'nestedUnexpected': 'value',
},
'unexepected': 'value'
});

expect(response.data, 'OK');
});
});
});
});
}

0 comments on commit feb1af1

Please sign in to comment.