Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add a next interceptor handler on DioInterceptor for missing mocks #159

Merged
merged 4 commits into from
Aug 25, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 14 additions & 1 deletion lib/src/adapters/dio_adapter.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -17,12 +19,22 @@ class DioAdapter with Recording, RequestHandling implements HttpClientAdapter {
@override
final HttpRequestMatcher matcher;

@override
late Logger logger;

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;
logger = getLogger(printLogs);
}

/// [DioAdapter]`s [fetch] configuration intended to work with mock data.
Expand All @@ -34,13 +46,14 @@ 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!',
);
}

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!);
Expand Down
18 changes: 18 additions & 0 deletions lib/src/interceptors/dio_interceptor.dart
Original file line number Diff line number Diff line change
@@ -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 {
Expand All @@ -11,12 +13,23 @@ class DioInterceptor extends Interceptor with Recording, RequestHandling {
@override
final HttpRequestMatcher matcher;

@override
late Logger logger;

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 = true,
}) {
dio.interceptors.add(this);
logger = getLogger(printLogs);
}

/// Dio [Interceptor]`s [onRequest] configuration intended to catch and return
Expand All @@ -26,6 +39,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);
Expand Down
15 changes: 15 additions & 0 deletions lib/src/logger/logger.dart
Original file line number Diff line number Diff line change
@@ -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,
),
);
}
21 changes: 18 additions & 3 deletions lib/src/mixins/recording.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,14 @@ 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 {
Logger get logger;

bool get failOnMissingMock;

HttpRequestMatcher get matcher;

/// The index of request invocations.
Expand All @@ -29,11 +34,21 @@ 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}',
);
}

logger.d(
'Not matched request: ${requestOptions.method} ${requestOptions.uri}');

return Future.value(null);
}

logger.d(
'Matched request: ${requestOptions.method} ${requestOptions.uri}');

return requestMatcher.mockResponse(requestOptions);
};

Expand Down
2 changes: 1 addition & 1 deletion lib/src/types.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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<MockResponse> Function(
typedef MockResponseBodyCallback = Future<MockResponse?> Function(
RequestOptions options,
);

Expand Down
1 change: 1 addition & 0 deletions pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down
6 changes: 4 additions & 2 deletions test/adapters/dio_adapter_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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!')),
);
});

Expand Down
82 changes: 82 additions & 0 deletions test/interceptors/dio_interceptor_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
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() {
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);

dioInterceptor1.onGet(
'/interceptor-1-route',
(server) => server.reply(
200,
{'message': 'Success from interceptor 1'},
),
);
dioInterceptor2.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'});

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 = <OutputEvent>[];
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 = <OutputEvent>[];
Logger.addOutputListener((event) {
capturedLogs.add(event);
});

await dio.get('/interceptor-1-route');

expect(capturedLogs.length, 0);
});
});
}