diff --git a/lib/src/extensions/matches_request.dart b/lib/src/extensions/matches_request.dart index 3665e63..8af0e95 100644 --- a/lib/src/extensions/matches_request.dart +++ b/lib/src/extensions/matches_request.dart @@ -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 ?? {}); @@ -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; } @@ -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; @@ -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; } @@ -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])) { diff --git a/lib/src/matchers/http_matcher.dart b/lib/src/matchers/http_matcher.dart index 3c5b337..d34402f 100644 --- a/lib/src/matchers/http_matcher.dart +++ b/lib/src/matchers/http_matcher.dart @@ -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); } } diff --git a/test/extensions/matches_request_test.dart b/test/extensions/matches_request_test.dart index 34cb55d..c4ef3d4 100644 --- a/test/extensions/matches_request_test.dart +++ b/test/extensions/matches_request_test.dart @@ -21,7 +21,7 @@ void main() { queryParameters: {}, ); - expect(options.matchesRequest(request), true); + expect(options.matchesRequest(request, false), true); }); group('matches', () { @@ -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'); + }); + }); }); }); }