diff --git a/pkgs/http_profile/lib/src/http_client_request_profile.dart b/pkgs/http_profile/lib/src/http_client_request_profile.dart index 1ce632cfd0..84db62f1a2 100644 --- a/pkgs/http_profile/lib/src/http_client_request_profile.dart +++ b/pkgs/http_profile/lib/src/http_client_request_profile.dart @@ -9,10 +9,21 @@ final class HttpProfileRequestEvent { final int _timestamp; final String _name; + DateTime get timestamp => DateTime.fromMicrosecondsSinceEpoch(_timestamp); + + String get name => _name; + HttpProfileRequestEvent({required DateTime timestamp, required String name}) : _timestamp = timestamp.microsecondsSinceEpoch, _name = name; + static HttpProfileRequestEvent _fromJson(Map json) => + HttpProfileRequestEvent( + timestamp: + DateTime.fromMicrosecondsSinceEpoch(json['timestamp'] as int), + name: json['event'] as String, + ); + Map _toJson() => { 'timestamp': _timestamp, 'event': _name, @@ -31,6 +42,12 @@ final class HttpClientRequestProfile { final _data = {}; + /// The HTTP request method associated with the request. + String get requestMethod => _data['requestMethod'] as String; + + /// The URI to which the request was sent. + String get requestUri => _data['requestUri'] as String; + /// Records an event related to the request. /// /// Usage example: @@ -54,6 +71,12 @@ final class HttpClientRequestProfile { _updated(); } + /// An unmodifiable list containing the events related to the request. + List get events => + UnmodifiableListView((_data['events'] as List>).map( + HttpProfileRequestEvent._fromJson, + )); + /// Details about the request. late final HttpProfileRequestData requestData; diff --git a/pkgs/http_profile/lib/src/http_profile.dart b/pkgs/http_profile/lib/src/http_profile.dart index 3c208050b4..488b68d683 100644 --- a/pkgs/http_profile/lib/src/http_profile.dart +++ b/pkgs/http_profile/lib/src/http_profile.dart @@ -3,7 +3,7 @@ // BSD-style license that can be found in the LICENSE file. import 'dart:async'; -import 'dart:collection' show UnmodifiableListView; +import 'dart:collection' show UnmodifiableListView, UnmodifiableMapView; import 'dart:developer' show Service, addHttpClientProfilingData; import 'dart:io' show HttpClient, HttpClientResponseCompressionState; import 'dart:isolate' show Isolate; diff --git a/pkgs/http_profile/lib/src/http_profile_request_data.dart b/pkgs/http_profile/lib/src/http_profile_request_data.dart index 2bcd1a7c6c..1463994c12 100644 --- a/pkgs/http_profile/lib/src/http_profile_request_data.dart +++ b/pkgs/http_profile/lib/src/http_profile_request_data.dart @@ -10,6 +10,14 @@ final class HttpProfileProxyData { final bool? _isDirect; final int? _port; + String? get host => _host; + + String? get username => _username; + + bool? get isDirect => _isDirect; + + int? get port => _port; + HttpProfileProxyData({ String? host, String? username, @@ -20,6 +28,14 @@ final class HttpProfileProxyData { _isDirect = isDirect, _port = port; + static HttpProfileProxyData _fromJson(Map json) => + HttpProfileProxyData( + host: json['host'] as String?, + username: json['username'] as String?, + isDirect: json['isDirect'] as bool?, + port: json['port'] as int?, + ); + Map _toJson() => { if (_host != null) 'host': _host, if (_username != null) 'username': _username, @@ -50,20 +66,43 @@ final class HttpProfileRequestData { /// This information is meant to be used for debugging. /// /// It can contain any arbitrary data as long as the values are of type - /// [String] or [int]. For example: - /// { 'localPort': 1285, 'remotePort': 443, 'connectionPoolId': '21x23' } - set connectionInfo(Map value) { + /// [String] or [int]. + /// + /// This field can only be modified by assigning a Map to it. That is: + /// ```dart + /// // Valid + /// profile?.requestData.connectionInfo = { + /// 'localPort': 1285, + /// 'remotePort': 443, + /// 'connectionPoolId': '21x23', + /// }; + /// + /// // Invalid + /// profile?.requestData.connectionInfo?['localPort'] = 1285; + /// ``` + set connectionInfo(Map? value) { _checkAndUpdate(); - for (final v in value.values) { - if (!(v is String || v is int)) { - throw ArgumentError( - 'The values in connectionInfo must be of type String or int.', - ); + if (value == null) { + _requestData.remove('connectionInfo'); + } else { + for (final v in value.values) { + if (!(v is String || v is int)) { + throw ArgumentError( + 'The values in connectionInfo must be of type String or int.', + ); + } } + _requestData['connectionInfo'] = {...value}; } - _requestData['connectionInfo'] = {...value}; } + Map? get connectionInfo => + _requestData['connectionInfo'] == null + ? null + : UnmodifiableMapView( + _requestData['connectionInfo'] as Map, + ); + /// The content length of the request, in bytes. set contentLength(int? value) { _checkAndUpdate(); @@ -74,12 +113,20 @@ final class HttpProfileRequestData { } } + int? get contentLength => _requestData['contentLength'] as int?; + /// Whether automatic redirect following was enabled for the request. - set followRedirects(bool value) { + set followRedirects(bool? value) { _checkAndUpdate(); - _requestData['followRedirects'] = value; + if (value == null) { + _requestData.remove('followRedirects'); + } else { + _requestData['followRedirects'] = value; + } } + bool? get followRedirects => _requestData['followRedirects'] as bool?; + /// The request headers where duplicate headers are represented using a list /// of values. /// @@ -89,7 +136,7 @@ final class HttpProfileRequestData { /// // Foo: Bar /// // Foo: Baz /// - /// profile?.requestData.headersListValues({'Foo', ['Bar', 'Baz']}); + /// profile?.requestData.headersListValues({'Foo': ['Bar', 'Baz']}); /// ``` set headersListValues(Map>? value) { _checkAndUpdate(); @@ -109,7 +156,7 @@ final class HttpProfileRequestData { /// // Foo: Bar /// // Foo: Baz /// - /// profile?.requestData.headersCommaValues({'Foo', 'Bar, Baz']}); + /// profile?.requestData.headersCommaValues({'Foo': 'Bar, Baz']}); /// ``` set headersCommaValues(Map? value) { _checkAndUpdate(); @@ -120,24 +167,81 @@ final class HttpProfileRequestData { _requestData['headers'] = splitHeaderValues(value); } + /// An unmodifiable map representing the request headers. Duplicate headers + /// are represented using a list of values. + /// + /// For example, the map + /// + /// ```dart + /// {'Foo': ['Bar', 'Baz']}); + /// ``` + /// + /// represents the headers + /// + /// Foo: Bar + /// Foo: Baz + Map>? get headers => _requestData['headers'] == null + ? null + : UnmodifiableMapView( + _requestData['headers'] as Map>); + /// The maximum number of redirects allowed during the request. - set maxRedirects(int value) { + set maxRedirects(int? value) { _checkAndUpdate(); - _requestData['maxRedirects'] = value; + if (value == null) { + _requestData.remove('maxRedirects'); + } else { + _requestData['maxRedirects'] = value; + } } + int? get maxRedirects => _requestData['maxRedirects'] as int?; + /// The requested persistent connection state. - set persistentConnection(bool value) { + set persistentConnection(bool? value) { _checkAndUpdate(); - _requestData['persistentConnection'] = value; + if (value == null) { + _requestData.remove('persistentConnection'); + } else { + _requestData['persistentConnection'] = value; + } } + bool? get persistentConnection => + _requestData['persistentConnection'] as bool?; + /// Proxy authentication details for the request. - set proxyDetails(HttpProfileProxyData value) { + set proxyDetails(HttpProfileProxyData? value) { _checkAndUpdate(); - _requestData['proxyDetails'] = value._toJson(); + if (value == null) { + _requestData.remove('proxyDetails'); + } else { + _requestData['proxyDetails'] = value._toJson(); + } } + HttpProfileProxyData? get proxyDetails => _requestData['proxyDetails'] == null + ? null + : HttpProfileProxyData._fromJson( + _requestData['proxyDetails'] as Map, + ); + + /// The time at which the request was initiated. + DateTime get startTime => DateTime.fromMicrosecondsSinceEpoch( + _data['requestStartTimestamp'] as int, + ); + + /// The time when the request was fully sent. + DateTime? get endTime => _data['requestEndTimestamp'] == null + ? null + : DateTime.fromMicrosecondsSinceEpoch( + _data['requestEndTimestamp'] as int, + ); + + /// The error that the request failed with. + String? get error => + _requestData['error'] == null ? null : _requestData['error'] as String; + HttpProfileRequestData._( this._data, this._updated, diff --git a/pkgs/http_profile/lib/src/http_profile_response_data.dart b/pkgs/http_profile/lib/src/http_profile_response_data.dart index 3a972b4f85..87c769c5e2 100644 --- a/pkgs/http_profile/lib/src/http_profile_response_data.dart +++ b/pkgs/http_profile/lib/src/http_profile_response_data.dart @@ -10,6 +10,12 @@ class HttpProfileRedirectData { final String _method; final String _location; + int get statusCode => _statusCode; + + String get method => _method; + + String get location => _location; + HttpProfileRedirectData({ required int statusCode, required String method, @@ -18,6 +24,13 @@ class HttpProfileRedirectData { _method = method, _location = location; + static HttpProfileRedirectData _fromJson(Map json) => + HttpProfileRedirectData( + statusCode: json['statusCode'] as int, + method: json['method'] as String, + location: json['location'] as String, + ); + Map _toJson() => { 'statusCode': _statusCode, 'method': _method, @@ -42,6 +55,12 @@ final class HttpProfileResponseData { .add(redirect._toJson()); } + /// An unmodifiable list containing the redirects that the connection went + /// through. + List get redirects => UnmodifiableListView( + (_responseData['redirects'] as List>) + .map(HttpProfileRedirectData._fromJson)); + /// A sink that can be used to record the body of the response. StreamSink> get bodySink => _body.sink; @@ -54,21 +73,40 @@ final class HttpProfileResponseData { /// This information is meant to be used for debugging. /// /// It can contain any arbitrary data as long as the values are of type - /// [String] or [int]. For example: - /// { 'localPort': 1285, 'remotePort': 443, 'connectionPoolId': '21x23' } - set connectionInfo(Map value) { + /// [String] or [int]. + /// + /// This field can only be modified by assigning a Map to it. That is: + /// ```dart + /// // Valid + /// profile?.responseData.connectionInfo = { + /// 'localPort': 1285, + /// 'remotePort': 443, + /// 'connectionPoolId': '21x23', + /// }; + /// + /// // Invalid + /// profile?.responseData.connectionInfo?['localPort'] = 1285; + /// ``` + set connectionInfo(Map? value) { _checkAndUpdate(); - for (final v in value.values) { - if (!(v is String || v is int)) { - throw ArgumentError( - 'The values in connectionInfo must be of type String or int.', - ); + if (value == null) { + _responseData.remove('connectionInfo'); + } else { + for (final v in value.values) { + if (!(v is String || v is int)) { + throw ArgumentError( + 'The values in connectionInfo must be of type String or int.', + ); + } } + _responseData['connectionInfo'] = {...value}; } - _responseData['connectionInfo'] = {...value}; } - /// The reponse headers where duplicate headers are represented using a list + Map? get connectionInfo => + _responseData['connectionInfo'] as Map?; + + /// The response headers where duplicate headers are represented using a list /// of values. /// /// For example: @@ -77,7 +115,7 @@ final class HttpProfileResponseData { /// // Foo: Bar /// // Foo: Baz /// - /// profile?.requestData.headersListValues({'Foo', ['Bar', 'Baz']}); + /// profile?.requestData.headersListValues({'Foo': ['Bar', 'Baz']}); /// ``` set headersListValues(Map>? value) { _checkAndUpdate(); @@ -97,7 +135,7 @@ final class HttpProfileResponseData { /// // Foo: Bar /// // Foo: Baz /// - /// profile?.responseData.headersCommaValues({'Foo', 'Bar, Baz']}); + /// profile?.responseData.headersCommaValues({'Foo': 'Bar, Baz']}); /// ``` set headersCommaValues(Map? value) { _checkAndUpdate(); @@ -108,16 +146,44 @@ final class HttpProfileResponseData { _responseData['headers'] = splitHeaderValues(value); } + /// An unmodifiable map representing the response headers. Duplicate headers + /// are represented using a list of values. + /// + /// For example, the map + /// + /// ```dart + /// {'Foo': ['Bar', 'Baz']}); + /// ``` + /// + /// represents the headers + /// + /// Foo: Bar + /// Foo: Baz + Map>? get headers => _responseData['headers'] == null + ? null + : UnmodifiableMapView( + _responseData['headers'] as Map>); + // The compression state of the response. // // This specifies whether the response bytes were compressed when they were // received across the wire and whether callers will receive compressed or // uncompressed bytes when they listen to the response body byte stream. - set compressionState(HttpClientResponseCompressionState value) { + set compressionState(HttpClientResponseCompressionState? value) { _checkAndUpdate(); - _responseData['compressionState'] = value.name; + if (value == null) { + _responseData.remove('compressionState'); + } else { + _responseData['compressionState'] = value.name; + } } + HttpClientResponseCompressionState? get compressionState => + _responseData['compressionState'] == null + ? null + : HttpClientResponseCompressionState.values + .firstWhere((v) => v.name == _responseData['compressionState']); + // The reason phrase associated with the response e.g. "OK". set reasonPhrase(String? value) { _checkAndUpdate(); @@ -128,18 +194,33 @@ final class HttpProfileResponseData { } } + String? get reasonPhrase => _responseData['reasonPhrase'] as String?; + /// Whether the status code was one of the normal redirect codes. - set isRedirect(bool value) { + set isRedirect(bool? value) { _checkAndUpdate(); - _responseData['isRedirect'] = value; + if (value == null) { + _responseData.remove('isRedirect'); + } else { + _responseData['isRedirect'] = value; + } } + bool? get isRedirect => _responseData['isRedirect'] as bool?; + /// The persistent connection state returned by the server. - set persistentConnection(bool value) { + set persistentConnection(bool? value) { _checkAndUpdate(); - _responseData['persistentConnection'] = value; + if (value == null) { + _responseData.remove('persistentConnection'); + } else { + _responseData['persistentConnection'] = value; + } } + bool? get persistentConnection => + _responseData['persistentConnection'] as bool?; + /// The content length of the response body, in bytes. set contentLength(int? value) { _checkAndUpdate(); @@ -150,17 +231,42 @@ final class HttpProfileResponseData { } } - set statusCode(int value) { + int? get contentLength => _responseData['contentLength'] as int?; + + set statusCode(int? value) { _checkAndUpdate(); - _responseData['statusCode'] = value; + if (value == null) { + _responseData.remove('statusCode'); + } else { + _responseData['statusCode'] = value; + } } + int? get statusCode => _responseData['statusCode'] as int?; + /// The time at which the initial response was received. - set startTime(DateTime value) { + set startTime(DateTime? value) { _checkAndUpdate(); - _responseData['startTime'] = value.microsecondsSinceEpoch; + if (value == null) { + _responseData.remove('startTime'); + } else { + _responseData['startTime'] = value.microsecondsSinceEpoch; + } } + DateTime? get startTime => _responseData['startTime'] == null + ? null + : DateTime.fromMicrosecondsSinceEpoch(_responseData['startTime'] as int); + + /// The time when the response was fully received. + DateTime? get endTime => _responseData['endTime'] == null + ? null + : DateTime.fromMicrosecondsSinceEpoch(_responseData['endTime'] as int); + + /// The error that the response failed with. + String? get error => + _responseData['error'] == null ? null : _responseData['error'] as String; + HttpProfileResponseData._( this._data, this._updated, diff --git a/pkgs/http_profile/test/http_client_request_profile_test.dart b/pkgs/http_profile/test/http_client_request_profile_test.dart index 6776db8a29..6c6a9e4884 100644 --- a/pkgs/http_profile/test/http_client_request_profile_test.dart +++ b/pkgs/http_profile/test/http_client_request_profile_test.dart @@ -53,20 +53,28 @@ void main() { }); test('calling HttpClientRequestProfile.addEvent', () async { - final events = backingMap['events'] as List>; - expect(events, isEmpty); + final eventsFromBackingMap = + backingMap['events'] as List>; + expect(eventsFromBackingMap, isEmpty); + + expect(profile.events, isEmpty); profile.addEvent(HttpProfileRequestEvent( timestamp: DateTime.parse('2024-03-22'), name: 'an event', )); - expect(events.length, 1); - final event = events.last; + expect(eventsFromBackingMap.length, 1); + final eventFromBackingMap = eventsFromBackingMap.last; expect( - event['timestamp'], + eventFromBackingMap['timestamp'], DateTime.parse('2024-03-22').microsecondsSinceEpoch, ); - expect(event['event'], 'an event'); + expect(eventFromBackingMap['event'], 'an event'); + + expect(profile.events.length, 1); + final eventFromGetter = profile.events.first; + expect(eventFromGetter.timestamp, DateTime.parse('2024-03-22')); + expect(eventFromGetter.name, 'an event'); }); } diff --git a/pkgs/http_profile/test/http_profile_request_data_test.dart b/pkgs/http_profile/test/http_profile_request_data_test.dart index cbd6a20ec4..6723e81331 100644 --- a/pkgs/http_profile/test/http_profile_request_data_test.dart +++ b/pkgs/http_profile/test/http_profile_request_data_test.dart @@ -37,22 +37,51 @@ void main() { ); expect(backingMap['requestMethod'], 'GET'); expect(backingMap['requestUri'], 'https://www.example.com'); + + expect(profile.requestData.startTime, DateTime.parse('2024-03-21')); + expect(profile.requestMethod, 'GET'); + expect(profile.requestUri, 'https://www.example.com'); }); test('populating HttpClientRequestProfile.requestEndTimestamp', () async { expect(backingMap['requestEndTimestamp'], isNull); + expect(profile.requestData.endTime, isNull); + await profile.requestData.close(DateTime.parse('2024-03-23')); expect( backingMap['requestEndTimestamp'], DateTime.parse('2024-03-23').microsecondsSinceEpoch, ); + expect(profile.requestData.endTime, DateTime.parse('2024-03-23')); }); test('populating HttpClientRequestProfile.requestData.connectionInfo', () async { final requestData = backingMap['requestData'] as Map; expect(requestData['connectionInfo'], isNull); + expect(profile.requestData.connectionInfo, isNull); + + profile.requestData.connectionInfo = { + 'localPort': 1285, + 'remotePort': 443, + 'connectionPoolId': '21x23' + }; + + final connectionInfoFromBackingMap = + requestData['connectionInfo'] as Map; + expect(connectionInfoFromBackingMap['localPort'], 1285); + expect(connectionInfoFromBackingMap['remotePort'], 443); + expect(connectionInfoFromBackingMap['connectionPoolId'], '21x23'); + + final connectionInfoFromGetter = profile.requestData.connectionInfo!; + expect(connectionInfoFromGetter['localPort'], 1285); + expect(connectionInfoFromGetter['remotePort'], 443); + expect(connectionInfoFromGetter['connectionPoolId'], '21x23'); + }); + + test('HttpClientRequestProfile.requestData.connectionInfo = null', () async { + final requestData = backingMap['requestData'] as Map; profile.requestData.connectionInfo = { 'localPort': 1285, @@ -60,73 +89,142 @@ void main() { 'connectionPoolId': '21x23' }; - final connectionInfo = + final connectionInfoFromBackingMap = requestData['connectionInfo'] as Map; - expect(connectionInfo['localPort'], 1285); - expect(connectionInfo['remotePort'], 443); - expect(connectionInfo['connectionPoolId'], '21x23'); + expect(connectionInfoFromBackingMap['localPort'], 1285); + expect(connectionInfoFromBackingMap['remotePort'], 443); + expect(connectionInfoFromBackingMap['connectionPoolId'], '21x23'); + + final connectionInfoFromGetter = profile.requestData.connectionInfo!; + expect(connectionInfoFromGetter['localPort'], 1285); + expect(connectionInfoFromGetter['remotePort'], 443); + expect(connectionInfoFromGetter['connectionPoolId'], '21x23'); + + profile.requestData.connectionInfo = null; + + expect(requestData['connectionInfo'], isNull); + expect(profile.requestData.connectionInfo, isNull); }); test('populating HttpClientRequestProfile.requestData.contentLength', () async { final requestData = backingMap['requestData'] as Map; expect(requestData['contentLength'], isNull); + expect(profile.requestData.contentLength, isNull); profile.requestData.contentLength = 1200; expect(requestData['contentLength'], 1200); + expect(profile.requestData.contentLength, 1200); }); - test('HttpClientRequestProfile.requestData.contentLength = nil', () async { + test('HttpClientRequestProfile.requestData.contentLength = null', () async { final requestData = backingMap['requestData'] as Map; profile.requestData.contentLength = 1200; expect(requestData['contentLength'], 1200); + expect(profile.requestData.contentLength, 1200); profile.requestData.contentLength = null; expect(requestData['contentLength'], isNull); + expect(profile.requestData.contentLength, isNull); }); test('populating HttpClientRequestProfile.requestData.error', () async { final requestData = backingMap['requestData'] as Map; expect(requestData['error'], isNull); + expect(profile.requestData.error, isNull); await profile.requestData.closeWithError('failed'); expect(requestData['error'], 'failed'); + expect(profile.requestData.error, 'failed'); }); test('populating HttpClientRequestProfile.requestData.followRedirects', () async { final requestData = backingMap['requestData'] as Map; expect(requestData['followRedirects'], isNull); + expect(profile.requestData.followRedirects, isNull); profile.requestData.followRedirects = true; expect(requestData['followRedirects'], true); + expect(profile.requestData.followRedirects, true); + }); + + test('HttpClientRequestProfile.requestData.followRedirects = null', () async { + final requestData = backingMap['requestData'] as Map; + + profile.requestData.followRedirects = true; + expect(requestData['followRedirects'], true); + expect(profile.requestData.followRedirects, true); + + profile.requestData.followRedirects = null; + expect(requestData['followRedirects'], isNull); + expect(profile.requestData.followRedirects, isNull); }); test('populating HttpClientRequestProfile.requestData.headersListValues', () async { final requestData = backingMap['requestData'] as Map; expect(requestData['headers'], isNull); + expect(profile.requestData.headers, isNull); profile.requestData.headersListValues = { 'fruit': ['apple', 'banana', 'grape'], 'content-length': ['0'], }; - final headers = requestData['headers'] as Map>; - expect(headers, { + expect( + requestData['headers'], + { + 'fruit': ['apple', 'banana', 'grape'], + 'content-length': ['0'], + }, + ); + expect( + profile.requestData.headers, + { + 'fruit': ['apple', 'banana', 'grape'], + 'content-length': ['0'], + }, + ); + }); + + test('HttpClientRequestProfile.requestData.headersListValues = null', + () async { + final requestData = backingMap['requestData'] as Map; + + profile.requestData.headersListValues = { 'fruit': ['apple', 'banana', 'grape'], 'content-length': ['0'], - }); + }; + expect( + requestData['headers'], + { + 'fruit': ['apple', 'banana', 'grape'], + 'content-length': ['0'], + }, + ); + expect( + profile.requestData.headers, + { + 'fruit': ['apple', 'banana', 'grape'], + 'content-length': ['0'], + }, + ); + + profile.requestData.headersListValues = null; + expect(requestData['headers'], isNull); + expect(profile.requestData.headers, isNull); }); test('populating HttpClientRequestProfile.requestData.headersCommaValues', () async { final requestData = backingMap['requestData'] as Map; expect(requestData['headers'], isNull); + expect(profile.requestData.headers, isNull); profile.requestData.headersCommaValues = { 'set-cookie': @@ -135,39 +233,114 @@ void main() { 'sessionId=e8bb43229de9; Domain=foo.example.com' }; - final headers = requestData['headers'] as Map>; - expect(headers, { - 'set-cookie': [ - 'id=a3fWa; Expires=Wed, 21 Oct 2015 07:28:00 GMT; Path=/,,HE,=L=LO', - 'sessionId=e8bb43229de9; Domain=foo.example.com' - ] - }); + expect( + requestData['headers'], + { + 'set-cookie': [ + 'id=a3fWa; Expires=Wed, 21 Oct 2015 07:28:00 GMT; Path=/,,HE,=L=LO', + 'sessionId=e8bb43229de9; Domain=foo.example.com' + ] + }, + ); + expect( + profile.requestData.headers, + { + 'set-cookie': [ + 'id=a3fWa; Expires=Wed, 21 Oct 2015 07:28:00 GMT; Path=/,,HE,=L=LO', + 'sessionId=e8bb43229de9; Domain=foo.example.com' + ] + }, + ); + }); + + test('HttpClientRequestProfile.requestData.headersCommaValues = null', + () async { + final requestData = backingMap['requestData'] as Map; + + profile.requestData.headersCommaValues = { + 'set-cookie': + // ignore: missing_whitespace_between_adjacent_strings + 'id=a3fWa; Expires=Wed, 21 Oct 2015 07:28:00 GMT; Path=/,,HE,=L=LO,' + 'sessionId=e8bb43229de9; Domain=foo.example.com' + }; + expect( + requestData['headers'], + { + 'set-cookie': [ + 'id=a3fWa; Expires=Wed, 21 Oct 2015 07:28:00 GMT; Path=/,,HE,=L=LO', + 'sessionId=e8bb43229de9; Domain=foo.example.com' + ] + }, + ); + expect( + profile.requestData.headers, + { + 'set-cookie': [ + 'id=a3fWa; Expires=Wed, 21 Oct 2015 07:28:00 GMT; Path=/,,HE,=L=LO', + 'sessionId=e8bb43229de9; Domain=foo.example.com' + ] + }, + ); + + profile.requestData.headersCommaValues = null; + expect(requestData['headers'], isNull); + expect(profile.requestData.headers, isNull); }); test('populating HttpClientRequestProfile.requestData.maxRedirects', () async { final requestData = backingMap['requestData'] as Map; expect(requestData['maxRedirects'], isNull); + expect(profile.requestData.maxRedirects, isNull); profile.requestData.maxRedirects = 5; expect(requestData['maxRedirects'], 5); + expect(profile.requestData.maxRedirects, 5); + }); + + test('HttpClientRequestProfile.requestData.maxRedirects = null', () async { + final requestData = backingMap['requestData'] as Map; + + profile.requestData.maxRedirects = 5; + expect(requestData['maxRedirects'], 5); + expect(profile.requestData.maxRedirects, 5); + + profile.requestData.maxRedirects = null; + expect(requestData['maxRedirects'], isNull); + expect(profile.requestData.maxRedirects, isNull); }); test('populating HttpClientRequestProfile.requestData.persistentConnection', () async { final requestData = backingMap['requestData'] as Map; expect(requestData['persistentConnection'], isNull); + expect(profile.requestData.persistentConnection, isNull); profile.requestData.persistentConnection = true; expect(requestData['persistentConnection'], true); + expect(profile.requestData.persistentConnection, true); + }); + + test('HttpClientRequestProfile.requestData.persistentConnection = null', + () async { + final requestData = backingMap['requestData'] as Map; + + profile.requestData.persistentConnection = true; + expect(requestData['persistentConnection'], true); + expect(profile.requestData.persistentConnection, true); + + profile.requestData.persistentConnection = null; + expect(requestData['persistentConnection'], isNull); + expect(profile.requestData.persistentConnection, isNull); }); test('populating HttpClientRequestProfile.requestData.proxyDetails', () async { final requestData = backingMap['requestData'] as Map; expect(requestData['proxyDetails'], isNull); + expect(profile.requestData.proxyDetails, isNull); profile.requestData.proxyDetails = HttpProfileProxyData( host: 'https://www.example.com', @@ -176,14 +349,47 @@ void main() { port: 4321, ); - final proxyDetails = requestData['proxyDetails'] as Map; - expect( - proxyDetails['host'], - 'https://www.example.com', + final proxyDetailsFromBackingMap = + requestData['proxyDetails'] as Map; + expect(proxyDetailsFromBackingMap['host'], 'https://www.example.com'); + expect(proxyDetailsFromBackingMap['username'], 'abc123'); + expect(proxyDetailsFromBackingMap['isDirect'], true); + expect(proxyDetailsFromBackingMap['port'], 4321); + + final proxyDetailsFromGetter = profile.requestData.proxyDetails!; + expect(proxyDetailsFromGetter.host, 'https://www.example.com'); + expect(proxyDetailsFromGetter.username, 'abc123'); + expect(proxyDetailsFromGetter.isDirect, true); + expect(proxyDetailsFromGetter.port, 4321); + }); + + test('HttpClientRequestProfile.requestData.proxyDetails = null', () async { + final requestData = backingMap['requestData'] as Map; + + profile.requestData.proxyDetails = HttpProfileProxyData( + host: 'https://www.example.com', + username: 'abc123', + isDirect: true, + port: 4321, ); - expect(proxyDetails['username'], 'abc123'); - expect(proxyDetails['isDirect'], true); - expect(proxyDetails['port'], 4321); + + final proxyDetailsFromBackingMap = + requestData['proxyDetails'] as Map; + expect(proxyDetailsFromBackingMap['host'], 'https://www.example.com'); + expect(proxyDetailsFromBackingMap['username'], 'abc123'); + expect(proxyDetailsFromBackingMap['isDirect'], true); + expect(proxyDetailsFromBackingMap['port'], 4321); + + final proxyDetailsFromGetter = profile.requestData.proxyDetails!; + expect(proxyDetailsFromGetter.host, 'https://www.example.com'); + expect(proxyDetailsFromGetter.username, 'abc123'); + expect(proxyDetailsFromGetter.isDirect, true); + expect(proxyDetailsFromGetter.port, 4321); + + profile.requestData.proxyDetails = null; + + expect(requestData['proxyDetails'], isNull); + expect(profile.requestData.proxyDetails, isNull); }); test('using HttpClientRequestProfile.requestData.bodySink', () async { diff --git a/pkgs/http_profile/test/http_profile_response_data_test.dart b/pkgs/http_profile/test/http_profile_response_data_test.dart index e24a758021..f256971463 100644 --- a/pkgs/http_profile/test/http_profile_response_data_test.dart +++ b/pkgs/http_profile/test/http_profile_response_data_test.dart @@ -28,8 +28,10 @@ void main() { test('calling HttpClientRequestProfile.responseData.addRedirect', () async { final responseData = backingMap['responseData'] as Map; - final redirects = responseData['redirects'] as List>; - expect(redirects, isEmpty); + final redirectsFromBackingMap = + responseData['redirects'] as List>; + expect(redirectsFromBackingMap, isEmpty); + expect(profile.responseData.redirects, isEmpty); profile.responseData.addRedirect(HttpProfileRedirectData( statusCode: 301, @@ -37,17 +39,24 @@ void main() { location: 'https://images.example.com/1', )); - expect(redirects.length, 1); - final redirect = redirects.last; - expect(redirect['statusCode'], 301); - expect(redirect['method'], 'GET'); - expect(redirect['location'], 'https://images.example.com/1'); + expect(redirectsFromBackingMap.length, 1); + final redirectFromBackingMap = redirectsFromBackingMap.last; + expect(redirectFromBackingMap['statusCode'], 301); + expect(redirectFromBackingMap['method'], 'GET'); + expect(redirectFromBackingMap['location'], 'https://images.example.com/1'); + + expect(profile.responseData.redirects.length, 1); + final redirectFromGetter = profile.responseData.redirects.first; + expect(redirectFromGetter.statusCode, 301); + expect(redirectFromGetter.method, 'GET'); + expect(redirectFromGetter.location, 'https://images.example.com/1'); }); test('populating HttpClientRequestProfile.responseData.connectionInfo', () async { final responseData = backingMap['responseData'] as Map; expect(responseData['connectionInfo'], isNull); + expect(profile.responseData.connectionInfo, isNull); profile.responseData.connectionInfo = { 'localPort': 1285, @@ -55,17 +64,49 @@ void main() { 'connectionPoolId': '21x23' }; - final connectionInfo = + final connectionInfoFromBackingMap = responseData['connectionInfo'] as Map; - expect(connectionInfo['localPort'], 1285); - expect(connectionInfo['remotePort'], 443); - expect(connectionInfo['connectionPoolId'], '21x23'); + expect(connectionInfoFromBackingMap['localPort'], 1285); + expect(connectionInfoFromBackingMap['remotePort'], 443); + expect(connectionInfoFromBackingMap['connectionPoolId'], '21x23'); + + final connectionInfoFromGetter = profile.responseData.connectionInfo!; + expect(connectionInfoFromGetter['localPort'], 1285); + expect(connectionInfoFromGetter['remotePort'], 443); + expect(connectionInfoFromGetter['connectionPoolId'], '21x23'); + }); + + test('HttpClientRequestProfile.responseData.connectionInfo = null', () async { + final responseData = backingMap['responseData'] as Map; + + profile.responseData.connectionInfo = { + 'localPort': 1285, + 'remotePort': 443, + 'connectionPoolId': '21x23' + }; + + final connectionInfoFromBackingMap = + responseData['connectionInfo'] as Map; + expect(connectionInfoFromBackingMap['localPort'], 1285); + expect(connectionInfoFromBackingMap['remotePort'], 443); + expect(connectionInfoFromBackingMap['connectionPoolId'], '21x23'); + + final connectionInfoFromGetter = profile.responseData.connectionInfo!; + expect(connectionInfoFromGetter['localPort'], 1285); + expect(connectionInfoFromGetter['remotePort'], 443); + expect(connectionInfoFromGetter['connectionPoolId'], '21x23'); + + profile.responseData.connectionInfo = null; + + expect(responseData['connectionInfo'], isNull); + expect(profile.responseData.connectionInfo, isNull); }); test('populating HttpClientRequestProfile.responseData.headersListValues', () async { final responseData = backingMap['responseData'] as Map; expect(responseData['headers'], isNull); + expect(profile.responseData.headers, isNull); profile.responseData.headersListValues = { 'connection': ['keep-alive'], @@ -73,18 +114,91 @@ void main() { 'content-type': ['application/json', 'charset=utf-8'], }; - final headers = responseData['headers'] as Map>; - expect(headers, { + expect( + responseData['headers'], + { + 'connection': ['keep-alive'], + 'cache-control': ['max-age=43200'], + 'content-type': ['application/json', 'charset=utf-8'], + }, + ); + expect( + profile.responseData.headers, + { + 'connection': ['keep-alive'], + 'cache-control': ['max-age=43200'], + 'content-type': ['application/json', 'charset=utf-8'], + }, + ); + }); + + test('HttpClientRequestProfile.responseData.headersListValues = null', + () async { + final responseData = backingMap['responseData'] as Map; + + profile.responseData.headersListValues = { 'connection': ['keep-alive'], 'cache-control': ['max-age=43200'], 'content-type': ['application/json', 'charset=utf-8'], - }); + }; + expect( + responseData['headers'], + { + 'connection': ['keep-alive'], + 'cache-control': ['max-age=43200'], + 'content-type': ['application/json', 'charset=utf-8'], + }, + ); + expect( + profile.responseData.headers, + { + 'connection': ['keep-alive'], + 'cache-control': ['max-age=43200'], + 'content-type': ['application/json', 'charset=utf-8'], + }, + ); + + profile.responseData.headersListValues = null; + expect(responseData['headers'], isNull); + expect(profile.responseData.headers, isNull); }); test('populating HttpClientRequestProfile.responseData.headersCommaValues', () async { final responseData = backingMap['responseData'] as Map; expect(responseData['headers'], isNull); + expect(profile.responseData.headers, isNull); + + profile.responseData.headersCommaValues = { + 'set-cookie': + // ignore: missing_whitespace_between_adjacent_strings + 'id=a3fWa; Expires=Wed, 21 Oct 2015 07:28:00 GMT; Path=/,,HE,=L=LO,' + 'sessionId=e8bb43229de9; Domain=foo.example.com' + }; + + expect( + responseData['headers'], + { + 'set-cookie': [ + 'id=a3fWa; Expires=Wed, 21 Oct 2015 07:28:00 GMT; Path=/,,HE,=L=LO', + 'sessionId=e8bb43229de9; Domain=foo.example.com' + ] + }, + ); + expect( + profile.responseData.headers, + { + 'set-cookie': [ + 'id=a3fWa; Expires=Wed, 21 Oct 2015 07:28:00 GMT; Path=/,,HE,=L=LO', + 'sessionId=e8bb43229de9; Domain=foo.example.com' + ] + }, + ); + }); + + test('HttpClientRequestProfile.responseData.headersCommaValues = null', + () async { + final responseData = backingMap['responseData'] as Map; profile.responseData.headersCommaValues = { 'set-cookie': @@ -92,87 +206,186 @@ void main() { 'id=a3fWa; Expires=Wed, 21 Oct 2015 07:28:00 GMT; Path=/,,HE,=L=LO,' 'sessionId=e8bb43229de9; Domain=foo.example.com' }; + expect( + responseData['headers'], + { + 'set-cookie': [ + 'id=a3fWa; Expires=Wed, 21 Oct 2015 07:28:00 GMT; Path=/,,HE,=L=LO', + 'sessionId=e8bb43229de9; Domain=foo.example.com' + ] + }, + ); + expect( + profile.responseData.headers, + { + 'set-cookie': [ + 'id=a3fWa; Expires=Wed, 21 Oct 2015 07:28:00 GMT; Path=/,,HE,=L=LO', + 'sessionId=e8bb43229de9; Domain=foo.example.com' + ] + }, + ); - final headers = responseData['headers'] as Map>; - expect(headers, { - 'set-cookie': [ - 'id=a3fWa; Expires=Wed, 21 Oct 2015 07:28:00 GMT; Path=/,,HE,=L=LO', - 'sessionId=e8bb43229de9; Domain=foo.example.com' - ] - }); + profile.responseData.headersCommaValues = null; + expect(responseData['headers'], isNull); + expect(profile.responseData.headers, isNull); }); test('populating HttpClientRequestProfile.responseData.compressionState', () async { final responseData = backingMap['responseData'] as Map; expect(responseData['compressionState'], isNull); + expect(profile.responseData.compressionState, isNull); profile.responseData.compressionState = HttpClientResponseCompressionState.decompressed; expect(responseData['compressionState'], 'decompressed'); + expect( + profile.responseData.compressionState, + HttpClientResponseCompressionState.decompressed, + ); + }); + + test('HttpClientRequestProfile.responseData.compressionState = null', + () async { + final responseData = backingMap['responseData'] as Map; + + profile.responseData.compressionState = + HttpClientResponseCompressionState.decompressed; + expect(responseData['compressionState'], 'decompressed'); + expect( + profile.responseData.compressionState, + HttpClientResponseCompressionState.decompressed, + ); + + profile.responseData.compressionState = null; + expect(responseData['compressionState'], isNull); + expect(profile.responseData.compressionState, isNull); }); test('populating HttpClientRequestProfile.responseData.reasonPhrase', () async { final responseData = backingMap['responseData'] as Map; expect(responseData['reasonPhrase'], isNull); + expect(profile.responseData.reasonPhrase, isNull); profile.responseData.reasonPhrase = 'OK'; expect(responseData['reasonPhrase'], 'OK'); + expect(profile.responseData.reasonPhrase, 'OK'); + }); + + test('HttpClientRequestProfile.responseData.reasonPhrase = null', () async { + final responseData = backingMap['responseData'] as Map; + + profile.responseData.reasonPhrase = 'OK'; + expect(responseData['reasonPhrase'], 'OK'); + expect(profile.responseData.reasonPhrase, 'OK'); + + profile.responseData.reasonPhrase = null; + expect(responseData['reasonPhrase'], isNull); + expect(profile.responseData.reasonPhrase, isNull); }); test('populating HttpClientRequestProfile.responseData.isRedirect', () async { final responseData = backingMap['responseData'] as Map; expect(responseData['isRedirect'], isNull); + expect(profile.responseData.isRedirect, isNull); profile.responseData.isRedirect = true; expect(responseData['isRedirect'], true); + expect(profile.responseData.isRedirect, true); + }); + + test('HttpClientRequestProfile.responseData.isRedirect = null', () async { + final responseData = backingMap['responseData'] as Map; + + profile.responseData.isRedirect = true; + expect(responseData['isRedirect'], true); + expect(profile.responseData.isRedirect, true); + + profile.responseData.isRedirect = null; + expect(responseData['isRedirect'], isNull); + expect(profile.responseData.isRedirect, isNull); }); test('populating HttpClientRequestProfile.responseData.persistentConnection', () async { final responseData = backingMap['responseData'] as Map; expect(responseData['persistentConnection'], isNull); + expect(profile.responseData.persistentConnection, isNull); profile.responseData.persistentConnection = true; expect(responseData['persistentConnection'], true); + expect(profile.responseData.persistentConnection, true); + }); + + test('HttpClientRequestProfile.responseData.persistentConnection = null', + () async { + final responseData = backingMap['responseData'] as Map; + + profile.responseData.persistentConnection = true; + expect(responseData['persistentConnection'], true); + expect(profile.responseData.persistentConnection, true); + + profile.responseData.persistentConnection = null; + expect(responseData['persistentConnection'], isNull); + expect(profile.responseData.persistentConnection, isNull); }); test('populating HttpClientRequestProfile.responseData.contentLength', () async { final responseData = backingMap['responseData'] as Map; expect(responseData['contentLength'], isNull); + expect(profile.responseData.contentLength, isNull); profile.responseData.contentLength = 1200; expect(responseData['contentLength'], 1200); + expect(profile.responseData.contentLength, 1200); }); - test('HttpClientRequestProfile.responseData.contentLength = nil', () async { + test('HttpClientRequestProfile.responseData.contentLength = null', () async { final responseData = backingMap['responseData'] as Map; + profile.responseData.contentLength = 1200; expect(responseData['contentLength'], 1200); + expect(profile.responseData.contentLength, 1200); profile.responseData.contentLength = null; expect(responseData['contentLength'], isNull); + expect(profile.responseData.contentLength, isNull); }); test('populating HttpClientRequestProfile.responseData.statusCode', () async { final responseData = backingMap['responseData'] as Map; expect(responseData['statusCode'], isNull); + expect(profile.responseData.statusCode, isNull); profile.responseData.statusCode = 200; expect(responseData['statusCode'], 200); + expect(profile.responseData.statusCode, 200); + }); + + test('HttpClientRequestProfile.responseData.statusCode = null', () async { + final responseData = backingMap['responseData'] as Map; + + profile.responseData.statusCode = 200; + expect(responseData['statusCode'], 200); + expect(profile.responseData.statusCode, 200); + + profile.responseData.statusCode = null; + expect(responseData['statusCode'], isNull); + expect(profile.responseData.statusCode, isNull); }); test('populating HttpClientRequestProfile.responseData.startTime', () async { final responseData = backingMap['responseData'] as Map; expect(responseData['startTime'], isNull); + expect(profile.responseData.startTime, isNull); profile.responseData.startTime = DateTime.parse('2024-03-21'); @@ -180,11 +393,28 @@ void main() { responseData['startTime'], DateTime.parse('2024-03-21').microsecondsSinceEpoch, ); + expect(profile.responseData.startTime, DateTime.parse('2024-03-21')); + }); + + test('HttpClientRequestProfile.responseData.startTime = null', () async { + final responseData = backingMap['responseData'] as Map; + + profile.responseData.startTime = DateTime.parse('2024-03-21'); + expect( + responseData['startTime'], + DateTime.parse('2024-03-21').microsecondsSinceEpoch, + ); + expect(profile.responseData.startTime, DateTime.parse('2024-03-21')); + + profile.responseData.startTime = null; + expect(responseData['startTime'], isNull); + expect(profile.responseData.startTime, isNull); }); test('populating HttpClientRequestProfile.responseData.endTime', () async { final responseData = backingMap['responseData'] as Map; expect(responseData['endTime'], isNull); + expect(profile.responseData.endTime, isNull); await profile.responseData.close(DateTime.parse('2024-03-23')); @@ -192,15 +422,18 @@ void main() { responseData['endTime'], DateTime.parse('2024-03-23').microsecondsSinceEpoch, ); + expect(profile.responseData.endTime, DateTime.parse('2024-03-23')); }); test('populating HttpClientRequestProfile.responseData.error', () async { final responseData = backingMap['responseData'] as Map; expect(responseData['error'], isNull); + expect(profile.responseData.error, isNull); await profile.responseData.closeWithError('failed'); expect(responseData['error'], 'failed'); + expect(profile.responseData.error, 'failed'); }); test('using HttpClientRequestProfile.responseData.bodySink', () async {