From 66c7bbfbd784d597e9a10fb2e4777acdfaeef1e6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Klemen=20Tu=C5=A1ar?= Date: Wed, 23 Jul 2025 07:05:23 +0100 Subject: [PATCH 1/7] :test_tube: add more tests --- test/extensions/base_reponse_test.dart | 66 ++- test/extensions/base_request_test.dart | 51 +++ .../extensions/io_streamed_response_test.dart | 160 +++++++ test/extensions/multipart_request_test.dart | 186 +++++++++ test/extensions/streamed_response_test.dart | 159 +++++++ test/http/intercepted_http_test.dart | 389 ++++++++++++++++++ .../http_interceptor_exception_test.dart | 51 +++ test/models/interceptor_contract_test.dart | 221 ++++++++++ test/models/retry_policy_test.dart | 14 + 9 files changed, 1295 insertions(+), 2 deletions(-) create mode 100644 test/extensions/io_streamed_response_test.dart create mode 100644 test/extensions/multipart_request_test.dart create mode 100644 test/extensions/streamed_response_test.dart create mode 100644 test/http/intercepted_http_test.dart create mode 100644 test/models/http_interceptor_exception_test.dart create mode 100644 test/models/interceptor_contract_test.dart diff --git a/test/extensions/base_reponse_test.dart b/test/extensions/base_reponse_test.dart index 2ac27fe..8eb9b38 100644 --- a/test/extensions/base_reponse_test.dart +++ b/test/extensions/base_reponse_test.dart @@ -1,3 +1,6 @@ +import 'dart:convert'; + +import 'package:http/io_client.dart'; import 'package:http_interceptor/http_interceptor.dart'; import 'package:test/test.dart'; @@ -8,8 +11,8 @@ main() { final BaseResponse baseResponse = Response("{'foo': 'bar'}", 200); // Act - final copiedBaseRequest = baseResponse.copyWith(); - final copied = copiedBaseRequest as Response; + final copiedBaseResponse = baseResponse.copyWith(); + final copied = copiedBaseResponse as Response; // Assert final response = baseResponse as Response; @@ -22,5 +25,64 @@ main() { expect( copied.persistentConnection, equals(response.persistentConnection)); }); + + test('IOStreamedResponse is copied from BaseResponse', () { + // Arrange + final testRequest = Request('GET', Uri.parse('https://example.com')); + final testHeaders = {'Content-Type': 'application/json'}; + final testStream = Stream.value(utf8.encode('test data')); + final testStatusCode = 200; + final testContentLength = 9; // 'test data'.length + final testIsRedirect = false; + final testPersistentConnection = true; + final testReasonPhrase = 'OK'; + + final BaseResponse baseResponse = IOStreamedResponse( + testStream, + testStatusCode, + contentLength: testContentLength, + request: testRequest, + headers: testHeaders, + isRedirect: testIsRedirect, + persistentConnection: testPersistentConnection, + reasonPhrase: testReasonPhrase, + ); + + // Act + final copiedBaseResponse = baseResponse.copyWith(); + + // Assert + final copied = copiedBaseResponse as IOStreamedResponse; + final response = baseResponse as IOStreamedResponse; + expect(copied.hashCode, isNot(equals(response.hashCode))); + expect(copied.statusCode, equals(response.statusCode)); + expect(copied.contentLength, equals(response.contentLength)); + expect(copied.request, equals(response.request)); + expect(copied.headers, equals(response.headers)); + expect(copied.isRedirect, equals(response.isRedirect)); + expect(copied.persistentConnection, equals(response.persistentConnection)); + expect(copied.reasonPhrase, equals(response.reasonPhrase)); + }); + + test('throws UnsupportedError for unsupported response type', () { + // Arrange + final unsupportedResponse = _UnsupportedResponse(); + + // Act & Assert + expect( + () => unsupportedResponse.copyWith(), + throwsA(isA().having( + (e) => e.message, + 'message', + 'Cannot copy unsupported type of response _UnsupportedResponse', + )), + ); + }); }); } + + +// Custom response type that doesn't extend any of the supported types +class _UnsupportedResponse extends BaseResponse { + _UnsupportedResponse() : super(200); +} diff --git a/test/extensions/base_request_test.dart b/test/extensions/base_request_test.dart index b75df27..c89b1fd 100644 --- a/test/extensions/base_request_test.dart +++ b/test/extensions/base_request_test.dart @@ -27,5 +27,56 @@ main() { expect(copied.maxRedirects, equals(request.maxRedirects)); expect(copied.persistentConnection, equals(request.persistentConnection)); }); + + test('MultipartRequest is copied from BaseRequest', () { + // Arrange + final testUrl = Uri.parse('https://example.com'); + final testHeaders = {'Content-Type': 'multipart/form-data'}; + final testFields = {'field1': 'value1', 'field2': 'value2'}; + + final MultipartRequest multipartRequest = MultipartRequest('POST', testUrl) + ..headers.addAll(testHeaders) + ..fields.addAll(testFields); + + // Add a test file to the request + final testFileBytes = utf8.encode('test file content'); + final testFile = MultipartFile.fromBytes( + 'file', + testFileBytes, + filename: 'test.txt', + ); + multipartRequest.files.add(testFile); + + // Act + final copied = multipartRequest.copyWith(); + + // Assert + expect(copied.hashCode, isNot(equals(multipartRequest.hashCode))); + expect(copied.url, equals(multipartRequest.url)); + expect(copied.method, equals(multipartRequest.method)); + expect(copied.headers, equals(multipartRequest.headers)); + expect(copied.fields, equals(multipartRequest.fields)); + expect(copied.files.length, equals(multipartRequest.files.length)); + }); + + test('throws UnsupportedError for unsupported request type', () { + // Arrange + final unsupportedRequest = _UnsupportedRequest(); + + // Act & Assert + expect( + () => unsupportedRequest.copyWith(), + throwsA(isA().having( + (e) => e.message, + 'message', + 'Cannot copy unsupported type of request _UnsupportedRequest', + )), + ); + }); }); } + +// Custom request type that doesn't extend any of the supported types +class _UnsupportedRequest extends BaseRequest { + _UnsupportedRequest() : super('GET', Uri.parse('https://example.com')); +} diff --git a/test/extensions/io_streamed_response_test.dart b/test/extensions/io_streamed_response_test.dart new file mode 100644 index 0000000..00d6428 --- /dev/null +++ b/test/extensions/io_streamed_response_test.dart @@ -0,0 +1,160 @@ +import 'dart:convert'; + +import 'package:http/io_client.dart'; +import 'package:http_interceptor/http_interceptor.dart'; +import 'package:test/test.dart'; + +void main() { + group('IOStreamedResponse.copyWith', () { + late IOStreamedResponse response; + final testRequest = Request('GET', Uri.parse('https://example.com')); + final testHeaders = {'Content-Type': 'application/json'}; + final testStream = Stream.value(utf8.encode('test data')); + final testStatusCode = 200; + final testContentLength = 9; // 'test data'.length + final testIsRedirect = false; + final testPersistentConnection = true; + final testReasonPhrase = 'OK'; + + setUp(() { + response = IOStreamedResponse( + testStream, + testStatusCode, + contentLength: testContentLength, + request: testRequest, + headers: testHeaders, + isRedirect: testIsRedirect, + persistentConnection: testPersistentConnection, + reasonPhrase: testReasonPhrase, + ); + }); + + test('creates a copy with the same properties when no parameters are provided', + () { + // Act + final copy = response.copyWith(); + + // Assert + expect(copy.statusCode, equals(testStatusCode)); + expect(copy.contentLength, equals(testContentLength)); + expect(copy.request, equals(testRequest)); + expect(copy.headers, equals(testHeaders)); + expect(copy.isRedirect, equals(testIsRedirect)); + expect(copy.persistentConnection, equals(testPersistentConnection)); + expect(copy.reasonPhrase, equals(testReasonPhrase)); + }); + + test('overrides statusCode when provided', () { + // Arrange + final newStatusCode = 201; + + // Act + final copy = response.copyWith(statusCode: newStatusCode); + + // Assert + expect(copy.statusCode, equals(newStatusCode)); + expect(copy.contentLength, equals(testContentLength)); // Other properties remain the same + }); + + test('overrides contentLength when provided', () { + // Arrange + final newContentLength = 100; + + // Act + final copy = response.copyWith(contentLength: newContentLength); + + // Assert + expect(copy.contentLength, equals(newContentLength)); + expect(copy.statusCode, equals(testStatusCode)); // Other properties remain the same + }); + + test('overrides request when provided', () { + // Arrange + final newRequest = Request('POST', Uri.parse('https://example.org')); + + // Act + final copy = response.copyWith(request: newRequest); + + // Assert + expect(copy.request, equals(newRequest)); + expect(copy.statusCode, equals(testStatusCode)); // Other properties remain the same + }); + + test('overrides headers when provided', () { + // Arrange + final newHeaders = {'Authorization': 'Bearer token'}; + + // Act + final copy = response.copyWith(headers: newHeaders); + + // Assert + expect(copy.headers, equals(newHeaders)); + expect(copy.statusCode, equals(testStatusCode)); // Other properties remain the same + }); + + test('overrides isRedirect when provided', () { + // Act + final copy = response.copyWith(isRedirect: !testIsRedirect); + + // Assert + expect(copy.isRedirect, equals(!testIsRedirect)); + expect(copy.statusCode, equals(testStatusCode)); // Other properties remain the same + }); + + test('overrides persistentConnection when provided', () { + // Act + final copy = response.copyWith(persistentConnection: !testPersistentConnection); + + // Assert + expect(copy.persistentConnection, equals(!testPersistentConnection)); + expect(copy.statusCode, equals(testStatusCode)); // Other properties remain the same + }); + + test('overrides reasonPhrase when provided', () { + // Arrange + final newReasonPhrase = 'Created'; + + // Act + final copy = response.copyWith(reasonPhrase: newReasonPhrase); + + // Assert + expect(copy.reasonPhrase, equals(newReasonPhrase)); + expect(copy.statusCode, equals(testStatusCode)); // Other properties remain the same + }); + + test('overrides stream when provided', () async { + // Arrange + final newData = utf8.encode('new data'); + final newStream = Stream.value(newData); + + // Act + final copy = response.copyWith(stream: newStream); + + // Assert + final copyData = await copy.stream.toList(); + final flattenedCopyData = copyData.expand((x) => x).toList(); + expect(flattenedCopyData, equals(newData)); + expect(copy.statusCode, equals(testStatusCode)); // Other properties remain the same + }); + + test('can override multiple properties at once', () { + // Arrange + final newStatusCode = 201; + final newHeaders = {'Authorization': 'Bearer token'}; + final newReasonPhrase = 'Created'; + + // Act + final copy = response.copyWith( + statusCode: newStatusCode, + headers: newHeaders, + reasonPhrase: newReasonPhrase, + ); + + // Assert + expect(copy.statusCode, equals(newStatusCode)); + expect(copy.headers, equals(newHeaders)); + expect(copy.reasonPhrase, equals(newReasonPhrase)); + expect(copy.contentLength, equals(testContentLength)); // Unchanged properties remain the same + }); + }); +} diff --git a/test/extensions/multipart_request_test.dart b/test/extensions/multipart_request_test.dart new file mode 100644 index 0000000..366cd1d --- /dev/null +++ b/test/extensions/multipart_request_test.dart @@ -0,0 +1,186 @@ +import 'dart:convert'; + +import 'package:http_interceptor/http_interceptor.dart'; +import 'package:test/test.dart'; + +void main() { + group('MultipartRequest.copyWith', () { + late MultipartRequest request; + final testUrl = Uri.parse('https://example.com'); + final testHeaders = {'Content-Type': 'multipart/form-data'}; + final testFields = {'field1': 'value1', 'field2': 'value2'}; + + setUp(() { + request = MultipartRequest('POST', testUrl) + ..headers.addAll(testHeaders) + ..fields.addAll(testFields); + + // Add a test file to the request + final testFileBytes = utf8.encode('test file content'); + final testFile = MultipartFile.fromBytes( + 'file', + testFileBytes, + filename: 'test.txt', + ); + request.files.add(testFile); + }); + + test( + 'creates a copy with the same properties when no parameters are provided', + () { + // Act + final copy = request.copyWith(); + + // Assert + expect(copy.method, equals(request.method)); + expect(copy.url, equals(request.url)); + expect(copy.headers, equals(request.headers)); + expect(copy.fields, equals(request.fields)); + expect(copy.files.length, equals(request.files.length)); + expect(copy.followRedirects, equals(request.followRedirects)); + expect(copy.maxRedirects, equals(request.maxRedirects)); + expect(copy.persistentConnection, equals(request.persistentConnection)); + }); + + test('overrides method when provided', () { + // Act + final copy = request.copyWith(method: HttpMethod.PUT); + + // Assert + expect(copy.method, equals('PUT')); + expect(copy.url, equals(request.url)); // Other properties remain the same + }); + + test('overrides url when provided', () { + // Arrange + final newUrl = Uri.parse('https://example.org'); + + // Act + final copy = request.copyWith(url: newUrl); + + // Assert + expect(copy.url, equals(newUrl)); + expect(copy.method, + equals(request.method)); // Other properties remain the same + }); + + test('overrides headers when provided', () { + // Arrange + final newHeaders = {'Authorization': 'Bearer token'}; + + // Act + final copy = request.copyWith(headers: newHeaders); + + // Assert + expect(copy.headers, equals(newHeaders)); + expect(copy.method, + equals(request.method)); // Other properties remain the same + }); + + test('overrides fields when provided', () { + // Arrange + final newFields = {'newField': 'newValue'}; + + // Act + final copy = request.copyWith(fields: newFields); + + // Assert + expect(copy.fields, equals(newFields)); + expect(copy.method, + equals(request.method)); // Other properties remain the same + }); + + test('copies files from original request (ignores files parameter)', () { + // Arrange + final newFileBytes = utf8.encode('new file content'); + final newFile = MultipartFile.fromBytes( + 'newFile', + newFileBytes, + filename: 'new.txt', + ); + + // Act + final copy = request.copyWith(files: [newFile]); + + // Assert + // The implementation ignores the files parameter and always copies from the original + expect(copy.files.length, equals(request.files.length)); + expect(copy.files.first.field, equals('file')); // Original file field + expect( + copy.files.first.filename, equals('test.txt')); // Original filename + expect(copy.method, + equals(request.method)); // Other properties remain the same + }); + + test('sets followRedirects on original request (bug)', () { + // Arrange + final originalValue = request.followRedirects; + final originalRequest = request; // Keep reference to original + + // Act + final copy = request.copyWith(followRedirects: !originalValue); + + // Assert + // The implementation incorrectly sets this on the original request + expect(originalRequest.followRedirects, equals(!originalValue)); + // The copy has the default value + expect(copy.followRedirects, equals(true)); + expect(copy.method, + equals(request.method)); // Other properties remain the same + }); + + test('sets maxRedirects on original request (bug)', () { + // Arrange + final newMaxRedirects = 10; + final originalRequest = request; // Keep reference to original + + // Act + final copy = request.copyWith(maxRedirects: newMaxRedirects); + + // Assert + // The implementation incorrectly sets this on the original request + expect(originalRequest.maxRedirects, equals(newMaxRedirects)); + // The copy has the default value + expect(copy.maxRedirects, equals(5)); + expect(copy.method, + equals(request.method)); // Other properties remain the same + }); + + test('sets persistentConnection on original request (bug)', () { + // Arrange + final originalValue = request.persistentConnection; + final originalRequest = request; // Keep reference to original + + // Act + final copy = request.copyWith(persistentConnection: !originalValue); + + // Assert + // The implementation incorrectly sets this on the original request + expect(originalRequest.persistentConnection, equals(!originalValue)); + // The copy has the default value + expect(copy.persistentConnection, equals(true)); + expect(copy.method, + equals(request.method)); // Other properties remain the same + }); + + test('can override multiple properties at once', () { + // Arrange + final newUrl = Uri.parse('https://example.org'); + final newHeaders = {'Authorization': 'Bearer token'}; + + // Act + final copy = request.copyWith( + method: HttpMethod.PUT, + url: newUrl, + headers: newHeaders, + ); + + // Assert + expect(copy.method, equals('PUT')); + expect(copy.url, equals(newUrl)); + expect(copy.headers, equals(newHeaders)); + expect(copy.fields, + equals(request.fields)); // Unchanged properties remain the same + }); + }); +} diff --git a/test/extensions/streamed_response_test.dart b/test/extensions/streamed_response_test.dart new file mode 100644 index 0000000..1e63e55 --- /dev/null +++ b/test/extensions/streamed_response_test.dart @@ -0,0 +1,159 @@ +import 'dart:convert'; + +import 'package:http_interceptor/http_interceptor.dart'; +import 'package:test/test.dart'; + +void main() { + group('StreamedResponse.copyWith', () { + late StreamedResponse response; + final testRequest = Request('GET', Uri.parse('https://example.com')); + final testHeaders = {'Content-Type': 'application/json'}; + final testStream = Stream.value(utf8.encode('test data')); + final testStatusCode = 200; + final testContentLength = 9; // 'test data'.length + final testIsRedirect = false; + final testPersistentConnection = true; + final testReasonPhrase = 'OK'; + + setUp(() { + response = StreamedResponse( + testStream, + testStatusCode, + contentLength: testContentLength, + request: testRequest, + headers: testHeaders, + isRedirect: testIsRedirect, + persistentConnection: testPersistentConnection, + reasonPhrase: testReasonPhrase, + ); + }); + + test('creates a copy with the same properties when no parameters are provided', + () { + // Act + final copy = response.copyWith(); + + // Assert + expect(copy.statusCode, equals(testStatusCode)); + expect(copy.contentLength, equals(testContentLength)); + expect(copy.request, equals(testRequest)); + expect(copy.headers, equals(testHeaders)); + expect(copy.isRedirect, equals(testIsRedirect)); + expect(copy.persistentConnection, equals(testPersistentConnection)); + expect(copy.reasonPhrase, equals(testReasonPhrase)); + }); + + test('overrides statusCode when provided', () { + // Arrange + final newStatusCode = 201; + + // Act + final copy = response.copyWith(statusCode: newStatusCode); + + // Assert + expect(copy.statusCode, equals(newStatusCode)); + expect(copy.contentLength, equals(testContentLength)); // Other properties remain the same + }); + + test('overrides contentLength when provided', () { + // Arrange + final newContentLength = 100; + + // Act + final copy = response.copyWith(contentLength: newContentLength); + + // Assert + expect(copy.contentLength, equals(newContentLength)); + expect(copy.statusCode, equals(testStatusCode)); // Other properties remain the same + }); + + test('overrides request when provided', () { + // Arrange + final newRequest = Request('POST', Uri.parse('https://example.org')); + + // Act + final copy = response.copyWith(request: newRequest); + + // Assert + expect(copy.request, equals(newRequest)); + expect(copy.statusCode, equals(testStatusCode)); // Other properties remain the same + }); + + test('overrides headers when provided', () { + // Arrange + final newHeaders = {'Authorization': 'Bearer token'}; + + // Act + final copy = response.copyWith(headers: newHeaders); + + // Assert + expect(copy.headers, equals(newHeaders)); + expect(copy.statusCode, equals(testStatusCode)); // Other properties remain the same + }); + + test('overrides isRedirect when provided', () { + // Act + final copy = response.copyWith(isRedirect: !testIsRedirect); + + // Assert + expect(copy.isRedirect, equals(!testIsRedirect)); + expect(copy.statusCode, equals(testStatusCode)); // Other properties remain the same + }); + + test('overrides persistentConnection when provided', () { + // Act + final copy = response.copyWith(persistentConnection: !testPersistentConnection); + + // Assert + expect(copy.persistentConnection, equals(!testPersistentConnection)); + expect(copy.statusCode, equals(testStatusCode)); // Other properties remain the same + }); + + test('overrides reasonPhrase when provided', () { + // Arrange + final newReasonPhrase = 'Created'; + + // Act + final copy = response.copyWith(reasonPhrase: newReasonPhrase); + + // Assert + expect(copy.reasonPhrase, equals(newReasonPhrase)); + expect(copy.statusCode, equals(testStatusCode)); // Other properties remain the same + }); + + test('overrides stream when provided', () async { + // Arrange + final newData = utf8.encode('new data'); + final newStream = Stream.value(newData); + + // Act + final copy = response.copyWith(stream: newStream); + + // Assert + final copyData = await copy.stream.toList(); + final flattenedCopyData = copyData.expand((x) => x).toList(); + expect(flattenedCopyData, equals(newData)); + expect(copy.statusCode, equals(testStatusCode)); // Other properties remain the same + }); + + test('can override multiple properties at once', () { + // Arrange + final newStatusCode = 201; + final newHeaders = {'Authorization': 'Bearer token'}; + final newReasonPhrase = 'Created'; + + // Act + final copy = response.copyWith( + statusCode: newStatusCode, + headers: newHeaders, + reasonPhrase: newReasonPhrase, + ); + + // Assert + expect(copy.statusCode, equals(newStatusCode)); + expect(copy.headers, equals(newHeaders)); + expect(copy.reasonPhrase, equals(newReasonPhrase)); + expect(copy.contentLength, equals(testContentLength)); // Unchanged properties remain the same + }); + }); +} diff --git a/test/http/intercepted_http_test.dart b/test/http/intercepted_http_test.dart new file mode 100644 index 0000000..40c66a5 --- /dev/null +++ b/test/http/intercepted_http_test.dart @@ -0,0 +1,389 @@ +import 'dart:async'; +import 'dart:convert'; +import 'dart:typed_data'; + +import 'package:http/http.dart'; +import 'package:http_interceptor/http_interceptor.dart'; +import 'package:test/test.dart'; + +void main() { + group('InterceptedHttp', () { + late MockInterceptor mockInterceptor; + late MockClient mockClient; + late InterceptedHttp http; + + setUp(() { + mockInterceptor = MockInterceptor(); + mockClient = MockClient(); + http = InterceptedHttp.build( + interceptors: [mockInterceptor], + client: mockClient, + ); + }); + + group('build factory method', () { + test('creates instance with provided interceptors', () { + // Arrange + final interceptor1 = MockInterceptor(); + final interceptor2 = MockInterceptor(); + + // Act + final http = InterceptedHttp.build( + interceptors: [interceptor1, interceptor2], + ); + + // Assert + expect(http.interceptors, contains(interceptor1)); + expect(http.interceptors, contains(interceptor2)); + expect(http.interceptors.length, 2); + }); + + test('creates instance with provided timeout', () { + // Arrange + final timeout = Duration(seconds: 30); + + // Act + final http = InterceptedHttp.build( + interceptors: [mockInterceptor], + requestTimeout: timeout, + ); + + // Assert + expect(http.requestTimeout, equals(timeout)); + }); + + test('creates instance with provided retry policy', () { + // Arrange + final retryPolicy = MockRetryPolicy(); + + // Act + final http = InterceptedHttp.build( + interceptors: [mockInterceptor], + retryPolicy: retryPolicy, + ); + + // Assert + expect(http.retryPolicy, equals(retryPolicy)); + }); + + test('creates instance with provided client', () { + // Arrange & Act + final http = InterceptedHttp.build( + interceptors: [mockInterceptor], + client: mockClient, + ); + + // Assert + expect(http.client, equals(mockClient)); + }); + }); + + group('HTTP methods', () { + setUp(() { + mockClient.response = Response('{"success": true}', 200); + }); + + test('GET method creates a new client and closes it after use', () async { + // Arrange + final url = Uri.parse('https://example.com'); + final headers = {'Authorization': 'Bearer token'}; + final params = {'query': 'test'}; + + // Act + await http.get(url, headers: headers, params: params); + + // Assert + expect(mockClient.closeCalled, true); + expect(mockClient.requestCount, 1); + expect(mockClient.lastRequest?.method, 'GET'); + expect(mockClient.lastRequest?.url.toString(), contains('query=test')); + expect( + mockClient.lastRequest?.headers['Authorization'], 'Bearer token'); + }); + + test('POST method creates a new client and closes it after use', + () async { + // Arrange + final url = Uri.parse('https://example.com'); + final headers = {'Content-Type': 'application/json'}; + final body = '{"name": "test"}'; + + // Act + await http.post(url, headers: headers, body: body); + + // Assert + expect(mockClient.closeCalled, true); + expect(mockClient.requestCount, 1); + expect(mockClient.lastRequest?.method, 'POST'); + expect(mockClient.lastRequest?.headers['Content-Type'], + contains('application/json')); + }); + + test('PUT method creates a new client and closes it after use', () async { + // Arrange + final url = Uri.parse('https://example.com'); + final headers = {'Content-Type': 'application/json'}; + final body = '{"name": "test"}'; + + // Act + await http.put(url, headers: headers, body: body); + + // Assert + expect(mockClient.closeCalled, true); + expect(mockClient.requestCount, 1); + expect(mockClient.lastRequest?.method, 'PUT'); + expect(mockClient.lastRequest?.headers['Content-Type'], + contains('application/json')); + }); + + test('PATCH method creates a new client and closes it after use', + () async { + // Arrange + final url = Uri.parse('https://example.com'); + final headers = {'Content-Type': 'application/json'}; + final body = '{"name": "test"}'; + + // Act + await http.patch(url, headers: headers, body: body); + + // Assert + expect(mockClient.closeCalled, true); + expect(mockClient.requestCount, 1); + expect(mockClient.lastRequest?.method, 'PATCH'); + expect(mockClient.lastRequest?.headers['Content-Type'], + contains('application/json')); + }); + + test('DELETE method creates a new client and closes it after use', + () async { + // Arrange + final url = Uri.parse('https://example.com'); + final headers = {'Authorization': 'Bearer token'}; + + // Act + await http.delete(url, headers: headers); + + // Assert + expect(mockClient.closeCalled, true); + expect(mockClient.requestCount, 1); + expect(mockClient.lastRequest?.method, 'DELETE'); + expect( + mockClient.lastRequest?.headers['Authorization'], 'Bearer token'); + }); + + test('HEAD method creates a new client and closes it after use', + () async { + // Arrange + final url = Uri.parse('https://example.com'); + final headers = {'Authorization': 'Bearer token'}; + + // Act + await http.head(url, headers: headers); + + // Assert + expect(mockClient.closeCalled, true); + expect(mockClient.requestCount, 1); + expect(mockClient.lastRequest?.method, 'HEAD'); + expect( + mockClient.lastRequest?.headers['Authorization'], 'Bearer token'); + }); + + test('read method returns response body as string', () async { + // Arrange + final url = Uri.parse('https://example.com'); + mockClient.response = Response('response body', 200); + + // Act + final result = await http.read(url); + + // Assert + expect(result, 'response body'); + expect(mockClient.closeCalled, true); + }); + + test('readBytes method returns response body as bytes', () async { + // Arrange + final url = Uri.parse('https://example.com'); + final bytes = utf8.encode('response body'); + // Create a response with the body text + mockClient.response = Response('response body', 200); + // Override the readBytes method to return our bytes + mockClient.bytesToReturn = bytes; + + // Act + final result = await http.readBytes(url); + + // Assert + expect(result, bytes); + expect(mockClient.closeCalled, true); + }); + }); + + group('error handling', () { + test('client is closed even when request throws an exception', () async { + // Arrange + final url = Uri.parse('https://example.com'); + mockClient.shouldThrow = true; + mockClient.exceptionToThrow = Exception('Network error'); + + // Act & Assert + await expectLater( + () => http.get(url), + throwsException, + ); + expect(mockClient.closeCalled, true); + }); + }); + + test('_withClient creates a new client with the same parameters', () async { + // Arrange + final timeout = Duration(seconds: 30); + final retryPolicy = MockRetryPolicy(); + final onTimeout = () => StreamedResponse(Stream.value([]), 408); + + http = InterceptedHttp.build( + interceptors: [mockInterceptor], + client: mockClient, + requestTimeout: timeout, + retryPolicy: retryPolicy, + onRequestTimeout: onTimeout, + ); + + // Act + await http.get(Uri.parse('https://example.com')); + + // Assert + // We can't directly check the internal client, but we can verify + // the request was made with the mockClient + expect(mockClient.requestCount, 1); + expect(mockClient.closeCalled, true); + }); + }); +} + +class MockInterceptor implements InterceptorContract { + @override + Future interceptRequest({required BaseRequest request}) async { + return request; + } + + @override + Future interceptResponse( + {required BaseResponse response}) async { + return response; + } + + @override + Future shouldInterceptRequest({required BaseRequest request}) async { + return true; + } + + @override + Future shouldInterceptResponse({required BaseResponse response}) async { + return true; + } + + @override + Future shouldInterceptError({ + BaseRequest? request, + BaseResponse? response, + }) async { + return true; + } + + @override + Future interceptError({ + BaseRequest? request, + BaseResponse? response, + Exception? error, + StackTrace? stackTrace, + }) async { + // Do nothing + } +} + +class MockClient implements Client { + int requestCount = 0; + bool closeCalled = false; + bool shouldThrow = false; + Exception exceptionToThrow = Exception('Test exception'); + Response response = Response('', 200); + BaseRequest? lastRequest; + Uint8List bytesToReturn = Uint8List(0); + + @override + Future send(BaseRequest request) async { + requestCount++; + lastRequest = request; + + if (shouldThrow) { + throw exceptionToThrow; + } + + // Convert Response to StreamedResponse + return StreamedResponse( + Stream.value(utf8.encode(response.body)), + response.statusCode, + headers: response.headers, + reasonPhrase: response.reasonPhrase, + isRedirect: response.isRedirect, + persistentConnection: response.persistentConnection, + request: response.request, + ); + } + + @override + void close() { + closeCalled = true; + } + + // Implement required methods from Client interface + @override + Future get(Uri url, {Map? headers}) async { + return response; + } + + @override + Future post(Uri url, + {Map? headers, Object? body, Encoding? encoding}) async { + return response; + } + + @override + Future put(Uri url, + {Map? headers, Object? body, Encoding? encoding}) async { + return response; + } + + @override + Future patch(Uri url, + {Map? headers, Object? body, Encoding? encoding}) async { + return response; + } + + @override + Future delete(Uri url, + {Map? headers, Object? body, Encoding? encoding}) async { + return response; + } + + @override + Future head(Uri url, {Map? headers}) async { + return response; + } + + @override + Future read(Uri url, {Map? headers}) async { + return response.body; + } + + @override + Future readBytes(Uri url, {Map? headers}) async { + return bytesToReturn; + } +} + +class MockRetryPolicy extends RetryPolicy { + @override + int get maxRetryAttempts => 1; +} diff --git a/test/models/http_interceptor_exception_test.dart b/test/models/http_interceptor_exception_test.dart new file mode 100644 index 0000000..dfc68b0 --- /dev/null +++ b/test/models/http_interceptor_exception_test.dart @@ -0,0 +1,51 @@ +import 'package:http_interceptor/http_interceptor.dart'; +import 'package:test/test.dart'; + +void main() { + group('HttpInterceptorException', () { + test('can be instantiated without a message', () { + // Act + final exception = HttpInterceptorException(); + + // Assert + expect(exception, isA()); + expect(exception.message, isNull); + }); + + test('can be instantiated with a message', () { + // Arrange + const message = 'Test error message'; + + // Act + final exception = HttpInterceptorException(message); + + // Assert + expect(exception, isA()); + expect(exception.message, equals(message)); + }); + + test('toString() returns "Exception" when message is null', () { + // Arrange + final exception = HttpInterceptorException(); + + // Act + final result = exception.toString(); + + // Assert + expect(result, equals('Exception')); + }); + + test('toString() returns "Exception: message" when message is provided', + () { + // Arrange + const message = 'Test error message'; + final exception = HttpInterceptorException(message); + + // Act + final result = exception.toString(); + + // Assert + expect(result, equals('Exception: $message')); + }); + }); +} diff --git a/test/models/interceptor_contract_test.dart b/test/models/interceptor_contract_test.dart new file mode 100644 index 0000000..2f53f1b --- /dev/null +++ b/test/models/interceptor_contract_test.dart @@ -0,0 +1,221 @@ +import 'package:http/http.dart'; +import 'package:http_interceptor/http_interceptor.dart'; +import 'package:test/test.dart'; + +void main() { + group('InterceptorContract', () { + late TestInterceptor interceptor; + late MinimalInterceptor minimalInterceptor; + late Request testRequest; + late Response testResponse; + late Exception testException; + late StackTrace testStackTrace; + + setUp(() { + interceptor = TestInterceptor(); + minimalInterceptor = MinimalInterceptor(); + testRequest = Request('GET', Uri.parse('https://example.com')); + testResponse = Response('Test body', 200); + testException = Exception('Test exception'); + testStackTrace = StackTrace.current; + }); + + group('default implementations', () { + test('shouldInterceptRequest returns true by default', () async { + // Act - use the minimal implementation that doesn't override the default + final result = await minimalInterceptor.shouldInterceptRequest( + request: testRequest); + + // Assert + expect(result, isTrue); + }); + + test('shouldInterceptResponse returns true by default', () async { + // Act - use the minimal implementation that doesn't override the default + final result = await minimalInterceptor.shouldInterceptResponse( + response: testResponse); + + // Assert + expect(result, isTrue); + }); + + test('shouldInterceptError returns true by default', () async { + // Act - use the minimal implementation that doesn't override the default + final result = await minimalInterceptor.shouldInterceptError( + request: testRequest, + response: testResponse, + ); + + // Assert + expect(result, isTrue); + }); + + test('interceptError has empty default implementation', () async { + // Act & Assert - use the minimal implementation that doesn't override the default + await minimalInterceptor.interceptError( + request: testRequest, + response: testResponse, + error: testException, + stackTrace: testStackTrace, + ); + // No assertion needed - just verifying it doesn't throw + }); + }); + + group('overriding default implementations', () { + test('can override shouldInterceptRequest', () async { + // Arrange + interceptor.shouldInterceptRequestResult = false; + + // Act + final result = + await interceptor.shouldInterceptRequest(request: testRequest); + + // Assert + expect(result, isFalse); + }); + + test('can override shouldInterceptResponse', () async { + // Arrange + interceptor.shouldInterceptResponseResult = false; + + // Act + final result = + await interceptor.shouldInterceptResponse(response: testResponse); + + // Assert + expect(result, isFalse); + }); + + test('can override shouldInterceptError', () async { + // Arrange + interceptor.shouldInterceptErrorResult = false; + + // Act + final result = await interceptor.shouldInterceptError( + request: testRequest, + response: testResponse, + ); + + // Assert + expect(result, isFalse); + }); + + test('can override interceptError', () async { + // Arrange + interceptor.interceptErrorCalled = false; + + // Act + await interceptor.interceptError( + request: testRequest, + response: testResponse, + error: testException, + stackTrace: testStackTrace, + ); + + // Assert + expect(interceptor.interceptErrorCalled, isTrue); + expect(interceptor.lastRequest, equals(testRequest)); + expect(interceptor.lastResponse, equals(testResponse)); + expect(interceptor.lastError, equals(testException)); + expect(interceptor.lastStackTrace, equals(testStackTrace)); + }); + }); + }); +} + +/// A minimal implementation that implements the methods with the same +/// default behavior as in the InterceptorContract abstract class +class MinimalInterceptor implements InterceptorContract { + @override + Future interceptRequest({required BaseRequest request}) async { + return request; + } + + @override + Future interceptResponse( + {required BaseResponse response}) async { + return response; + } + + @override + Future shouldInterceptRequest({required BaseRequest request}) async => + true; + + @override + Future shouldInterceptResponse( + {required BaseResponse response}) async => + true; + + @override + Future shouldInterceptError({ + BaseRequest? request, + BaseResponse? response, + }) async => + true; + + @override + Future interceptError({ + BaseRequest? request, + BaseResponse? response, + Exception? error, + StackTrace? stackTrace, + }) async { + // Default implementation does nothing + } +} + +class TestInterceptor implements InterceptorContract { + bool shouldInterceptRequestResult = true; + bool shouldInterceptResponseResult = true; + bool shouldInterceptErrorResult = true; + bool interceptErrorCalled = false; + + BaseRequest? lastRequest; + BaseResponse? lastResponse; + Exception? lastError; + StackTrace? lastStackTrace; + + @override + Future interceptRequest({required BaseRequest request}) async { + return request; + } + + @override + Future interceptResponse( + {required BaseResponse response}) async { + return response; + } + + @override + Future shouldInterceptRequest({required BaseRequest request}) async { + return shouldInterceptRequestResult; + } + + @override + Future shouldInterceptResponse({required BaseResponse response}) async { + return shouldInterceptResponseResult; + } + + @override + Future shouldInterceptError({ + BaseRequest? request, + BaseResponse? response, + }) async { + return shouldInterceptErrorResult; + } + + @override + Future interceptError({ + BaseRequest? request, + BaseResponse? response, + Exception? error, + StackTrace? stackTrace, + }) async { + interceptErrorCalled = true; + lastRequest = request; + lastResponse = response; + lastError = error; + lastStackTrace = stackTrace; + } +} diff --git a/test/models/retry_policy_test.dart b/test/models/retry_policy_test.dart index 65e0d2e..7131654 100644 --- a/test/models/retry_policy_test.dart +++ b/test/models/retry_policy_test.dart @@ -20,6 +20,14 @@ main() { expect(testObject.maxRetryAttempts, 5); }); + + test("base class default is 1", () { + // Arrange + final policy = MinimalRetryPolicy(); + + // Act & Assert + expect(policy.maxRetryAttempts, 1); + }); }); group("delayRetryAttemptOnException", () { @@ -78,3 +86,9 @@ class TestRetryPolicy extends RetryPolicy { @override int get maxRetryAttempts => internalMaxRetryAttempts; } + +/// A minimal implementation of RetryPolicy that doesn't override any methods +/// Used to test the default implementations in the base class +class MinimalRetryPolicy extends RetryPolicy { + // No overrides - uses all default implementations +} From 9bacb11e25e7aa708c0f9b5371d669d5507a5954 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Klemen=20Tu=C5=A1ar?= Date: Wed, 23 Jul 2025 07:08:22 +0100 Subject: [PATCH 2/7] :rotating_light: fix linter warnings --- test/extensions/base_reponse_test.dart | 4 +-- test/extensions/base_request_test.dart | 7 ++-- .../extensions/io_streamed_response_test.dart | 33 ++++++++++++------- test/extensions/streamed_response_test.dart | 33 ++++++++++++------- 4 files changed, 50 insertions(+), 27 deletions(-) diff --git a/test/extensions/base_reponse_test.dart b/test/extensions/base_reponse_test.dart index 8eb9b38..faf611a 100644 --- a/test/extensions/base_reponse_test.dart +++ b/test/extensions/base_reponse_test.dart @@ -60,7 +60,8 @@ main() { expect(copied.request, equals(response.request)); expect(copied.headers, equals(response.headers)); expect(copied.isRedirect, equals(response.isRedirect)); - expect(copied.persistentConnection, equals(response.persistentConnection)); + expect( + copied.persistentConnection, equals(response.persistentConnection)); expect(copied.reasonPhrase, equals(response.reasonPhrase)); }); @@ -81,7 +82,6 @@ main() { }); } - // Custom response type that doesn't extend any of the supported types class _UnsupportedResponse extends BaseResponse { _UnsupportedResponse() : super(200); diff --git a/test/extensions/base_request_test.dart b/test/extensions/base_request_test.dart index c89b1fd..08e4baf 100644 --- a/test/extensions/base_request_test.dart +++ b/test/extensions/base_request_test.dart @@ -34,9 +34,10 @@ main() { final testHeaders = {'Content-Type': 'multipart/form-data'}; final testFields = {'field1': 'value1', 'field2': 'value2'}; - final MultipartRequest multipartRequest = MultipartRequest('POST', testUrl) - ..headers.addAll(testHeaders) - ..fields.addAll(testFields); + final MultipartRequest multipartRequest = + MultipartRequest('POST', testUrl) + ..headers.addAll(testHeaders) + ..fields.addAll(testFields); // Add a test file to the request final testFileBytes = utf8.encode('test file content'); diff --git a/test/extensions/io_streamed_response_test.dart b/test/extensions/io_streamed_response_test.dart index 00d6428..03ccffd 100644 --- a/test/extensions/io_streamed_response_test.dart +++ b/test/extensions/io_streamed_response_test.dart @@ -29,7 +29,8 @@ void main() { ); }); - test('creates a copy with the same properties when no parameters are provided', + test( + 'creates a copy with the same properties when no parameters are provided', () { // Act final copy = response.copyWith(); @@ -53,7 +54,8 @@ void main() { // Assert expect(copy.statusCode, equals(newStatusCode)); - expect(copy.contentLength, equals(testContentLength)); // Other properties remain the same + expect(copy.contentLength, + equals(testContentLength)); // Other properties remain the same }); test('overrides contentLength when provided', () { @@ -65,7 +67,8 @@ void main() { // Assert expect(copy.contentLength, equals(newContentLength)); - expect(copy.statusCode, equals(testStatusCode)); // Other properties remain the same + expect(copy.statusCode, + equals(testStatusCode)); // Other properties remain the same }); test('overrides request when provided', () { @@ -77,7 +80,8 @@ void main() { // Assert expect(copy.request, equals(newRequest)); - expect(copy.statusCode, equals(testStatusCode)); // Other properties remain the same + expect(copy.statusCode, + equals(testStatusCode)); // Other properties remain the same }); test('overrides headers when provided', () { @@ -89,7 +93,8 @@ void main() { // Assert expect(copy.headers, equals(newHeaders)); - expect(copy.statusCode, equals(testStatusCode)); // Other properties remain the same + expect(copy.statusCode, + equals(testStatusCode)); // Other properties remain the same }); test('overrides isRedirect when provided', () { @@ -98,16 +103,19 @@ void main() { // Assert expect(copy.isRedirect, equals(!testIsRedirect)); - expect(copy.statusCode, equals(testStatusCode)); // Other properties remain the same + expect(copy.statusCode, + equals(testStatusCode)); // Other properties remain the same }); test('overrides persistentConnection when provided', () { // Act - final copy = response.copyWith(persistentConnection: !testPersistentConnection); + final copy = + response.copyWith(persistentConnection: !testPersistentConnection); // Assert expect(copy.persistentConnection, equals(!testPersistentConnection)); - expect(copy.statusCode, equals(testStatusCode)); // Other properties remain the same + expect(copy.statusCode, + equals(testStatusCode)); // Other properties remain the same }); test('overrides reasonPhrase when provided', () { @@ -119,7 +127,8 @@ void main() { // Assert expect(copy.reasonPhrase, equals(newReasonPhrase)); - expect(copy.statusCode, equals(testStatusCode)); // Other properties remain the same + expect(copy.statusCode, + equals(testStatusCode)); // Other properties remain the same }); test('overrides stream when provided', () async { @@ -134,7 +143,8 @@ void main() { final copyData = await copy.stream.toList(); final flattenedCopyData = copyData.expand((x) => x).toList(); expect(flattenedCopyData, equals(newData)); - expect(copy.statusCode, equals(testStatusCode)); // Other properties remain the same + expect(copy.statusCode, + equals(testStatusCode)); // Other properties remain the same }); test('can override multiple properties at once', () { @@ -154,7 +164,8 @@ void main() { expect(copy.statusCode, equals(newStatusCode)); expect(copy.headers, equals(newHeaders)); expect(copy.reasonPhrase, equals(newReasonPhrase)); - expect(copy.contentLength, equals(testContentLength)); // Unchanged properties remain the same + expect(copy.contentLength, + equals(testContentLength)); // Unchanged properties remain the same }); }); } diff --git a/test/extensions/streamed_response_test.dart b/test/extensions/streamed_response_test.dart index 1e63e55..172db66 100644 --- a/test/extensions/streamed_response_test.dart +++ b/test/extensions/streamed_response_test.dart @@ -28,7 +28,8 @@ void main() { ); }); - test('creates a copy with the same properties when no parameters are provided', + test( + 'creates a copy with the same properties when no parameters are provided', () { // Act final copy = response.copyWith(); @@ -52,7 +53,8 @@ void main() { // Assert expect(copy.statusCode, equals(newStatusCode)); - expect(copy.contentLength, equals(testContentLength)); // Other properties remain the same + expect(copy.contentLength, + equals(testContentLength)); // Other properties remain the same }); test('overrides contentLength when provided', () { @@ -64,7 +66,8 @@ void main() { // Assert expect(copy.contentLength, equals(newContentLength)); - expect(copy.statusCode, equals(testStatusCode)); // Other properties remain the same + expect(copy.statusCode, + equals(testStatusCode)); // Other properties remain the same }); test('overrides request when provided', () { @@ -76,7 +79,8 @@ void main() { // Assert expect(copy.request, equals(newRequest)); - expect(copy.statusCode, equals(testStatusCode)); // Other properties remain the same + expect(copy.statusCode, + equals(testStatusCode)); // Other properties remain the same }); test('overrides headers when provided', () { @@ -88,7 +92,8 @@ void main() { // Assert expect(copy.headers, equals(newHeaders)); - expect(copy.statusCode, equals(testStatusCode)); // Other properties remain the same + expect(copy.statusCode, + equals(testStatusCode)); // Other properties remain the same }); test('overrides isRedirect when provided', () { @@ -97,16 +102,19 @@ void main() { // Assert expect(copy.isRedirect, equals(!testIsRedirect)); - expect(copy.statusCode, equals(testStatusCode)); // Other properties remain the same + expect(copy.statusCode, + equals(testStatusCode)); // Other properties remain the same }); test('overrides persistentConnection when provided', () { // Act - final copy = response.copyWith(persistentConnection: !testPersistentConnection); + final copy = + response.copyWith(persistentConnection: !testPersistentConnection); // Assert expect(copy.persistentConnection, equals(!testPersistentConnection)); - expect(copy.statusCode, equals(testStatusCode)); // Other properties remain the same + expect(copy.statusCode, + equals(testStatusCode)); // Other properties remain the same }); test('overrides reasonPhrase when provided', () { @@ -118,7 +126,8 @@ void main() { // Assert expect(copy.reasonPhrase, equals(newReasonPhrase)); - expect(copy.statusCode, equals(testStatusCode)); // Other properties remain the same + expect(copy.statusCode, + equals(testStatusCode)); // Other properties remain the same }); test('overrides stream when provided', () async { @@ -133,7 +142,8 @@ void main() { final copyData = await copy.stream.toList(); final flattenedCopyData = copyData.expand((x) => x).toList(); expect(flattenedCopyData, equals(newData)); - expect(copy.statusCode, equals(testStatusCode)); // Other properties remain the same + expect(copy.statusCode, + equals(testStatusCode)); // Other properties remain the same }); test('can override multiple properties at once', () { @@ -153,7 +163,8 @@ void main() { expect(copy.statusCode, equals(newStatusCode)); expect(copy.headers, equals(newHeaders)); expect(copy.reasonPhrase, equals(newReasonPhrase)); - expect(copy.contentLength, equals(testContentLength)); // Unchanged properties remain the same + expect(copy.contentLength, + equals(testContentLength)); // Unchanged properties remain the same }); }); } From a7851a165d57d62133beb01961f843352763d222 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Klemen=20Tu=C5=A1ar?= Date: Wed, 23 Jul 2025 07:17:52 +0100 Subject: [PATCH 3/7] :rotating_light: fix linter warnings --- test/http/intercepted_http_test.dart | 3 +-- test/models/interceptor_contract_test.dart | 1 - test/utils/utils_test.dart | 2 +- 3 files changed, 2 insertions(+), 4 deletions(-) diff --git a/test/http/intercepted_http_test.dart b/test/http/intercepted_http_test.dart index 40c66a5..4b3a225 100644 --- a/test/http/intercepted_http_test.dart +++ b/test/http/intercepted_http_test.dart @@ -2,7 +2,6 @@ import 'dart:async'; import 'dart:convert'; import 'dart:typed_data'; -import 'package:http/http.dart'; import 'package:http_interceptor/http_interceptor.dart'; import 'package:test/test.dart'; @@ -239,7 +238,7 @@ void main() { // Arrange final timeout = Duration(seconds: 30); final retryPolicy = MockRetryPolicy(); - final onTimeout = () => StreamedResponse(Stream.value([]), 408); + StreamedResponse onTimeout() => StreamedResponse(Stream.value([]), 408); http = InterceptedHttp.build( interceptors: [mockInterceptor], diff --git a/test/models/interceptor_contract_test.dart b/test/models/interceptor_contract_test.dart index 2f53f1b..1826761 100644 --- a/test/models/interceptor_contract_test.dart +++ b/test/models/interceptor_contract_test.dart @@ -1,4 +1,3 @@ -import 'package:http/http.dart'; import 'package:http_interceptor/http_interceptor.dart'; import 'package:test/test.dart'; diff --git a/test/utils/utils_test.dart b/test/utils/utils_test.dart index 33c58f5..fdbac34 100644 --- a/test/utils/utils_test.dart +++ b/test/utils/utils_test.dart @@ -1,5 +1,5 @@ -import 'package:test/test.dart'; import 'package:http_interceptor/utils/utils.dart'; +import 'package:test/test.dart'; main() { group("buildUrlString", () { From 414ca86752a3f516295aeeae79241a1241855abe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Klemen=20Tu=C5=A1ar?= Date: Sat, 26 Jul 2025 10:54:10 +0100 Subject: [PATCH 4/7] :bug: update copyWith method to return a Future and improve stream handling --- lib/extensions/streamed_request.dart | 34 ++++++++++++---------------- 1 file changed, 14 insertions(+), 20 deletions(-) diff --git a/lib/extensions/streamed_request.dart b/lib/extensions/streamed_request.dart index e0a8e75..fdb0734 100644 --- a/lib/extensions/streamed_request.dart +++ b/lib/extensions/streamed_request.dart @@ -5,7 +5,7 @@ import 'package:http_interceptor/http/http_methods.dart'; extension StreamedRequestCopyWith on StreamedRequest { /// Creates a new instance of [StreamedRequest] based of on `this`. It copies /// all the properties and overrides the ones sent via parameters. - StreamedRequest copyWith({ + Future copyWith({ HttpMethod? method, Uri? url, Map? headers, @@ -13,26 +13,20 @@ extension StreamedRequestCopyWith on StreamedRequest { bool? followRedirects, int? maxRedirects, bool? persistentConnection, - }) { - // Create a new StreamedRequest with the same method and URL - final StreamedRequest clonedRequest = - StreamedRequest(method?.asString ?? this.method, url ?? this.url) - ..headers.addAll(headers ?? this.headers); + }) async { + final StreamedRequest clonedRequest = StreamedRequest( + method?.toString() ?? this.method, + url ?? this.url, + ) + ..followRedirects = followRedirects ?? this.followRedirects + ..maxRedirects = maxRedirects ?? this.maxRedirects + ..persistentConnection = persistentConnection ?? this.persistentConnection + ..headers.addAll(headers ?? this.headers); - // Use a broadcast stream to allow multiple listeners - final Stream> broadcastStream = - stream?.asBroadcastStream() ?? finalize().asBroadcastStream(); - - // Pipe the broadcast stream into the cloned request's sink - broadcastStream.listen( - (List data) => clonedRequest.sink.add(data), - onDone: () => clonedRequest.sink.close(), - ); - - this.persistentConnection = - persistentConnection ?? this.persistentConnection; - this.followRedirects = followRedirects ?? this.followRedirects; - this.maxRedirects = maxRedirects ?? this.maxRedirects; + await for (List chunk in stream ?? finalize()) { + clonedRequest.sink.add(chunk); + } + clonedRequest.sink.close(); return clonedRequest; } From 64d3e126ad1a09054286f5e02ee5ac52661a5bd4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Klemen=20Tu=C5=A1ar?= Date: Sat, 26 Jul 2025 10:54:21 +0100 Subject: [PATCH 5/7] :recycle: update copyWith method to return a Future and improve async handling --- lib/extensions/base_request.dart | 74 +++++++++++++------------- lib/http/intercepted_client.dart | 2 +- test/extensions/base_request_test.dart | 4 +- 3 files changed, 41 insertions(+), 39 deletions(-) diff --git a/lib/extensions/base_request.dart b/lib/extensions/base_request.dart index aaf3156..6d49c1b 100644 --- a/lib/extensions/base_request.dart +++ b/lib/extensions/base_request.dart @@ -1,3 +1,4 @@ +import 'dart:async'; import 'dart:convert'; import 'package:http/http.dart'; @@ -18,7 +19,7 @@ extension BaseRequestCopyWith on BaseRequest { /// instance. /// /// [stream] are only copied if `this` is a [StreamedRequest] instance. - BaseRequest copyWith({ + Future copyWith({ HttpMethod? method, Uri? url, Map? headers, @@ -33,39 +34,40 @@ extension BaseRequestCopyWith on BaseRequest { List? files, // StreamedRequest only properties. Stream>? stream, - }) => - switch (this) { - Request req => req.copyWith( - method: method, - url: url, - headers: headers, - body: body, - encoding: encoding, - followRedirects: followRedirects, - maxRedirects: maxRedirects, - persistentConnection: persistentConnection, - ), - StreamedRequest req => req.copyWith( - method: method, - url: url, - headers: headers, - stream: stream, - followRedirects: followRedirects, - maxRedirects: maxRedirects, - persistentConnection: persistentConnection, - ), - MultipartRequest req => req.copyWith( - method: method, - url: url, - headers: headers, - fields: fields, - files: files, - followRedirects: followRedirects, - maxRedirects: maxRedirects, - persistentConnection: persistentConnection, - ), - _ => throw UnsupportedError( - 'Cannot copy unsupported type of request $runtimeType', - ), - }; + }) async { + return switch (this) { + Request req => req.copyWith( + method: method, + url: url, + headers: headers, + body: body, + encoding: encoding, + followRedirects: followRedirects, + maxRedirects: maxRedirects, + persistentConnection: persistentConnection, + ), + StreamedRequest req => await req.copyWith( + method: method, + url: url, + headers: headers, + stream: stream, + followRedirects: followRedirects, + maxRedirects: maxRedirects, + persistentConnection: persistentConnection, + ), + MultipartRequest req => req.copyWith( + method: method, + url: url, + headers: headers, + fields: fields, + files: files, + followRedirects: followRedirects, + maxRedirects: maxRedirects, + persistentConnection: persistentConnection, + ), + _ => throw UnsupportedError( + 'Cannot copy unsupported type of request $runtimeType', + ), + }; + } } diff --git a/lib/http/intercepted_client.dart b/lib/http/intercepted_client.dart index 323623c..4ec0518 100644 --- a/lib/http/intercepted_client.dart +++ b/lib/http/intercepted_client.dart @@ -397,7 +397,7 @@ class InterceptedClient extends BaseClient { /// This internal function intercepts the request. Future _interceptRequest(BaseRequest request) async { - BaseRequest interceptedRequest = request.copyWith(); + BaseRequest interceptedRequest = await request.copyWith(); for (InterceptorContract interceptor in interceptors) { if (await interceptor.shouldInterceptRequest( request: interceptedRequest, diff --git a/test/extensions/base_request_test.dart b/test/extensions/base_request_test.dart index 08e4baf..34e15fb 100644 --- a/test/extensions/base_request_test.dart +++ b/test/extensions/base_request_test.dart @@ -5,12 +5,12 @@ import 'package:test/test.dart'; main() { group('BaseRequest.copyWith: ', () { - test('Request is copied from BaseRequest', () { + test('Request is copied from BaseRequest', () async { // Arrange final BaseRequest baseRequest = Request("GET", Uri.https("www.google.com", "/helloworld")) ..body = jsonEncode({'some_param': 'some value'}); - final copiedBaseRequest = baseRequest.copyWith(); + final copiedBaseRequest = await baseRequest.copyWith(); // Act final copied = copiedBaseRequest as Request; From 21ee7b121476f222ba7264eb60ddf85d9d8d741c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Klemen=20Tu=C5=A1ar?= Date: Sat, 26 Jul 2025 10:54:25 +0100 Subject: [PATCH 6/7] :white_check_mark: add tests --- test/extensions/request_test.dart | 4 +- test/extensions/streamed_request_test.dart | 135 +++++++++++++++++++++ 2 files changed, 137 insertions(+), 2 deletions(-) create mode 100644 test/extensions/streamed_request_test.dart diff --git a/test/extensions/request_test.dart b/test/extensions/request_test.dart index 09fab4b..747c643 100644 --- a/test/extensions/request_test.dart +++ b/test/extensions/request_test.dart @@ -18,9 +18,9 @@ main() { }); group('BaseRequest.copyWith: ', () { - test('Request is copied from BaseRequest', () { + test('Request is copied from BaseRequest', () async { // Arrange - final copiedBaseRequest = baseRequest.copyWith(); + final copiedBaseRequest = await baseRequest.copyWith(); // Act final copied = copiedBaseRequest as Request; diff --git a/test/extensions/streamed_request_test.dart b/test/extensions/streamed_request_test.dart new file mode 100644 index 0000000..90b2326 --- /dev/null +++ b/test/extensions/streamed_request_test.dart @@ -0,0 +1,135 @@ +import 'dart:convert'; +import 'dart:typed_data'; + +import 'package:http_interceptor/http_interceptor.dart'; +import 'package:test/test.dart'; + +void main() { + group('StreamedRequest.copyWith', () { + late StreamedRequest request; + final Uri testUrl = Uri.parse('https://example.com'); + final Map testHeaders = { + 'Content-Type': 'application/json', + }; + + setUp(() { + request = StreamedRequest('POST', testUrl)..headers.addAll(testHeaders); + + // Add test data to the request + final Uint8List testData = utf8.encode('test data'); + request.sink.add(testData); + request.sink.close(); + }); + + test( + 'creates a copy with the same properties when no parameters are provided', + () async { + // Buffer the original body before copyWith() ever runs + final Uint8List originalBytes = await request.finalize().toBytes(); + + // Act: pass the buffered stream into copyWith + final StreamedRequest copy = await request.copyWith( + stream: Stream.value(originalBytes), + ); + + // Assert basic properties + expect(copy.method, equals(request.method)); + expect(copy.url, equals(request.url)); + expect(copy.headers, equals(request.headers)); + + // Only finalize the copy, and compare its bytes to the buffer + final Uint8List copyBytes = await copy.finalize().toBytes(); + expect(copyBytes, equals(originalBytes)); + + // Assert default flags + expect(copy.followRedirects, equals(true)); + expect(copy.maxRedirects, equals(5)); + expect(copy.persistentConnection, equals(true)); + }, + ); + + test('overrides method when provided', () async { + // Act + final StreamedRequest copy = + await request.copyWith(method: HttpMethod.PUT); + + // Assert + expect(copy.method, equals('PUT')); + }); + + test('overrides url when provided', () async { + // Arrange + final Uri newUrl = Uri.parse('https://example.org'); + + // Act + final StreamedRequest copy = await request.copyWith(url: newUrl); + + // Assert + expect(copy.url, equals(newUrl)); + }); + + test('overrides headers when provided', () async { + // Arrange + final Map newHeaders = {'Authorization': 'Bearer token'}; + + // Act + final StreamedRequest copy = await request.copyWith(headers: newHeaders); + + // Assert + expect(copy.headers, equals(newHeaders)); + }); + + test('overrides stream when provided', () async { + // Arrange + final Uint8List newData = utf8.encode('new data'); + final Stream newStream = Stream.value(newData); + + // Act + final StreamedRequest copy = await request.copyWith(stream: newStream); + + // Assert + final Uint8List copyData = await copy.finalize().toBytes(); + expect(copyData, equals(newData)); + }); + + test('sets followRedirects on original request (bug)', () async { + // Arrange + final bool originalValue = request.followRedirects; + + // Act + final StreamedRequest copy = + await request.copyWith(followRedirects: !originalValue); + + // Assert + expect(request.followRedirects, equals(originalValue)); + expect(copy.followRedirects, equals(!originalValue)); + }); + + test('sets maxRedirects on original request (bug)', () async { + // Arrange + final int newMaxRedirects = 10; + + // Act + final StreamedRequest copy = + await request.copyWith(maxRedirects: newMaxRedirects); + + // Assert + expect(request.maxRedirects, equals(5)); + expect(copy.maxRedirects, equals(newMaxRedirects)); + expect(copy.method, equals(request.method)); + }); + + test('sets persistentConnection on original request (bug)', () async { + // Arrange + final bool originalValue = request.persistentConnection; + + // Act + final StreamedRequest copy = + await request.copyWith(persistentConnection: !originalValue); + + // Assert + expect(request.persistentConnection, equals(originalValue)); + expect(copy.persistentConnection, equals(!originalValue)); + }); + }); +} From c1c7fcfb26d1eb659884d5311362d7c1573f4175 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Klemen=20Tu=C5=A1ar?= Date: Sat, 26 Jul 2025 11:00:18 +0100 Subject: [PATCH 7/7] :recycle: refactor imports in request and response files for consistency --- lib/extensions/base_request.dart | 10 ++++------ lib/extensions/base_response_io.dart | 5 ++--- lib/extensions/base_response_none.dart | 5 ++--- lib/extensions/io_streamed_response.dart | 2 +- lib/extensions/request.dart | 2 +- lib/http/intercepted_client.dart | 7 +++---- 6 files changed, 13 insertions(+), 18 deletions(-) diff --git a/lib/extensions/base_request.dart b/lib/extensions/base_request.dart index 6d49c1b..9b2faa9 100644 --- a/lib/extensions/base_request.dart +++ b/lib/extensions/base_request.dart @@ -1,13 +1,11 @@ -import 'dart:async'; -import 'dart:convert'; +import 'dart:convert' show Encoding; import 'package:http/http.dart'; +import 'package:http_interceptor/extensions/multipart_request.dart'; +import 'package:http_interceptor/extensions/request.dart'; +import 'package:http_interceptor/extensions/streamed_request.dart'; import 'package:http_interceptor/http/http_methods.dart'; -import './multipart_request.dart'; -import './request.dart'; -import './streamed_request.dart'; - /// Extends [BaseRequest] to provide copied instances. extension BaseRequestCopyWith on BaseRequest { /// Creates a new instance of [BaseRequest] based of on `this`. It copies diff --git a/lib/extensions/base_response_io.dart b/lib/extensions/base_response_io.dart index 8f7c0c1..2d61d7b 100644 --- a/lib/extensions/base_response_io.dart +++ b/lib/extensions/base_response_io.dart @@ -3,9 +3,8 @@ import 'dart:io'; import 'package:http/http.dart'; import 'package:http/io_client.dart'; import 'package:http_interceptor/extensions/io_streamed_response.dart'; - -import './response.dart'; -import './streamed_response.dart'; +import 'package:http_interceptor/extensions/response.dart'; +import 'package:http_interceptor/extensions/streamed_response.dart'; // Extends [BaseRequest] to provide copied instances. extension BaseResponseCopyWith on BaseResponse { diff --git a/lib/extensions/base_response_none.dart b/lib/extensions/base_response_none.dart index 8a23ee5..b2a9c50 100644 --- a/lib/extensions/base_response_none.dart +++ b/lib/extensions/base_response_none.dart @@ -1,7 +1,6 @@ import 'package:http/http.dart'; - -import './response.dart'; -import './streamed_response.dart'; +import 'package:http_interceptor/extensions/response.dart'; +import 'package:http_interceptor/extensions/streamed_response.dart'; // Extends [BaseRequest] to provide copied instances. extension BaseResponseCopyWith on BaseResponse { diff --git a/lib/extensions/io_streamed_response.dart b/lib/extensions/io_streamed_response.dart index 424f870..2508878 100644 --- a/lib/extensions/io_streamed_response.dart +++ b/lib/extensions/io_streamed_response.dart @@ -1,4 +1,4 @@ -import 'dart:io'; +import 'dart:io' show HttpClientResponse; import 'package:http/http.dart'; import 'package:http/io_client.dart'; diff --git a/lib/extensions/request.dart b/lib/extensions/request.dart index 59c8fdd..4ed57da 100644 --- a/lib/extensions/request.dart +++ b/lib/extensions/request.dart @@ -1,4 +1,4 @@ -import 'dart:convert'; +import 'dart:convert' show Encoding; import 'package:http/http.dart'; import 'package:http_interceptor/http/http_methods.dart'; diff --git a/lib/http/intercepted_client.dart b/lib/http/intercepted_client.dart index 4ec0518..8ae0b6d 100644 --- a/lib/http/intercepted_client.dart +++ b/lib/http/intercepted_client.dart @@ -5,10 +5,9 @@ import 'dart:typed_data'; import 'package:http/http.dart'; import 'package:http_interceptor/extensions/base_request.dart'; import 'package:http_interceptor/extensions/uri.dart'; - -import '../models/interceptor_contract.dart'; -import '../models/retry_policy.dart'; -import 'http_methods.dart'; +import 'package:http_interceptor/http/http_methods.dart'; +import 'package:http_interceptor/models/interceptor_contract.dart'; +import 'package:http_interceptor/models/retry_policy.dart'; typedef TimeoutCallback = FutureOr Function();