diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 90344511f..e6955f4c2 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -85,7 +85,7 @@ jobs: run: melos run httpbun:local - name: '[Verify step] Test Dart packages [VM]' run: melos run test:vm - - name: Use httpbun.com for VM/Flutter tests + - name: Use httpbun.com for Web/Flutter tests run: melos run httpbun:com - name: '[Verify step] Test Dart packages [Chrome]' run: melos run test:web:chrome diff --git a/dio/dart_test.yaml b/dio/dart_test.yaml index 53189589b..7e313fcd6 100644 --- a/dio/dart_test.yaml +++ b/dio/dart_test.yaml @@ -20,4 +20,4 @@ override_platforms: arguments: --headless executable: # https://github.com/dart-lang/test/pull/2195 - mac_os: '/Applications/Firefox.app/Contents/MacOS/firefox' \ No newline at end of file + mac_os: '/Applications/Firefox.app/Contents/MacOS/firefox' diff --git a/dio/test/basic_test.dart b/dio/test/basic_test.dart deleted file mode 100644 index 80446934b..000000000 --- a/dio/test/basic_test.dart +++ /dev/null @@ -1,24 +0,0 @@ -import 'dart:async'; - -import 'package:dio/dio.dart'; -import 'package:test/test.dart'; - -import 'mock/adapters.dart'; - -void main() { - test('cancellation', () async { - final dio = Dio() - ..httpClientAdapter = MockAdapter() - ..options.baseUrl = MockAdapter.mockBase; - final token = CancelToken(); - Future.delayed(const Duration(milliseconds: 10), () { - token.cancel('cancelled'); - dio.httpClientAdapter.close(force: true); - }); - - await expectLater( - dio.get('/test-timeout', cancelToken: token), - throwsA((e) => e is DioException && CancelToken.isCancel(e)), - ); - }); -} diff --git a/dio/test/cancel_token_test.dart b/dio/test/cancel_token_test.dart index c6dc0da8c..347a4492c 100644 --- a/dio/test/cancel_token_test.dart +++ b/dio/test/cancel_token_test.dart @@ -1,5 +1,6 @@ import 'package:dio/dio.dart'; import 'package:dio/src/adapters/io_adapter.dart'; +import 'package:dio_test/util.dart'; import 'package:mockito/mockito.dart'; import 'package:test/test.dart'; @@ -60,16 +61,18 @@ void main() { for (final future in futures) { expectLater( future, - throwsA( - (error) => - error is DioException && - error.type == DioExceptionType.cancel && - error.error == reason, + throwsDioException( + DioExceptionType.cancel, + matcher: isA().having( + (e) => e.error, + 'error', + reason, + ), ), ); } - await Future.delayed(const Duration(milliseconds: 50)); + await Future.delayed(const Duration(milliseconds: 100)); token.cancel(reason); expect(requests, hasLength(2)); diff --git a/dio/test/download_test.dart b/dio/test/download_test.dart deleted file mode 100644 index 6027b3942..000000000 --- a/dio/test/download_test.dart +++ /dev/null @@ -1,231 +0,0 @@ -@TestOn('vm') -import 'dart:io'; - -import 'package:dio/dio.dart'; -import 'package:dio_test/util.dart'; -import 'package:path/path.dart' as p; -import 'package:test/test.dart'; - -import 'mock/adapters.dart'; -import 'utils.dart'; - -void main() { - late Directory tmp; - - setUpAll(() { - tmp = Directory.systemTemp.createTempSync('dio_test_'); - addTearDown(() { - tmp.deleteSync(recursive: true); - }); - }); - - setUp(startServer); - tearDown(stopServer); - - test('download does not change the response type', () async { - final savePath = p.join(tmp.path, 'download0.md'); - - final dio = Dio()..options.baseUrl = serverUrl.toString(); - final options = Options(responseType: ResponseType.plain); - await dio.download('/download', savePath, options: options); - expect(options.responseType, ResponseType.plain); - }); - - test('download1', () async { - final savePath = p.join(tmp.path, 'download1.md'); - final dio = Dio()..options.baseUrl = serverUrl.toString(); - await dio.download('/download', savePath); - - int? total; - int? count; - await dio.download( - '/download', - savePath, - onReceiveProgress: (c, t) { - total = t; - count = c; - }, - ); - - final f = File(savePath); - expect(f.readAsStringSync(), equals('I am a text file')); - expect(count, f.readAsBytesSync().length); - expect(count, total); - }); - - test('download2', () async { - final savePath = p.join(tmp.path, 'download2.md'); - final dio = Dio()..options.baseUrl = serverUrl.toString(); - await dio.downloadUri( - serverUrl.replace(path: '/download'), - (header) => savePath, - ); - - final f = File(savePath); - expect(f.readAsStringSync(), equals('I am a text file')); - }); - - test('download error', () async { - final savePath = p.join(tmp.path, 'download_error.md'); - final dio = Dio()..options.baseUrl = serverUrl.toString(); - Response response = await dio - .download('/error', savePath) - .catchError((e) => (e as DioException).response!); - expect(response.data, 'error'); - response = await dio - .download( - '/error', - savePath, - options: Options(receiveDataWhenStatusError: false), - ) - .catchError((e) => (e as DioException).response!); - expect(response.data, null); - }); - - test( - 'download timeout', - () async { - final dio = Dio(); - final timeoutMatcher = allOf([ - throwsA(isA()), - throwsA( - predicate( - (e) => e.type == DioExceptionType.receiveTimeout, - ), - ), - ]); - await expectLater( - dio.downloadUri( - Uri.parse('$serverUrl/download').replace( - queryParameters: {'count': '3', 'gap': '2'}, - ), - p.join(tmp.path, 'download_timeout.md'), - options: Options(receiveTimeout: Duration(seconds: 1)), - ), - timeoutMatcher, - ); - // Throws nothing if it constantly gets response bytes. - await dio.download( - 'https://github.com/cfug/flutter.cn/archive/refs/heads/main.zip', - p.join(tmp.path, 'main.zip'), - options: Options(receiveTimeout: Duration(seconds: 1)), - ); - }, - // The download of the main.zip file can be slow, - // so we need to increase the timeout. - timeout: Timeout(Duration(minutes: 1)), - ); - - test('download cancellation', () async { - final savePath = p.join(tmp.path, 'download_cancellation.md'); - final cancelToken = CancelToken(); - Future.delayed(Duration(milliseconds: 100), () { - cancelToken.cancel(); - }); - expect( - Dio() - .download( - '$serverUrl/download', - savePath, - cancelToken: cancelToken, - ) - .catchError((e) => throw (e as DioException).type), - throwsA(DioExceptionType.cancel), - ); - }); - - test('delete on error', () async { - final savePath = p.join(tmp.path, 'delete_on_error.md'); - final f = File(savePath)..createSync(recursive: true); - expect(f.existsSync(), isTrue); - - final dio = Dio()..options.baseUrl = serverUrl.toString(); - await expectLater( - dio - .download( - '/download', - savePath, - deleteOnError: true, - onReceiveProgress: (count, total) => throw AssertionError(), - ) - .catchError((e) => throw (e as DioException).error!), - throwsA(isA()), - ); - expect(f.existsSync(), isFalse); - }); - - test('delete on cancel', () async { - final savePath = p.join(tmp.path, 'delete_on_cancel.md'); - final f = File(savePath)..createSync(recursive: true); - expect(f.existsSync(), isTrue); - - final cancelToken = CancelToken(); - final dio = Dio()..options.baseUrl = serverUrl.toString(); - await expectLater( - dio.download( - '/download', - savePath, - deleteOnError: true, - cancelToken: cancelToken, - onReceiveProgress: (count, total) => cancelToken.cancel(), - ), - throwsDioException( - DioExceptionType.cancel, - stackTraceContains: 'test/download_test.dart', - ), - ); - await Future.delayed(const Duration(milliseconds: 100)); - expect(f.existsSync(), isFalse); - }); - - test('cancel download mid stream', () async { - const savePath = 'test/download/_test.md'; - final f = File(savePath)..createSync(recursive: true); - expect(f.existsSync(), isTrue); - - final cancelToken = CancelToken(); - final dio = Dio()..options.baseUrl = httpbunBaseUrl; - - await expectLater( - dio.download( - '/bytes/10000', - savePath, - cancelToken: cancelToken, - deleteOnError: true, - onReceiveProgress: (c, t) { - if (c > 5000) { - cancelToken.cancel(); - } - }, - ), - throwsDioException( - DioExceptionType.cancel, - stackTraceContains: 'test/download_test.dart', - ), - ); - - await Future.delayed(const Duration(milliseconds: 100)); - expect(f.existsSync(), isFalse); - }); - - test('`savePath` types', () async { - final testPath = p.join(tmp.path, 'savePath'); - - final dio = Dio() - ..options.baseUrl = EchoAdapter.mockBase - ..httpClientAdapter = EchoAdapter(); - - await expectLater( - dio.download('/test', testPath), - completes, - ); - await expectLater( - dio.download('/test', (headers) => testPath), - completes, - ); - await expectLater( - dio.download('/test', (headers) async => testPath), - completes, - ); - }); -} diff --git a/dio/test/mock/flutter.png b/dio/test/mock/flutter.png deleted file mode 100644 index a82ca4352..000000000 Binary files a/dio/test/mock/flutter.png and /dev/null differ diff --git a/dio/test/options_test.dart b/dio/test/options_test.dart index 4135b7d20..7437d0712 100644 --- a/dio/test/options_test.dart +++ b/dio/test/options_test.dart @@ -10,42 +10,8 @@ import 'package:test/test.dart'; import 'mock/adapters.dart'; import 'mock/http_mock.mocks.dart'; -import 'utils.dart'; void main() { - setUp(startServer); - tearDown(stopServer); - - test('headers are kept after redirects', () async { - final dio = Dio( - BaseOptions( - baseUrl: serverUrl.toString(), - headers: {'x-test-base': 'test-base'}, - ), - ); - final response = await dio.get( - '/redirect', - options: Options(headers: {'x-test-header': 'test-value'}), - ); - expect(response.isRedirect, isTrue); - expect( - response.data['headers']['x-test-base'].single, - equals('test-base'), - ); - expect( - response.data['headers']['x-test-header'].single, - equals('test-value'), - ); - expect( - response.requestOptions.headers['x-test-base'], - equals('test-base'), - ); - expect( - response.requestOptions.headers['x-test-header'], - equals('test-value'), - ); - }); - test('options', () { final map = {'a': '5'}; final mapOverride = {'b': '6'}; @@ -256,113 +222,6 @@ void main() { ro3.copyWith(); }); - test('default content-type', () async { - final dio = Dio(); - dio.options.baseUrl = EchoAdapter.mockBase; - dio.httpClientAdapter = EchoAdapter(); - - final r1 = await dio.get(''); - expect( - r1.requestOptions.headers[Headers.contentTypeHeader], - null, - ); - - final r2 = await dio.get( - '', - options: Options(contentType: Headers.jsonContentType), - ); - expect( - r2.requestOptions.headers[Headers.contentTypeHeader], - Headers.jsonContentType, - ); - - final r3 = await dio.get( - '', - options: Options( - headers: {Headers.contentTypeHeader: Headers.jsonContentType}, - ), - ); - expect( - r3.requestOptions.headers[Headers.contentTypeHeader], - Headers.jsonContentType, - ); - - final r4 = await dio.post('', data: ''); - expect( - r4.requestOptions.headers[Headers.contentTypeHeader], - Headers.jsonContentType, - ); - - final r5 = await dio.get( - '', - options: Options( - // Final result should respect this. - contentType: Headers.textPlainContentType, - // Rather than this. - headers: {Headers.contentTypeHeader: Headers.formUrlEncodedContentType}, - ), - ); - expect( - r5.requestOptions.headers[Headers.contentTypeHeader], - Headers.textPlainContentType, - ); - - final r6 = await dio.get( - '', - data: '', - options: Options( - contentType: Headers.formUrlEncodedContentType, - headers: {Headers.contentTypeHeader: Headers.jsonContentType}, - ), - ); - expect( - r6.requestOptions.headers[Headers.contentTypeHeader], - Headers.formUrlEncodedContentType, - ); - - // Update the base option. - dio.options.contentType = Headers.textPlainContentType; - final r7 = await dio.get(''); - expect( - r7.requestOptions.headers[Headers.contentTypeHeader], - Headers.textPlainContentType, - ); - - final r8 = await dio.get( - '', - options: Options(contentType: Headers.jsonContentType), - ); - expect( - r8.requestOptions.headers[Headers.contentTypeHeader], - Headers.jsonContentType, - ); - - final r9 = await dio.get( - '', - options: Options( - headers: {Headers.contentTypeHeader: Headers.jsonContentType}, - ), - ); - expect( - r9.requestOptions.headers[Headers.contentTypeHeader], - Headers.jsonContentType, - ); - - final r10 = await dio.post('', data: FormData()); - expect( - r10.requestOptions.contentType, - startsWith(Headers.multipartFormDataContentType), - ); - - // Regression: https://github.com/cfug/dio/issues/1834 - final r11 = await dio.get(''); - expect(r11.data, ''); - final r12 = await dio.get(''); - expect(r12.data, null); - final r13 = await dio.get>(''); - expect(r13.data, null); - }); - test('default content-type 2', () async { final dio = Dio(); dio.options.baseUrl = 'https://www.example.com'; diff --git a/dio_test/lib/src/test/cancellation_tests.dart b/dio_test/lib/src/test/cancellation_tests.dart new file mode 100644 index 000000000..eb1063cb5 --- /dev/null +++ b/dio_test/lib/src/test/cancellation_tests.dart @@ -0,0 +1,117 @@ +import 'dart:async'; + +import 'package:dio/dio.dart'; +import 'package:dio_test/util.dart'; +import 'package:test/test.dart'; + +void cancellationTests( + Dio Function(String baseUrl) create, +) { + late Dio dio; + + setUp(() { + dio = create(httpbunBaseUrl); + }); + + group('cancellation', () { + test('basic', () { + final token = CancelToken(); + + Future.delayed(const Duration(milliseconds: 250), () { + token.cancel('cancelled'); + }); + + expectLater( + dio.get('/drip-lines?delay=0', cancelToken: token), + throwsDioException( + DioExceptionType.cancel, + stackTraceContains: kIsWeb + ? 'test/test_suite_test.dart' + : 'test/cancellation_tests.dart', + matcher: allOf([ + isA().having( + (e) => e.error, + 'error', + 'cancelled', + ), + isA().having( + (e) => e.message, + 'message', + 'The request was manually cancelled by the user.', + ), + ]), + ), + ); + }); + + test('cancel multiple requests with single token', () async { + final token = CancelToken(); + + final receiveSuccess1 = Completer(); + final receiveSuccess2 = Completer(); + final futures = [ + dio.get( + '/drip-lines?delay=0&duration=5&numbytes=100', + cancelToken: token, + onReceiveProgress: (count, total) { + if (!receiveSuccess1.isCompleted) { + receiveSuccess1.complete(); + } + }, + ), + dio.get( + '/drip-lines?delay=0&duration=5&numbytes=100', + cancelToken: token, + onReceiveProgress: (count, total) { + if (!receiveSuccess2.isCompleted) { + receiveSuccess2.complete(); + } + }, + ), + ]; + + for (final future in futures) { + expectLater( + future, + throwsDioException( + DioExceptionType.cancel, + stackTraceContains: kIsWeb + ? 'test/test_suite_test.dart' + : 'test/cancellation_tests.dart', + matcher: allOf([ + isA().having( + (e) => e.error, + 'error', + 'cancelled', + ), + isA().having( + (e) => e.message, + 'message', + 'The request was manually cancelled by the user.', + ), + ]), + ), + ); + } + + await Future.wait([ + receiveSuccess1.future, + receiveSuccess2.future, + ]); + + token.cancel('cancelled'); + + expect(receiveSuccess1.isCompleted, isTrue); + expect(receiveSuccess2.isCompleted, isTrue); + expect(token.isCancelled, isTrue); + expect( + token.cancelError, + isA().having( + (e) => e.type, + 'type', + DioExceptionType.cancel, + ), + ); + }); + }); +} diff --git a/dio_test/lib/src/test/download_stream_tests.dart b/dio_test/lib/src/test/download_stream_tests.dart deleted file mode 100644 index 32016b6e4..000000000 --- a/dio_test/lib/src/test/download_stream_tests.dart +++ /dev/null @@ -1,154 +0,0 @@ -import 'dart:async'; -import 'dart:io'; - -import 'package:dio/dio.dart'; -import 'package:dio_test/util.dart'; -import 'package:path/path.dart' as p; -import 'package:test/test.dart'; - -void downloadStreamTests( - Dio Function(String baseUrl) create, -) { - group( - 'download', - () { - late Dio dio; - late Directory tmp; - - setUp(() { - dio = create(httpbunBaseUrl); - }); - - setUpAll(() { - tmp = Directory.systemTemp.createTempSync('dio_test_'); - addTearDown(() { - tmp.deleteSync(recursive: true); - }); - }); - - test('bytes', () async { - final path = p.join(tmp.path, 'bytes.txt'); - - final size = 50000; - int progressEventCount = 0; - int count = 0; - int total = 0; - await dio.download( - '/bytes/$size', - path, - onReceiveProgress: (c, t) { - count = c; - total = t; - progressEventCount++; - }, - ); - - final f = File(path); - expect(count, f.readAsBytesSync().length); - expect(progressEventCount, greaterThanOrEqualTo(1)); - expect(count, total); - }); - - test('cancels request', () async { - final cancelToken = CancelToken(); - - final res = await dio.get( - '/drip', - queryParameters: {'duration': '5', 'delay': '0'}, - options: Options(responseType: ResponseType.stream), - cancelToken: cancelToken, - ); - - Future.delayed(const Duration(seconds: 2), () { - cancelToken.cancel(); - }); - - final completer = Completer(); - res.data!.stream.listen((event) {}, onError: (e, s) { - completer.completeError(e, s); - }); - - await expectLater( - completer.future, - throwsDioException( - DioExceptionType.cancel, - stackTraceContains: 'test/download_stream_tests.dart', - ), - ); - }); - - test('cancels download', () async { - final cancelToken = CancelToken(); - final path = p.join(tmp.path, 'download.txt'); - - Future.delayed(const Duration(milliseconds: 50), () { - cancelToken.cancel(); - }); - - await expectLater( - dio.download( - '/drip', - path, - queryParameters: {'duration': '5', 'delay': '0'}, - cancelToken: cancelToken, - ), - throwsDioException( - DioExceptionType.cancel, - stackTraceContains: 'test/download_stream_tests.dart', - ), - ); - - await Future.delayed(const Duration(milliseconds: 250), () {}); - expect(File(path).existsSync(), false); - }); - - test('cancels streamed response mid request', () async { - final cancelToken = CancelToken(); - final response = await dio.get( - '/bytes/${1024 * 1024 * 100}', - options: Options(responseType: ResponseType.stream), - cancelToken: cancelToken, - onReceiveProgress: (c, t) { - if (c > 5000) { - cancelToken.cancel(); - } - }, - ); - - await expectLater( - (response.data as ResponseBody).stream.last, - throwsDioException( - DioExceptionType.cancel, - stackTraceContains: 'test/download_stream_tests.dart', - ), - ); - }); - - test('cancels download mid request', () async { - final cancelToken = CancelToken(); - final path = p.join(tmp.path, 'download_2.txt'); - - await expectLater( - dio.download( - '/bytes/${1024 * 1024 * 10}', - path, - cancelToken: cancelToken, - onReceiveProgress: (c, t) { - if (c > 5000) { - cancelToken.cancel(); - } - }, - ), - throwsDioException( - DioExceptionType.cancel, - stackTraceContains: 'test/download_stream_tests.dart', - ), - ); - - await Future.delayed(const Duration(milliseconds: 250), () {}); - expect(File(path).existsSync(), false); - }); - }, - testOn: 'vm', - ); -} diff --git a/dio_test/lib/src/test/download_tests.dart b/dio_test/lib/src/test/download_tests.dart new file mode 100644 index 000000000..019f52d21 --- /dev/null +++ b/dio_test/lib/src/test/download_tests.dart @@ -0,0 +1,388 @@ +import 'dart:async'; +import 'dart:convert'; +import 'dart:io'; + +import 'package:dio/dio.dart'; +import 'package:dio_test/util.dart'; +import 'package:path/path.dart' as p; +import 'package:test/test.dart'; + +void downloadTests( + Dio Function(String baseUrl) create, +) { + group( + 'download', + () { + late Dio dio; + late Directory tmp; + + setUp(() { + dio = create(httpbunBaseUrl); + }); + + setUpAll(() { + tmp = Directory.systemTemp.createTempSync('dio_test_'); + addTearDown(() { + tmp.deleteSync(recursive: true); + }); + }); + + test('bytes', () async { + final path = p.join(tmp.path, 'bytes.txt'); + + final size = 50000; + int progressEventCount = 0; + int count = 0; + int total = 0; + await dio.download( + '/bytes/$size', + path, + onReceiveProgress: (c, t) { + count = c; + total = t; + progressEventCount++; + }, + ); + + final f = File(path); + expect(count, f.readAsBytesSync().length); + expect(progressEventCount, greaterThanOrEqualTo(1)); + expect(count, total); + }); + + test('cancels request', () async { + final cancelToken = CancelToken(); + + final res = await dio.get( + '/drip', + queryParameters: {'duration': '5', 'delay': '0'}, + options: Options(responseType: ResponseType.stream), + cancelToken: cancelToken, + ); + + Future.delayed(const Duration(seconds: 2), () { + cancelToken.cancel(); + }); + + final completer = Completer(); + res.data!.stream.listen((event) {}, onError: (e, s) { + completer.completeError(e, s); + }); + + await expectLater( + completer.future, + throwsDioException( + DioExceptionType.cancel, + stackTraceContains: 'test/download_tests.dart', + ), + ); + }); + + test('cancels download', () async { + final cancelToken = CancelToken(); + final path = p.join(tmp.path, 'download.txt'); + + Future.delayed(const Duration(milliseconds: 50), () { + cancelToken.cancel(); + }); + + await expectLater( + dio.download( + '/drip', + path, + queryParameters: {'duration': '5', 'delay': '0'}, + cancelToken: cancelToken, + ), + throwsDioException( + DioExceptionType.cancel, + stackTraceContains: 'test/download_tests.dart', + ), + ); + + await Future.delayed(const Duration(milliseconds: 250), () {}); + expect(File(path).existsSync(), false); + }); + + test('cancels streamed response mid request', () async { + final cancelToken = CancelToken(); + final response = await dio.get( + '/bytes/${1024 * 1024 * 100}', + options: Options(responseType: ResponseType.stream), + cancelToken: cancelToken, + onReceiveProgress: (c, t) { + if (c > 5000) { + cancelToken.cancel(); + } + }, + ); + + await expectLater( + (response.data as ResponseBody).stream.last, + throwsDioException( + DioExceptionType.cancel, + stackTraceContains: 'test/download_tests.dart', + ), + ); + }); + + test('cancels download mid request', () async { + final cancelToken = CancelToken(); + final path = p.join(tmp.path, 'download_2.txt'); + + await expectLater( + dio.download( + '/bytes/${1024 * 1024 * 10}', + path, + cancelToken: cancelToken, + onReceiveProgress: (c, t) { + if (c > 5000) { + cancelToken.cancel(); + } + }, + ), + throwsDioException( + DioExceptionType.cancel, + stackTraceContains: 'test/download_tests.dart', + ), + ); + + await Future.delayed(const Duration(milliseconds: 250), () {}); + expect(File(path).existsSync(), false); + }); + + test('does not change the response type', () async { + final savePath = p.join(tmp.path, 'download0.md'); + + final options = Options(responseType: ResponseType.plain); + await dio.download('/bytes/1000', savePath, options: options); + expect(options.responseType, ResponseType.plain); + }); + + test('text file', () async { + final savePath = p.join(tmp.path, 'download.txt'); + + int? total; + int? count; + await dio.download( + '/payload', + savePath, + data: 'I am a text file', + options: Options( + contentType: Headers.textPlainContentType, + ), + onReceiveProgress: (c, t) { + total = t; + count = c; + }, + ); + + final f = File(savePath); + expect( + f.readAsStringSync(), + equals('I am a text file'), + ); + expect(count, f.readAsBytesSync().length); + expect(count, total); + }); + + test('text file 2', () async { + final savePath = p.join(tmp.path, 'download2.txt'); + + await dio.downloadUri( + Uri.parse(dio.options.baseUrl).replace(path: '/payload'), + (header) => savePath, + data: 'I am a text file', + options: Options( + contentType: Headers.textPlainContentType, + ), + ); + + final f = File(savePath); + expect( + f.readAsStringSync(), + equals('I am a text file'), + ); + }); + + test('error', () async { + final savePath = p.join(tmp.path, 'download_error.md'); + + expectLater( + dio.download( + '/mix/s=400/b64=${base64Encode('error'.codeUnits)}', + savePath, + ), + throwsDioException( + DioExceptionType.badResponse, + stackTraceContains: 'test/download_tests.dart', + matcher: isA().having( + (e) => e.response!.data, + 'data', + 'error', + ), + ), + ); + + expectLater( + dio.download( + '/mix/s=400/b64=${base64Encode('error'.codeUnits)}', + savePath, + options: Options(receiveDataWhenStatusError: false), + ), + throwsDioException( + DioExceptionType.badResponse, + stackTraceContains: 'test/download_tests.dart', + matcher: isA().having( + (e) => e.response!.data, + 'data', + isNull, + ), + ), + ); + }); + + test( + 'timeout', + () async { + final timeoutMatcher = allOf([ + throwsA(isA()), + throwsA( + predicate( + (e) => e.type == DioExceptionType.receiveTimeout, + ), + ), + ]); + await expectLater( + dio.downloadUri( + Uri.parse('/drip?delay=0&duration=6&numbytes=6').replace( + queryParameters: {'count': '3', 'gap': '2'}, + ), + p.join(tmp.path, 'download_timeout.md'), + options: Options(receiveTimeout: Duration(seconds: 1)), + ), + timeoutMatcher, + ); + + // Throws nothing if it constantly gets response bytes. + await dio.download( + 'https://github.com/cfug/flutter.cn/archive/refs/heads/main.zip', + p.join(tmp.path, 'main.zip'), + options: Options(receiveTimeout: Duration(seconds: 1)), + ); + }, + // The download of the main.zip file can be slow, + // so we need to increase the timeout. + timeout: Timeout(Duration(minutes: 1)), + ); + + test('delete on error', () async { + final savePath = p.join(tmp.path, 'delete_on_error.txt'); + final f = File(savePath)..createSync(recursive: true); + expect(f.existsSync(), isTrue); + + await expectLater( + dio.download( + '/drip?delay=0&duration=5', + savePath, + deleteOnError: true, + onReceiveProgress: (count, total) => throw AssertionError(), + ), + throwsDioException( + DioExceptionType.unknown, + stackTraceContains: 'test/download_tests.dart', + matcher: isA().having( + (e) => e.error, + 'error', + isA(), + ), + ), + ); + + await Future.delayed(const Duration(milliseconds: 100)); + expect(f.existsSync(), isFalse); + }); + + test('delete on cancel', () async { + final savePath = p.join(tmp.path, 'delete_on_cancel.md'); + final f = File(savePath)..createSync(recursive: true); + expect(f.existsSync(), isTrue); + + final cancelToken = CancelToken(); + + await expectLater( + dio.download( + '/bytes/5000', + savePath, + deleteOnError: true, + cancelToken: cancelToken, + onReceiveProgress: (count, total) => cancelToken.cancel(), + ), + throwsDioException( + DioExceptionType.cancel, + stackTraceContains: 'test/download_tests.dart', + ), + ); + + await Future.delayed(const Duration(milliseconds: 100)); + expect(f.existsSync(), isFalse); + }); + + test('cancel download mid stream', () async { + const savePath = 'test/download/_test.md'; + final f = File(savePath)..createSync(recursive: true); + expect(f.existsSync(), isTrue); + + final cancelToken = CancelToken(); + final dio = Dio()..options.baseUrl = httpbunBaseUrl; + + await expectLater( + dio.download( + '/bytes/10000', + savePath, + cancelToken: cancelToken, + deleteOnError: true, + onReceiveProgress: (c, t) { + if (c > 5000) { + cancelToken.cancel(); + } + }, + ), + throwsDioException( + DioExceptionType.cancel, + stackTraceContains: 'test/download_tests.dart', + ), + ); + + await Future.delayed(const Duration(milliseconds: 100)); + expect(f.existsSync(), isFalse); + }); + + test('`savePath` types', () async { + final testPath = p.join(tmp.path, 'savePath.txt'); + + await expectLater( + dio.download( + '/bytes/5000', + testPath, + ), + completes, + ); + await expectLater( + dio.download( + '/bytes/5000', + (headers) => testPath, + ), + completes, + ); + await expectLater( + dio.download( + '/bytes/5000', + (headers) async => testPath, + ), + completes, + ); + }); + }, + testOn: 'vm', + ); +} diff --git a/dio_test/lib/src/test/headers_tests.dart b/dio_test/lib/src/test/headers_tests.dart index 46298b742..29ea34339 100644 --- a/dio_test/lib/src/test/headers_tests.dart +++ b/dio_test/lib/src/test/headers_tests.dart @@ -49,5 +49,142 @@ void headerTests( expect(content, contains('Numkey: 2')); expect(content, contains('Booleankey: false')); }); + + test( + 'headers are kept after redirects', + () async { + dio.options.headers.putIfAbsent('x-test-base', () => 'test-base'); + + final response = await dio.get( + '/redirect/3', + options: Options(headers: {'x-test-header': 'test-value'}), + ); + expect(response.isRedirect, isTrue); + // The returned headers are uppercased by the server. + expect( + response.data['headers']['X-Test-Base'], + equals('test-base'), + ); + expect( + response.data['headers']['X-Test-Header'], + equals('test-value'), + ); + // The sent headers are still lowercase. + expect( + response.requestOptions.headers['x-test-base'], + equals('test-base'), + ); + expect( + response.requestOptions.headers['x-test-header'], + equals('test-value'), + ); + }, + testOn: 'vm', + ); + + test('default content-type', () async { + final r1 = await dio.get('/get'); + expect( + r1.requestOptions.headers[Headers.contentTypeHeader], + null, + ); + + final r2 = await dio.get( + '/get', + options: Options(contentType: Headers.jsonContentType), + ); + expect( + r2.requestOptions.headers[Headers.contentTypeHeader], + Headers.jsonContentType, + ); + + final r3 = await dio.get( + '/get', + options: Options( + headers: {Headers.contentTypeHeader: Headers.jsonContentType}, + ), + ); + expect( + r3.requestOptions.headers[Headers.contentTypeHeader], + Headers.jsonContentType, + ); + + final r4 = await dio.post('/post', data: ''); + expect( + r4.requestOptions.headers[Headers.contentTypeHeader], + Headers.jsonContentType, + ); + + final r5 = await dio.get( + '/get', + options: Options( + // Final result should respect this. + contentType: Headers.textPlainContentType, + // Rather than this. + headers: { + Headers.contentTypeHeader: Headers.formUrlEncodedContentType + }, + ), + ); + expect( + r5.requestOptions.headers[Headers.contentTypeHeader], + Headers.textPlainContentType, + ); + + final r6 = await dio.get( + '/get', + data: '', + options: Options( + contentType: Headers.formUrlEncodedContentType, + headers: {Headers.contentTypeHeader: Headers.jsonContentType}, + ), + ); + expect( + r6.requestOptions.headers[Headers.contentTypeHeader], + Headers.formUrlEncodedContentType, + ); + + // Update the base option. + dio.options.contentType = Headers.textPlainContentType; + final r7 = await dio.get('/get'); + expect( + r7.requestOptions.headers[Headers.contentTypeHeader], + Headers.textPlainContentType, + ); + + final r8 = await dio.get( + '/get', + options: Options(contentType: Headers.jsonContentType), + ); + expect( + r8.requestOptions.headers[Headers.contentTypeHeader], + Headers.jsonContentType, + ); + + final r9 = await dio.get( + '/get', + options: Options( + headers: {Headers.contentTypeHeader: Headers.jsonContentType}, + ), + ); + expect( + r9.requestOptions.headers[Headers.contentTypeHeader], + Headers.jsonContentType, + ); + + final r10 = await dio.post('/post', data: FormData()); + expect( + r10.requestOptions.contentType, + startsWith(Headers.multipartFormDataContentType), + ); + + // Regression: https://github.com/cfug/dio/issues/1834 + final r11 = await dio.get('/payload'); + expect(r11.data, ''); + final r12 = await dio.get('/payload'); + expect(r12.data, null); + final r13 = await dio.get>('/payload'); + expect(r13.data, null); + }); }); } diff --git a/dio_test/lib/src/test/status_code_tests.dart b/dio_test/lib/src/test/status_code_tests.dart index 4c9682293..8785a59bb 100644 --- a/dio_test/lib/src/test/status_code_tests.dart +++ b/dio_test/lib/src/test/status_code_tests.dart @@ -18,11 +18,13 @@ void statusCodeTests( dio.get('/status/$code'), throwsDioException( DioExceptionType.badResponse, - stackTraceContains: kIsWeb ? null : 'test/status_code_tests.dart', + stackTraceContains: kIsWeb + ? 'test/test_suite_test.dart' + : 'test/status_code_tests.dart', matcher: isA().having( (e) => e.response!.statusCode, 'statusCode', - equals(code), + code, ), ), ); @@ -39,11 +41,13 @@ void statusCodeTests( ), throwsDioException( DioExceptionType.badResponse, - stackTraceContains: kIsWeb ? null : 'test/status_code_tests.dart', + stackTraceContains: kIsWeb + ? 'test/test_suite_test.dart' + : 'test/status_code_tests.dart', matcher: isA().having( (e) => e.response!.statusCode, 'statusCode', - equals(200), + 200, ), ), ); diff --git a/dio_test/lib/src/test/suite.dart b/dio_test/lib/src/test/suite.dart index b5e88a55a..c17211eda 100644 --- a/dio_test/lib/src/test/suite.dart +++ b/dio_test/lib/src/test/suite.dart @@ -7,14 +7,17 @@ typedef TestSuiteFunction = void Function( const _tests = [ basicTests, + cancellationTests, corsTests, - downloadStreamTests, + downloadTests, headerTests, httpMethodTests, parameterTests, redirectTests, statusCodeTests, timeoutTests, + uploadTests, + urlEncodedTests, ]; void dioAdapterTestSuite( diff --git a/dio/test/upload_test.dart b/dio_test/lib/src/test/upload_tests.dart similarity index 53% rename from dio/test/upload_test.dart rename to dio_test/lib/src/test/upload_tests.dart index b4055ccb0..3d5d964ee 100644 --- a/dio/test/upload_test.dart +++ b/dio_test/lib/src/test/upload_tests.dart @@ -5,13 +5,16 @@ import 'dart:typed_data'; import 'package:dio/dio.dart'; import 'package:dio_test/util.dart'; +import 'package:path/path.dart' as p; import 'package:test/test.dart'; -void main() { +void uploadTests( + Dio Function(String baseUrl) create, +) { late Dio dio; setUp(() { - dio = Dio()..options.baseUrl = httpbunBaseUrl; + dio = create(httpbunBaseUrl); }); test('Uint8List should not be transformed', () async { @@ -56,7 +59,13 @@ void main() { test( 'file stream', () async { - final f = File('test/mock/flutter.png'); + final tmp = Directory.systemTemp.createTempSync('dio_test_'); + addTearDown(() => tmp.deleteSync(recursive: true)); + + final f = File(p.join(tmp.path, 'flutter.png')); + f.createSync(exclusive: false); + f.writeAsBytesSync(base64Decode(_flutterLogPngBase64)); + final contentLength = f.lengthSync(); final r = await dio.put( '/put', @@ -64,7 +73,7 @@ void main() { options: Options( contentType: 'image/png', headers: { - Headers.contentLengthHeader: contentLength, // set content-length + Headers.contentLengthHeader: contentLength, }, ), ); @@ -79,7 +88,13 @@ void main() { test( 'file stream', () async { - final f = File('test/mock/flutter.png'); + final tmp = Directory.systemTemp.createTempSync('dio_test_'); + addTearDown(() => tmp.deleteSync(recursive: true)); + + final f = File(p.join(tmp.path, 'flutter.png')); + f.createSync(exclusive: false); + f.writeAsBytesSync(base64Decode(_flutterLogPngBase64)); + final contentLength = f.lengthSync(); final r = await dio.put( '/put', @@ -87,7 +102,7 @@ void main() { options: Options( contentType: 'image/png', headers: { - Headers.contentLengthHeader: contentLength, // set content-length + Headers.contentLengthHeader: contentLength, }, ), ); @@ -134,3 +149,6 @@ class _TestTransformer extends BackgroundTransformer { return super.transformRequest(options); } } + +const _flutterLogPngBase64 = + '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'; diff --git a/dio/test/url_encoded_test.dart b/dio_test/lib/src/test/url_encoded_tests.dart similarity index 86% rename from dio/test/url_encoded_test.dart rename to dio_test/lib/src/test/url_encoded_tests.dart index dc47f443c..b6a8a5dbf 100644 --- a/dio/test/url_encoded_test.dart +++ b/dio_test/lib/src/test/url_encoded_tests.dart @@ -1,9 +1,16 @@ import 'package:dio/dio.dart'; +import 'package:dio_test/util.dart'; import 'package:test/test.dart'; -import 'mock/adapters.dart'; +void urlEncodedTests( + Dio Function(String baseUrl) create, +) { + late Dio dio; + + setUp(() { + dio = create(httpbunBaseUrl); + }); -void main() async { group('x-www-url-encoded', () { test('posts maps correctly', () async { final data = { @@ -22,12 +29,8 @@ void main() async { }, }; - final dio = Dio() - ..options.baseUrl = EchoAdapter.mockBase - ..httpClientAdapter = EchoAdapter(); - final response = await dio.post( - '/post', + '/payload', data: data, options: Options( contentType: Headers.formUrlEncodedContentType, diff --git a/dio_test/lib/tests.dart b/dio_test/lib/tests.dart index faa24198d..2825751e3 100644 --- a/dio_test/lib/tests.dart +++ b/dio_test/lib/tests.dart @@ -1,6 +1,7 @@ export 'src/test/basic_tests.dart'; +export 'src/test/cancellation_tests.dart'; export 'src/test/cors_tests.dart'; -export 'src/test/download_stream_tests.dart'; +export 'src/test/download_tests.dart'; export 'src/test/headers_tests.dart'; export 'src/test/http_method_tests.dart'; export 'src/test/parameter_tests.dart'; @@ -8,3 +9,5 @@ export 'src/test/redirect_tests.dart'; export 'src/test/status_code_tests.dart'; export 'src/test/suite.dart'; export 'src/test/timeout_tests.dart'; +export 'src/test/upload_tests.dart'; +export 'src/test/url_encoded_tests.dart';