From 325055da4bc21c36d020e7c010c42ef57fa8361a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20B=C3=BCchler?= Date: Thu, 24 Aug 2023 18:25:03 +0200 Subject: [PATCH 1/4] add a next interceptor handler on DioInterceptor for missing mocks --- lib/src/adapters/dio_adapter.dart | 9 ++++- lib/src/interceptors/dio_interceptor.dart | 13 +++++++ lib/src/mixins/recording.dart | 22 ++++++++++- lib/src/types.dart | 2 +- test/interceptors/dio_interceptor_test.dart | 43 +++++++++++++++++++++ 5 files changed, 85 insertions(+), 4 deletions(-) create mode 100644 test/interceptors/dio_interceptor_test.dart diff --git a/lib/src/adapters/dio_adapter.dart b/lib/src/adapters/dio_adapter.dart index 0f09e95..a5cf0dd 100644 --- a/lib/src/adapters/dio_adapter.dart +++ b/lib/src/adapters/dio_adapter.dart @@ -17,10 +17,17 @@ class DioAdapter with Recording, RequestHandling implements HttpClientAdapter { @override final HttpRequestMatcher matcher; + @override + final bool printLogs; + + @override + final bool failOnMissingMock = true; + /// Constructs a [DioAdapter] and configures the passed [Dio] instance. DioAdapter({ required this.dio, this.matcher = const FullHttpRequestMatcher(), + this.printLogs = false, }) { dio.httpClientAdapter = this; } @@ -40,7 +47,7 @@ class DioAdapter with Recording, RequestHandling implements HttpClientAdapter { } await setDefaultRequestHeaders(dio, requestOptions); - final response = await mockResponse(requestOptions); + final response = await mockResponse(requestOptions) as MockResponse; // Waits for defined duration. if (response.delay != null) await Future.delayed(response.delay!); diff --git a/lib/src/interceptors/dio_interceptor.dart b/lib/src/interceptors/dio_interceptor.dart index 11ad9f0..e019c52 100644 --- a/lib/src/interceptors/dio_interceptor.dart +++ b/lib/src/interceptors/dio_interceptor.dart @@ -11,10 +11,18 @@ class DioInterceptor extends Interceptor with Recording, RequestHandling { @override final HttpRequestMatcher matcher; + @override + final bool printLogs; + + @override + final bool failOnMissingMock; + /// Constructs a [DioInterceptor] and configures the passed [Dio] instance. DioInterceptor({ required this.dio, this.matcher = const FullHttpRequestMatcher(), + this.printLogs = false, + this.failOnMissingMock = false, }) { dio.interceptors.add(this); } @@ -26,6 +34,11 @@ class DioInterceptor extends Interceptor with Recording, RequestHandling { await setDefaultRequestHeaders(dio, requestOptions); final response = await mockResponse(requestOptions); + if (response == null) { + requestInterceptorHandler.next(requestOptions); + return; + } + // Reject the response if type is MockDioException. if (isMockDioException(response)) { requestInterceptorHandler.reject(response as DioException); diff --git a/lib/src/mixins/recording.dart b/lib/src/mixins/recording.dart index d9f6bf0..163f855 100644 --- a/lib/src/mixins/recording.dart +++ b/lib/src/mixins/recording.dart @@ -5,6 +5,10 @@ import 'package:http_mock_adapter/src/types.dart'; /// An ability that lets a construct to record a [RequestMatcher] history. mixin Recording { + bool get printLogs; + + bool get failOnMissingMock; + HttpRequestMatcher get matcher; /// The index of request invocations. @@ -29,8 +33,22 @@ mixin Recording { // Fail when a mocked route is not found for the request. if (_invocationIndex == null || _invocationIndex! < 0) { - throw AssertionError( - 'Could not find mocked route matching request for ${requestOptions.signature}', + if (failOnMissingMock) { + throw AssertionError( + 'Could not find mocked route matching request for ${requestOptions.signature}', + ); + } + if (printLogs) { + print( + 'Not matched request: ${requestOptions.method} ${requestOptions.uri}', + ); + } + return Future.value(null); + } + + if (printLogs) { + print( + 'Matched request: ${requestOptions.method} ${requestOptions.uri}', ); } diff --git a/lib/src/types.dart b/lib/src/types.dart index ee3d372..c61d95f 100644 --- a/lib/src/types.dart +++ b/lib/src/types.dart @@ -9,7 +9,7 @@ typedef MockServerCallback = void Function(MockServer server); /// Type for [Recording]'s [ResponseBody], which takes [RequestOptions] as a parameter /// and compares its signature to saved [Request]'s signature and chooses right response. -typedef MockResponseBodyCallback = Future Function( +typedef MockResponseBodyCallback = Future Function( RequestOptions options, ); diff --git a/test/interceptors/dio_interceptor_test.dart b/test/interceptors/dio_interceptor_test.dart new file mode 100644 index 0000000..249811d --- /dev/null +++ b/test/interceptors/dio_interceptor_test.dart @@ -0,0 +1,43 @@ +import 'package:dio/dio.dart'; +import 'package:http_mock_adapter/http_mock_adapter.dart'; +import 'package:test/test.dart'; + +void main() { + late Dio dio; + late DioInterceptor dioInterceptor1; + late DioInterceptor dioInterceptor2; + + setUp(() { + dio = Dio(); + }); + + group('DioInterceptor', () { + test('if failOnMissingMock is false do not fail on missing mock', () async { + dioInterceptor1 = DioInterceptor(dio: dio, failOnMissingMock: false); + dioInterceptor2 = DioInterceptor(dio: dio, failOnMissingMock: false); + + dio.interceptors.addAll([dioInterceptor1, dioInterceptor2]); + + dioInterceptor1.onGet( + '/interceptor-1-route', + (server) => server.reply( + 200, + {'message': 'Success from interceptor 1'}, + ), + ); + dioInterceptor1.onGet( + '/interceptor-2-route', + (server) => server.reply( + 200, + {'message': 'Success from interceptor 2'}, + ), + ); + + final int1Response = await dio.get('/interceptor-1-route'); + expect(int1Response.data, {'message': 'Success from interceptor 1'}); + + final int2Response = await dio.get('/interceptor-2-route'); + expect(int2Response.data, {'message': 'Success from interceptor 2'}); + }); + }); +} From 90668fb3a6cfa0fe8681aa6d81c5112138a4653b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20B=C3=BCchler?= Date: Thu, 24 Aug 2023 18:34:01 +0200 Subject: [PATCH 2/4] set default --- lib/src/interceptors/dio_interceptor.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/src/interceptors/dio_interceptor.dart b/lib/src/interceptors/dio_interceptor.dart index e019c52..c465ca5 100644 --- a/lib/src/interceptors/dio_interceptor.dart +++ b/lib/src/interceptors/dio_interceptor.dart @@ -22,7 +22,7 @@ class DioInterceptor extends Interceptor with Recording, RequestHandling { required this.dio, this.matcher = const FullHttpRequestMatcher(), this.printLogs = false, - this.failOnMissingMock = false, + this.failOnMissingMock = true, }) { dio.interceptors.add(this); } From d378908bc15182ce3ca704965d1cfed8c7d025ed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20B=C3=BCchler?= Date: Fri, 25 Aug 2023 10:03:33 +0200 Subject: [PATCH 3/4] introduce custom logger --- lib/src/adapters/dio_adapter.dart | 5 +++ lib/src/interceptors/dio_interceptor.dart | 5 +++ lib/src/logger/logger.dart | 15 +++++++ lib/src/mixins/recording.dart | 19 ++++----- pubspec.yaml | 1 + test/interceptors/dio_interceptor_test.dart | 47 +++++++++++++++++++-- 6 files changed, 77 insertions(+), 15 deletions(-) create mode 100644 lib/src/logger/logger.dart diff --git a/lib/src/adapters/dio_adapter.dart b/lib/src/adapters/dio_adapter.dart index a5cf0dd..0967abe 100644 --- a/lib/src/adapters/dio_adapter.dart +++ b/lib/src/adapters/dio_adapter.dart @@ -2,9 +2,11 @@ import 'dart:typed_data'; import 'package:dio/dio.dart'; import 'package:http_mock_adapter/src/exceptions.dart'; +import 'package:http_mock_adapter/src/logger/logger.dart'; import 'package:http_mock_adapter/src/matchers/http_matcher.dart'; import 'package:http_mock_adapter/src/mixins/mixins.dart'; import 'package:http_mock_adapter/src/response.dart'; +import 'package:logger/logger.dart'; /// [HttpClientAdapter] extension with data mocking and recording functionality. class DioAdapter with Recording, RequestHandling implements HttpClientAdapter { @@ -18,6 +20,8 @@ class DioAdapter with Recording, RequestHandling implements HttpClientAdapter { final HttpRequestMatcher matcher; @override + late Logger logger; + final bool printLogs; @override @@ -30,6 +34,7 @@ class DioAdapter with Recording, RequestHandling implements HttpClientAdapter { this.printLogs = false, }) { dio.httpClientAdapter = this; + logger = getLogger(printLogs); } /// [DioAdapter]`s [fetch] configuration intended to work with mock data. diff --git a/lib/src/interceptors/dio_interceptor.dart b/lib/src/interceptors/dio_interceptor.dart index c465ca5..80107e4 100644 --- a/lib/src/interceptors/dio_interceptor.dart +++ b/lib/src/interceptors/dio_interceptor.dart @@ -1,7 +1,9 @@ import 'package:dio/dio.dart'; +import 'package:http_mock_adapter/src/logger/logger.dart'; import 'package:http_mock_adapter/src/matchers/http_matcher.dart'; import 'package:http_mock_adapter/src/mixins/mixins.dart'; import 'package:http_mock_adapter/src/response.dart'; +import 'package:logger/logger.dart'; /// [DioInterceptor] is a class for mocking [Dio] requests with [Interceptor]. class DioInterceptor extends Interceptor with Recording, RequestHandling { @@ -12,6 +14,8 @@ class DioInterceptor extends Interceptor with Recording, RequestHandling { final HttpRequestMatcher matcher; @override + late Logger logger; + final bool printLogs; @override @@ -25,6 +29,7 @@ class DioInterceptor extends Interceptor with Recording, RequestHandling { this.failOnMissingMock = true, }) { dio.interceptors.add(this); + logger = getLogger(printLogs); } /// Dio [Interceptor]`s [onRequest] configuration intended to catch and return diff --git a/lib/src/logger/logger.dart b/lib/src/logger/logger.dart new file mode 100644 index 0000000..a20917f --- /dev/null +++ b/lib/src/logger/logger.dart @@ -0,0 +1,15 @@ +import 'package:logger/logger.dart'; + +getLogger(bool printLogs) { + return Logger( + level: printLogs ? Level.debug : Level.off, + printer: PrettyPrinter( + methodCount: 0, + errorMethodCount: 8, + lineLength: 120, + colors: true, + printEmojis: true, + printTime: false, + ), + ); +} diff --git a/lib/src/mixins/recording.dart b/lib/src/mixins/recording.dart index 163f855..9422fdc 100644 --- a/lib/src/mixins/recording.dart +++ b/lib/src/mixins/recording.dart @@ -2,10 +2,11 @@ import 'package:http_mock_adapter/src/extensions/signature.dart'; import 'package:http_mock_adapter/src/matchers/http_matcher.dart'; import 'package:http_mock_adapter/src/request.dart'; import 'package:http_mock_adapter/src/types.dart'; +import 'package:logger/logger.dart'; /// An ability that lets a construct to record a [RequestMatcher] history. mixin Recording { - bool get printLogs; + Logger get logger; bool get failOnMissingMock; @@ -38,19 +39,15 @@ mixin Recording { 'Could not find mocked route matching request for ${requestOptions.signature}', ); } - if (printLogs) { - print( - 'Not matched request: ${requestOptions.method} ${requestOptions.uri}', - ); - } + + logger.d( + 'Not matched request: ${requestOptions.method} ${requestOptions.uri}'); + return Future.value(null); } - if (printLogs) { - print( - 'Matched request: ${requestOptions.method} ${requestOptions.uri}', - ); - } + logger.d( + 'Matched request: ${requestOptions.method} ${requestOptions.uri}'); return requestMatcher.mockResponse(requestOptions); }; diff --git a/pubspec.yaml b/pubspec.yaml index 1da884a..a5388c0 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -12,6 +12,7 @@ dependencies: collection: ^1.18.0 dio: ^5.3.2 http_parser: ^4.0.2 + logger: ^2.0.1 dev_dependencies: lints: '>=2.1.1' diff --git a/test/interceptors/dio_interceptor_test.dart b/test/interceptors/dio_interceptor_test.dart index 249811d..0e989ff 100644 --- a/test/interceptors/dio_interceptor_test.dart +++ b/test/interceptors/dio_interceptor_test.dart @@ -1,5 +1,6 @@ import 'package:dio/dio.dart'; import 'package:http_mock_adapter/http_mock_adapter.dart'; +import 'package:logger/logger.dart'; import 'package:test/test.dart'; void main() { @@ -11,13 +12,11 @@ void main() { dio = Dio(); }); - group('DioInterceptor', () { + group('DioInterceptor -', () { test('if failOnMissingMock is false do not fail on missing mock', () async { dioInterceptor1 = DioInterceptor(dio: dio, failOnMissingMock: false); dioInterceptor2 = DioInterceptor(dio: dio, failOnMissingMock: false); - dio.interceptors.addAll([dioInterceptor1, dioInterceptor2]); - dioInterceptor1.onGet( '/interceptor-1-route', (server) => server.reply( @@ -25,7 +24,7 @@ void main() { {'message': 'Success from interceptor 1'}, ), ); - dioInterceptor1.onGet( + dioInterceptor2.onGet( '/interceptor-2-route', (server) => server.reply( 200, @@ -38,6 +37,46 @@ void main() { final int2Response = await dio.get('/interceptor-2-route'); expect(int2Response.data, {'message': 'Success from interceptor 2'}); + + final googleResponse = await dio.get('https://google.com'); + expect(googleResponse.statusCode, 200); + }); + test('if printLogs is true we should see logs on mocked calls', () async { + dioInterceptor1 = DioInterceptor(dio: dio, printLogs: true); + + dio.interceptors.add(dioInterceptor1); + + dioInterceptor1.onGet( + '/interceptor-1-route', (server) => server.reply(200, 'OK')); + + final capturedLogs = []; + Logger.addOutputListener((event) { + capturedLogs.add(event); + }); + + await dio.get('/interceptor-1-route'); + + expect(capturedLogs.first.origin.message, + 'Matched request: GET /interceptor-1-route'); + expect(capturedLogs.first.origin.level, Level.debug); + }); + test('if printLogs is false we should not see logs on mocked calls', + () async { + dioInterceptor1 = DioInterceptor(dio: dio, printLogs: false); + + dio.interceptors.add(dioInterceptor1); + + dioInterceptor1.onGet( + '/interceptor-1-route', (server) => server.reply(200, 'OK')); + + final capturedLogs = []; + Logger.addOutputListener((event) { + capturedLogs.add(event); + }); + + await dio.get('/interceptor-1-route'); + + expect(capturedLogs.length, 0); }); }); } From f3edabefab81b1882e8bddc447a059ea8ce0d0cb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20B=C3=BCchler?= Date: Fri, 25 Aug 2023 10:17:12 +0200 Subject: [PATCH 4/4] increase test coverage --- lib/src/adapters/dio_adapter.dart | 1 + test/adapters/dio_adapter_test.dart | 6 ++++-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/lib/src/adapters/dio_adapter.dart b/lib/src/adapters/dio_adapter.dart index 0967abe..7ae370e 100644 --- a/lib/src/adapters/dio_adapter.dart +++ b/lib/src/adapters/dio_adapter.dart @@ -46,6 +46,7 @@ class DioAdapter with Recording, RequestHandling implements HttpClientAdapter { Future? cancelFuture, ) async { if (_isClosed) { + logger.e('Cannot establish connection after [$runtimeType] got closed!'); throw ClosedException( 'Cannot establish connection after [$runtimeType] got closed!', ); diff --git a/test/adapters/dio_adapter_test.dart b/test/adapters/dio_adapter_test.dart index 52b49d9..da641ca 100644 --- a/test/adapters/dio_adapter_test.dart +++ b/test/adapters/dio_adapter_test.dart @@ -25,8 +25,10 @@ void main() { expect( () async => await dio.get('/route'), - throwsA(predicate( - (DioException dioError) => dioError.error is ClosedException)), + throwsA(predicate((DioException dioError) => + dioError.error is ClosedException && + dioError.error.toString() == + 'ClosedException: Cannot establish connection after [DioAdapter] got closed!')), ); });