diff --git a/doc/dev_guide/api_endpoints_for_testing.md b/doc/dev_guide/api_endpoints_for_testing.md index 5e0c1378c..ab953ab5a 100644 --- a/doc/dev_guide/api_endpoints_for_testing.md +++ b/doc/dev_guide/api_endpoints_for_testing.md @@ -47,3 +47,14 @@ A List of API endpoints that can be used for testing API Dash ## SSE - https://sse.dev + +## Auth + - **Bearer** + - https://httpbin.org/bearer + + - **Basic Auth** + - https://httpbin.org/basic-auth/{username}/{password} + + - **Digest Auth** + - https://httpbin.org/digest-auth/{qop}/{usenamer}/{password}/{algorithm} + diff --git a/lib/consts.dart b/lib/consts.dart index 3e9dcacd4..c5a829d09 100644 --- a/lib/consts.dart +++ b/lib/consts.dart @@ -448,6 +448,7 @@ const kLabelURLParams = "Params"; const kLabelHeaders = "Headers"; const kLabelBody = "Body"; const kLabelScripts = "Scripts"; +const kLabelAuth = "Auth"; const kLabelQuery = "Query"; const kNameCheckbox = "Checkbox"; const kNameURLParam = "URL Parameter"; diff --git a/lib/models/history_request_model.dart b/lib/models/history_request_model.dart index 1ffc54608..f46382a72 100644 --- a/lib/models/history_request_model.dart +++ b/lib/models/history_request_model.dart @@ -18,6 +18,7 @@ class HistoryRequestModel with _$HistoryRequestModel { required HttpResponseModel httpResponseModel, String? preRequestScript, String? postRequestScript, + AuthModel? authModel, }) = _HistoryRequestModel; factory HistoryRequestModel.fromJson(Map json) => diff --git a/lib/models/history_request_model.freezed.dart b/lib/models/history_request_model.freezed.dart index ac43f3eee..020ea13de 100644 --- a/lib/models/history_request_model.freezed.dart +++ b/lib/models/history_request_model.freezed.dart @@ -26,6 +26,7 @@ mixin _$HistoryRequestModel { HttpResponseModel get httpResponseModel => throw _privateConstructorUsedError; String? get preRequestScript => throw _privateConstructorUsedError; String? get postRequestScript => throw _privateConstructorUsedError; + AuthModel? get authModel => throw _privateConstructorUsedError; /// Serializes this HistoryRequestModel to a JSON map. Map toJson() => throw _privateConstructorUsedError; @@ -49,11 +50,13 @@ abstract class $HistoryRequestModelCopyWith<$Res> { HttpRequestModel httpRequestModel, HttpResponseModel httpResponseModel, String? preRequestScript, - String? postRequestScript}); + String? postRequestScript, + AuthModel? authModel}); $HistoryMetaModelCopyWith<$Res> get metaData; $HttpRequestModelCopyWith<$Res> get httpRequestModel; $HttpResponseModelCopyWith<$Res> get httpResponseModel; + $AuthModelCopyWith<$Res>? get authModel; } /// @nodoc @@ -77,6 +80,7 @@ class _$HistoryRequestModelCopyWithImpl<$Res, $Val extends HistoryRequestModel> Object? httpResponseModel = null, Object? preRequestScript = freezed, Object? postRequestScript = freezed, + Object? authModel = freezed, }) { return _then(_value.copyWith( historyId: null == historyId @@ -103,6 +107,10 @@ class _$HistoryRequestModelCopyWithImpl<$Res, $Val extends HistoryRequestModel> ? _value.postRequestScript : postRequestScript // ignore: cast_nullable_to_non_nullable as String?, + authModel: freezed == authModel + ? _value.authModel + : authModel // ignore: cast_nullable_to_non_nullable + as AuthModel?, ) as $Val); } @@ -135,6 +143,20 @@ class _$HistoryRequestModelCopyWithImpl<$Res, $Val extends HistoryRequestModel> return _then(_value.copyWith(httpResponseModel: value) as $Val); }); } + + /// Create a copy of HistoryRequestModel + /// with the given fields replaced by the non-null parameter values. + @override + @pragma('vm:prefer-inline') + $AuthModelCopyWith<$Res>? get authModel { + if (_value.authModel == null) { + return null; + } + + return $AuthModelCopyWith<$Res>(_value.authModel!, (value) { + return _then(_value.copyWith(authModel: value) as $Val); + }); + } } /// @nodoc @@ -151,7 +173,8 @@ abstract class _$$HistoryRequestModelImplCopyWith<$Res> HttpRequestModel httpRequestModel, HttpResponseModel httpResponseModel, String? preRequestScript, - String? postRequestScript}); + String? postRequestScript, + AuthModel? authModel}); @override $HistoryMetaModelCopyWith<$Res> get metaData; @@ -159,6 +182,8 @@ abstract class _$$HistoryRequestModelImplCopyWith<$Res> $HttpRequestModelCopyWith<$Res> get httpRequestModel; @override $HttpResponseModelCopyWith<$Res> get httpResponseModel; + @override + $AuthModelCopyWith<$Res>? get authModel; } /// @nodoc @@ -180,6 +205,7 @@ class __$$HistoryRequestModelImplCopyWithImpl<$Res> Object? httpResponseModel = null, Object? preRequestScript = freezed, Object? postRequestScript = freezed, + Object? authModel = freezed, }) { return _then(_$HistoryRequestModelImpl( historyId: null == historyId @@ -206,6 +232,10 @@ class __$$HistoryRequestModelImplCopyWithImpl<$Res> ? _value.postRequestScript : postRequestScript // ignore: cast_nullable_to_non_nullable as String?, + authModel: freezed == authModel + ? _value.authModel + : authModel // ignore: cast_nullable_to_non_nullable + as AuthModel?, )); } } @@ -220,7 +250,8 @@ class _$HistoryRequestModelImpl implements _HistoryRequestModel { required this.httpRequestModel, required this.httpResponseModel, this.preRequestScript, - this.postRequestScript}); + this.postRequestScript, + this.authModel}); factory _$HistoryRequestModelImpl.fromJson(Map json) => _$$HistoryRequestModelImplFromJson(json); @@ -237,10 +268,12 @@ class _$HistoryRequestModelImpl implements _HistoryRequestModel { final String? preRequestScript; @override final String? postRequestScript; + @override + final AuthModel? authModel; @override String toString() { - return 'HistoryRequestModel(historyId: $historyId, metaData: $metaData, httpRequestModel: $httpRequestModel, httpResponseModel: $httpResponseModel, preRequestScript: $preRequestScript, postRequestScript: $postRequestScript)'; + return 'HistoryRequestModel(historyId: $historyId, metaData: $metaData, httpRequestModel: $httpRequestModel, httpResponseModel: $httpResponseModel, preRequestScript: $preRequestScript, postRequestScript: $postRequestScript, authModel: $authModel)'; } @override @@ -259,13 +292,22 @@ class _$HistoryRequestModelImpl implements _HistoryRequestModel { (identical(other.preRequestScript, preRequestScript) || other.preRequestScript == preRequestScript) && (identical(other.postRequestScript, postRequestScript) || - other.postRequestScript == postRequestScript)); + other.postRequestScript == postRequestScript) && + (identical(other.authModel, authModel) || + other.authModel == authModel)); } @JsonKey(includeFromJson: false, includeToJson: false) @override - int get hashCode => Object.hash(runtimeType, historyId, metaData, - httpRequestModel, httpResponseModel, preRequestScript, postRequestScript); + int get hashCode => Object.hash( + runtimeType, + historyId, + metaData, + httpRequestModel, + httpResponseModel, + preRequestScript, + postRequestScript, + authModel); /// Create a copy of HistoryRequestModel /// with the given fields replaced by the non-null parameter values. @@ -291,7 +333,8 @@ abstract class _HistoryRequestModel implements HistoryRequestModel { required final HttpRequestModel httpRequestModel, required final HttpResponseModel httpResponseModel, final String? preRequestScript, - final String? postRequestScript}) = _$HistoryRequestModelImpl; + final String? postRequestScript, + final AuthModel? authModel}) = _$HistoryRequestModelImpl; factory _HistoryRequestModel.fromJson(Map json) = _$HistoryRequestModelImpl.fromJson; @@ -308,6 +351,8 @@ abstract class _HistoryRequestModel implements HistoryRequestModel { String? get preRequestScript; @override String? get postRequestScript; + @override + AuthModel? get authModel; /// Create a copy of HistoryRequestModel /// with the given fields replaced by the non-null parameter values. diff --git a/lib/models/history_request_model.g.dart b/lib/models/history_request_model.g.dart index 705a20222..66aee83b7 100644 --- a/lib/models/history_request_model.g.dart +++ b/lib/models/history_request_model.g.dart @@ -17,6 +17,10 @@ _$HistoryRequestModelImpl _$$HistoryRequestModelImplFromJson(Map json) => Map.from(json['httpResponseModel'] as Map)), preRequestScript: json['preRequestScript'] as String?, postRequestScript: json['postRequestScript'] as String?, + authModel: json['authModel'] == null + ? null + : AuthModel.fromJson( + Map.from(json['authModel'] as Map)), ); Map _$$HistoryRequestModelImplToJson( @@ -28,4 +32,5 @@ Map _$$HistoryRequestModelImplToJson( 'httpResponseModel': instance.httpResponseModel.toJson(), 'preRequestScript': instance.preRequestScript, 'postRequestScript': instance.postRequestScript, + 'authModel': instance.authModel?.toJson(), }; diff --git a/lib/providers/collection_providers.dart b/lib/providers/collection_providers.dart index 1e23a5ed4..263575745 100644 --- a/lib/providers/collection_providers.dart +++ b/lib/providers/collection_providers.dart @@ -207,6 +207,7 @@ class CollectionStateNotifier String? id, HTTPVerb? method, APIType? apiType, + AuthModel? authModel, String? url, String? name, String? description, @@ -242,6 +243,7 @@ class CollectionStateNotifier url: url ?? currentHttpRequestModel.url, headers: headers ?? currentHttpRequestModel.headers, params: params ?? currentHttpRequestModel.params, + authModel: authModel ?? currentHttpRequestModel.authModel, isHeaderEnabledList: isHeaderEnabledList ?? currentHttpRequestModel.isHeaderEnabledList, isParamEnabledList: @@ -315,6 +317,7 @@ class CollectionStateNotifier var responseRec = await sendHttpRequest( requestId, apiType, + requestModel.httpRequestModel?.authModel, substitutedHttpRequestModel, defaultUriScheme: defaultUriScheme, noSSL: noSSL, @@ -356,8 +359,11 @@ class CollectionStateNotifier httpResponseModel: httpResponseModel, preRequestScript: requestModel.preRequestScript, postRequestScript: requestModel.postRequestScript, + authModel: requestModel.httpRequestModel?.authModel, ); + ref.read(historyMetaStateNotifier.notifier).addHistoryRequest(model); + if (!requestModel.postRequestScript.isNullOrEmpty()) { newRequestModel = await handlePostResponseScript( newRequestModel, @@ -373,7 +379,6 @@ class CollectionStateNotifier }, ); } - ref.read(historyMetaStateNotifier.notifier).addHistoryRequest(model); } // update state with response data @@ -444,6 +449,7 @@ class CollectionStateNotifier : (state?[id]?.copyWith(httpResponseModel: null))?.toJson(), ); } + await hiveHandler.removeUnused(); ref.read(saveDataStateProvider.notifier).state = false; ref.read(hasUnsavedChangesProvider.notifier).state = false; diff --git a/lib/screens/common_widgets/auth/api_key_auth_fields.dart b/lib/screens/common_widgets/auth/api_key_auth_fields.dart new file mode 100644 index 000000000..edf13de99 --- /dev/null +++ b/lib/screens/common_widgets/auth/api_key_auth_fields.dart @@ -0,0 +1,103 @@ +import 'package:flutter/material.dart'; +import 'package:apidash_core/apidash_core.dart'; +import 'package:apidash_design_system/apidash_design_system.dart'; +import 'package:apidash/widgets/widgets.dart'; +import 'consts.dart'; + +class ApiKeyAuthFields extends StatefulWidget { + final AuthModel? authData; + final bool readOnly; + final Function(AuthModel?)? updateAuth; + + const ApiKeyAuthFields( + {super.key, + required this.authData, + this.updateAuth, + this.readOnly = false}); + + @override + State createState() => _ApiKeyAuthFieldsState(); +} + +class _ApiKeyAuthFieldsState extends State { + late TextEditingController _keyController; + late TextEditingController _nameController; + late String _addKeyTo; + + @override + void initState() { + super.initState(); + final apiAuth = widget.authData?.apikey; + _keyController = TextEditingController(text: apiAuth?.key ?? ''); + _nameController = + TextEditingController(text: apiAuth?.name ?? kApiKeyHeaderName); + _addKeyTo = apiAuth?.location ?? kAddToDefaultLocation; + } + + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + kLabelAddTo, + style: TextStyle( + fontWeight: FontWeight.normal, + fontSize: 14, + ), + ), + SizedBox( + height: 4, + ), + ADPopupMenu( + value: kAddToLocationsMap[_addKeyTo], + values: kAddToLocations, + tooltip: kTooltipApiKeyAuth, + isOutlined: true, + onChanged: widget.readOnly + ? null + : (String? newLocation) { + if (newLocation != null) { + setState(() { + _addKeyTo = newLocation; + }); + _updateApiKeyAuth(); + } + }, + ), + const SizedBox(height: 16), + AuthTextField( + readOnly: widget.readOnly, + controller: _nameController, + hintText: kHintTextFieldName, + onChanged: (value) => _updateApiKeyAuth(), + ), + const SizedBox(height: 16), + AuthTextField( + readOnly: widget.readOnly, + controller: _keyController, + title: kLabelApiKey, + hintText: kHintTextKey, + isObscureText: true, + onChanged: (value) => _updateApiKeyAuth(), + ), + ], + ); + } + + void _updateApiKeyAuth() { + final apiKey = AuthApiKeyModel( + key: _keyController.text.trim(), + name: _nameController.text.trim(), + location: _addKeyTo, + ); + widget.updateAuth?.call(widget.authData?.copyWith( + type: APIAuthType.apiKey, + apikey: apiKey, + ) ?? + AuthModel( + type: APIAuthType.apiKey, + apikey: apiKey, + )); + } +} diff --git a/lib/screens/common_widgets/auth/auth.dart b/lib/screens/common_widgets/auth/auth.dart new file mode 100644 index 000000000..0ea643fd5 --- /dev/null +++ b/lib/screens/common_widgets/auth/auth.dart @@ -0,0 +1,6 @@ +export 'api_key_auth_fields.dart'; +export 'auth_page.dart'; +export 'basic_auth_fields.dart'; +export 'bearer_auth_fields.dart'; +export 'digest_auth_fields.dart'; +export 'jwt_auth_fields.dart'; diff --git a/lib/screens/common_widgets/auth/auth_page.dart b/lib/screens/common_widgets/auth/auth_page.dart new file mode 100644 index 000000000..e34b9f06f --- /dev/null +++ b/lib/screens/common_widgets/auth/auth_page.dart @@ -0,0 +1,89 @@ +import 'package:apidash_design_system/apidash_design_system.dart'; +import 'package:flutter/material.dart'; +import 'package:apidash_core/apidash_core.dart'; +import 'api_key_auth_fields.dart'; +import 'basic_auth_fields.dart'; +import 'bearer_auth_fields.dart'; +import 'digest_auth_fields.dart'; +import 'jwt_auth_fields.dart'; +import 'consts.dart'; + +class AuthPage extends StatelessWidget { + final AuthModel? authModel; + final bool readOnly; + final Function(APIAuthType? newType)? onChangedAuthType; + final Function(AuthModel? model)? updateAuthData; + + const AuthPage({ + super.key, + this.authModel, + this.readOnly = false, + this.onChangedAuthType, + this.updateAuthData, + }); + + @override + Widget build(BuildContext context) { + return SingleChildScrollView( + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + kLabelSelectAuthType, + style: TextStyle( + fontWeight: FontWeight.bold, + ), + ), + SizedBox( + height: 8, + ), + ADPopupMenu( + value: authModel?.type.displayType, + values: APIAuthType.values + .map((type) => (type, type.displayType)) + .toList(), + tooltip: kTooltipSelectAuth, + isOutlined: true, + onChanged: readOnly ? null : onChangedAuthType, + ), + const SizedBox(height: 48), + switch (authModel?.type) { + APIAuthType.basic => BasicAuthFields( + readOnly: readOnly, + authData: authModel, + updateAuth: updateAuthData, + ), + APIAuthType.bearer => BearerAuthFields( + readOnly: readOnly, + authData: authModel, + updateAuth: updateAuthData, + ), + APIAuthType.apiKey => ApiKeyAuthFields( + readOnly: readOnly, + authData: authModel, + updateAuth: updateAuthData, + ), + APIAuthType.jwt => JwtAuthFields( + readOnly: readOnly, + authData: authModel, + updateAuth: updateAuthData, + ), + APIAuthType.digest => DigestAuthFields( + readOnly: readOnly, + authData: authModel, + updateAuth: updateAuthData, + ), + APIAuthType.none => + Text(readOnly ? kMsgNoAuth : kMsgNoAuthSelected), + _ => Text(readOnly + ? "${authModel?.type.name} $kMsgAuthNotSupported" + : kMsgNotImplemented), + } + ], + ), + ), + ); + } +} diff --git a/lib/screens/common_widgets/auth/basic_auth_fields.dart b/lib/screens/common_widgets/auth/basic_auth_fields.dart new file mode 100644 index 000000000..5be5f1e48 --- /dev/null +++ b/lib/screens/common_widgets/auth/basic_auth_fields.dart @@ -0,0 +1,71 @@ +import 'package:flutter/material.dart'; +import 'package:apidash_core/apidash_core.dart'; +import 'package:apidash/widgets/widgets.dart'; +import 'consts.dart'; + +class BasicAuthFields extends StatelessWidget { + final AuthModel? authData; + final Function(AuthModel?)? updateAuth; + final bool readOnly; + + const BasicAuthFields({ + super.key, + required this.authData, + this.updateAuth, + this.readOnly = false, + }); + + @override + Widget build(BuildContext context) { + final usernameController = TextEditingController( + text: authData?.basic?.username ?? '', + ); + final passwordController = TextEditingController( + text: authData?.basic?.password ?? '', + ); + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + AuthTextField( + readOnly: readOnly, + hintText: kHintUsername, + controller: usernameController, + onChanged: (_) => _updateBasicAuth( + usernameController, + passwordController, + ), + ), + const SizedBox(height: 16), + AuthTextField( + readOnly: readOnly, + hintText: kHintPassword, + isObscureText: true, + controller: passwordController, + onChanged: (_) => _updateBasicAuth( + usernameController, + passwordController, + ), + ), + ], + ); + } + + void _updateBasicAuth( + TextEditingController usernameController, + TextEditingController passwordController, + ) { + final basicAuth = AuthBasicAuthModel( + username: usernameController.text.trim(), + password: passwordController.text.trim(), + ); + updateAuth?.call(authData?.copyWith( + type: APIAuthType.basic, + basic: basicAuth, + ) ?? + AuthModel( + type: APIAuthType.basic, + basic: basicAuth, + )); + } +} diff --git a/lib/screens/common_widgets/auth/bearer_auth_fields.dart b/lib/screens/common_widgets/auth/bearer_auth_fields.dart new file mode 100644 index 000000000..2ab8cdb1e --- /dev/null +++ b/lib/screens/common_widgets/auth/bearer_auth_fields.dart @@ -0,0 +1,56 @@ +import 'package:flutter/material.dart'; +import 'package:apidash_core/apidash_core.dart'; +import 'package:apidash/widgets/widgets.dart'; +import 'consts.dart'; + +class BearerAuthFields extends StatefulWidget { + final AuthModel? authData; + final Function(AuthModel?)? updateAuth; + final bool readOnly; + + const BearerAuthFields({ + super.key, + required this.authData, + this.updateAuth, + this.readOnly = false, + }); + + @override + State createState() => _BearerAuthFieldsState(); +} + +class _BearerAuthFieldsState extends State { + late TextEditingController _tokenController; + + @override + void initState() { + super.initState(); + final bearerAuth = widget.authData?.bearer; + _tokenController = TextEditingController(text: bearerAuth?.token ?? ''); + } + + @override + Widget build(BuildContext context) { + return AuthTextField( + readOnly: widget.readOnly, + controller: _tokenController, + hintText: kHintToken, + isObscureText: true, + onChanged: (value) => _updateBearerAuth(), + ); + } + + void _updateBearerAuth() { + final bearer = AuthBearerModel( + token: _tokenController.text.trim(), + ); + widget.updateAuth?.call(widget.authData?.copyWith( + type: APIAuthType.bearer, + bearer: bearer, + ) ?? + AuthModel( + type: APIAuthType.bearer, + bearer: bearer, + )); + } +} diff --git a/lib/screens/common_widgets/auth/consts.dart b/lib/screens/common_widgets/auth/consts.dart new file mode 100644 index 000000000..479f09801 --- /dev/null +++ b/lib/screens/common_widgets/auth/consts.dart @@ -0,0 +1,73 @@ +const kEmpty = ''; + +// API Key Auth +const kApiKeyHeaderName = 'x-api-key'; +const kAddToLocations = [ + ('header', 'Header'), + ('query', 'Query Params'), +]; +final kAddToDefaultLocation = kAddToLocations[0].$1; +final kAddToLocationsMap = {for (var v in kAddToLocations) v.$1: v.$2}; +const kLabelAddTo = "Add to"; +const kTooltipApiKeyAuth = "Select where to add API key"; +const kHintTextFieldName = "Header/Query Param Name"; +const kLabelApiKey = "API Key"; +const kHintTextKey = "Key"; + +// Username-password auth +const kHintUsername = "Username"; +const kHintPassword = "Password"; + +// Bearer Token AUth +const kHintToken = "Token"; + +// Digest Auth +const kInfoDigestUsername = + "Your username for digest authentication. This will be sent to the server for credential verification."; +const kInfoDigestPassword = + "Your password for digest authentication. This is hashed and not sent in plain text to the server."; +const kHintRealm = "Realm"; +const kInfoDigestRealm = + "Authentication realm as specified by the server. This defines the protection space for the credentials."; +const kHintNonce = "Nonce"; +const kInfoDigestNonce = + "Server-generated random value used to prevent replay attacks."; +const kAlgorithm = "Algorithm"; +const kTooltipAlgorithm = "Algorithm that will be used to produce the digest"; +const kHintQop = "QOP"; +const kInfoDigestQop = + "Quality of Protection. Typically 'auth' for authentication only, or 'auth-int' for authentication with integrity protection."; +const kHintDataString = "Opaque"; +const kInfoDigestDataString = + "Server-specified data string that should be returned unchanged in the authorization header. Usually obtained from server's 401 response."; + +// JWT Auth +const kMsgAddToken = "Add JWT token to"; +const kTooltipTokenAddTo = "Select where to add JWT token"; +const kTextAlgo = "Algorithm"; +const kTooltipJWTAlgo = "Select JWT algorithm"; +const kStartAlgo = "HS"; +const kHintSecret = "Secret Key"; +const kInfoSecret = + "The secret key used to sign the JWT token. Keep this secure and match it with your server configuration."; +const kMsgSecret = "Secret is Base64 encoded"; +const kMsgPrivateKey = "Private Key"; +const kHintRSA = ''' +-----BEGIN RSA PRIVATE KEY----- +Private Key in PKCS#8 PEM Format +-----END RSA PRIVATE KEY----- +'''; +const kMsgPayload = "Payload (JSON format)"; +const kHintJson = + '{"sub": "1234567890", "name": "John Doe", "iat": 1516239022}'; +const kHeaderPrefix = 'Bearer'; +const kQueryParamKey = 'token'; + +//AuthPAge +const kLabelSelectAuthType = "Authentication Type"; +const kTooltipSelectAuth = "Select Authentication Type"; +const kMsgNoAuth = "No authentication was used for this request."; +const kMsgNoAuthSelected = "No authentication selected."; +const kMsgAuthNotSupported = + "authentication details are not yet supported in history view."; +const kMsgNotImplemented = "This auth type is not implemented yet."; diff --git a/lib/screens/common_widgets/auth/digest_auth_fields.dart b/lib/screens/common_widgets/auth/digest_auth_fields.dart new file mode 100644 index 000000000..8b060ccca --- /dev/null +++ b/lib/screens/common_widgets/auth/digest_auth_fields.dart @@ -0,0 +1,148 @@ +import 'package:flutter/material.dart'; +import 'package:apidash_core/apidash_core.dart'; +import 'package:apidash_design_system/apidash_design_system.dart'; +import 'package:apidash/widgets/widgets.dart'; +import 'consts.dart'; + +class DigestAuthFields extends StatefulWidget { + final AuthModel? authData; + final bool readOnly; + final Function(AuthModel?)? updateAuth; + + const DigestAuthFields({ + super.key, + required this.authData, + this.updateAuth, + this.readOnly = false, + }); + + @override + State createState() => _DigestAuthFieldsState(); +} + +class _DigestAuthFieldsState extends State { + late TextEditingController _usernameController; + late TextEditingController _passwordController; + late TextEditingController _realmController; + late TextEditingController _nonceController; + late String _algorithmController; + late TextEditingController _qopController; + late TextEditingController _opaqueController; + + @override + void initState() { + super.initState(); + final digest = widget.authData?.digest; + _usernameController = TextEditingController(text: digest?.username ?? ''); + _passwordController = TextEditingController(text: digest?.password ?? ''); + _realmController = TextEditingController(text: digest?.realm ?? ''); + _nonceController = TextEditingController(text: digest?.nonce ?? ''); + _algorithmController = digest?.algorithm ?? kDigestAlgos[0]; + _qopController = TextEditingController(text: digest?.qop ?? kQop[0]); + _opaqueController = TextEditingController(text: digest?.opaque ?? ''); + } + + @override + Widget build(BuildContext context) { + return SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + AuthTextField( + readOnly: widget.readOnly, + controller: _usernameController, + hintText: kHintUsername, + infoText: kInfoDigestUsername, + onChanged: (_) => _updateDigestAuth(), + ), + const SizedBox(height: 12), + AuthTextField( + readOnly: widget.readOnly, + controller: _passwordController, + hintText: kHintPassword, + isObscureText: true, + infoText: kInfoDigestPassword, + onChanged: (_) => _updateDigestAuth(), + ), + const SizedBox(height: 12), + AuthTextField( + readOnly: widget.readOnly, + controller: _realmController, + hintText: kHintRealm, + infoText: kInfoDigestRealm, + onChanged: (_) => _updateDigestAuth(), + ), + const SizedBox(height: 12), + AuthTextField( + readOnly: widget.readOnly, + controller: _nonceController, + hintText: kHintNonce, + infoText: kInfoDigestNonce, + onChanged: (_) => _updateDigestAuth(), + ), + const SizedBox(height: 12), + Text( + kAlgorithm, + style: TextStyle( + fontWeight: FontWeight.normal, + fontSize: 14, + ), + ), + SizedBox(height: 4), + ADPopupMenu( + value: _algorithmController.trim(), + values: kDigestAlgos.map((i) => (i, i)), + tooltip: kTooltipAlgorithm, + isOutlined: true, + onChanged: widget.readOnly + ? null + : (String? newLocation) { + if (newLocation != null) { + setState(() { + _algorithmController = newLocation; + }); + _updateDigestAuth(); + } + }, + ), + const SizedBox(height: 12), + AuthTextField( + readOnly: widget.readOnly, + controller: _qopController, + hintText: kHintQop, + infoText: kInfoDigestQop, + onChanged: (_) => _updateDigestAuth(), + ), + const SizedBox(height: 12), + AuthTextField( + readOnly: widget.readOnly, + controller: _opaqueController, + hintText: kHintDataString, + infoText: kInfoDigestDataString, + onChanged: (_) => _updateDigestAuth(), + ), + ], + ), + ); + } + + void _updateDigestAuth() { + final digest = AuthDigestModel( + username: _usernameController.text.trim(), + password: _passwordController.text.trim(), + realm: _realmController.text.trim(), + nonce: _nonceController.text.trim(), + algorithm: _algorithmController.trim(), + qop: _qopController.text.trim(), + opaque: _opaqueController.text.trim(), + ); + widget.updateAuth?.call(widget.authData?.copyWith( + type: APIAuthType.digest, + digest: digest, + ) ?? + AuthModel( + type: APIAuthType.digest, + digest: digest, + )); + } +} diff --git a/lib/screens/common_widgets/auth/jwt_auth_fields.dart b/lib/screens/common_widgets/auth/jwt_auth_fields.dart new file mode 100644 index 000000000..bff161575 --- /dev/null +++ b/lib/screens/common_widgets/auth/jwt_auth_fields.dart @@ -0,0 +1,231 @@ +import 'package:flutter/material.dart'; +import 'package:apidash_core/apidash_core.dart'; +import 'package:apidash_design_system/apidash_design_system.dart'; +import 'package:apidash/widgets/widgets.dart'; +import 'consts.dart'; + +class JwtAuthFields extends StatefulWidget { + final AuthModel? authData; + final Function(AuthModel?)? updateAuth; + final bool readOnly; + + const JwtAuthFields({ + super.key, + required this.authData, + this.updateAuth, + this.readOnly = false, + }); + + @override + State createState() => _JwtAuthFieldsState(); +} + +class _JwtAuthFieldsState extends State { + late TextEditingController _secretController; + late TextEditingController _privateKeyController; + late TextEditingController _payloadController; + late String _addTokenTo; + late String _algorithm; + late bool _isSecretBase64Encoded; + + @override + void initState() { + super.initState(); + final jwt = widget.authData?.jwt; + _secretController = TextEditingController(text: jwt?.secret ?? ''); + _privateKeyController = TextEditingController(text: jwt?.privateKey ?? ''); + _payloadController = TextEditingController(text: jwt?.payload ?? ''); + _addTokenTo = jwt?.addTokenTo ?? kAddToDefaultLocation; + _algorithm = jwt?.algorithm ?? kJwtAlgos[0]; + _isSecretBase64Encoded = jwt?.isSecretBase64Encoded ?? false; + } + + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + kMsgAddToken, + style: TextStyle( + fontWeight: FontWeight.normal, + fontSize: 14, + ), + ), + SizedBox(height: 4), + ADPopupMenu( + value: kAddToLocationsMap[_addTokenTo], + values: kAddToLocations, + tooltip: kTooltipTokenAddTo, + isOutlined: true, + onChanged: widget.readOnly + ? null + : (String? newAddTokenTo) { + if (newAddTokenTo != null) { + setState(() { + _addTokenTo = newAddTokenTo; + }); + _updateJwtAuth(); + } + }, + ), + const SizedBox(height: 16), + Text( + kTextAlgo, + style: TextStyle( + fontWeight: FontWeight.normal, + fontSize: 14, + ), + ), + SizedBox(height: 4), + ADPopupMenu( + value: _algorithm, + values: kJwtAlgos.map((i) => (i, i)), + tooltip: kTooltipJWTAlgo, + isOutlined: true, + onChanged: widget.readOnly + ? null + : (String? newAlgorithm) { + if (newAlgorithm != null) { + setState(() { + _algorithm = newAlgorithm; + }); + _updateJwtAuth(); + } + }, + ), + const SizedBox(height: 16), + if (_algorithm.startsWith(kStartAlgo)) ...[ + AuthTextField( + readOnly: widget.readOnly, + controller: _secretController, + isObscureText: true, + hintText: kHintSecret, + infoText: kInfoSecret, + onChanged: (value) => _updateJwtAuth(), + ), + const SizedBox(height: 16), + CheckboxListTile( + title: Text( + kMsgSecret, + style: TextStyle( + fontWeight: FontWeight.normal, + fontSize: 14, + ), + ), + value: _isSecretBase64Encoded, + contentPadding: EdgeInsets.zero, + controlAffinity: ListTileControlAffinity.leading, + onChanged: (bool? value) { + setState(() { + _isSecretBase64Encoded = value ?? false; + }); + + _updateJwtAuth(); + }, + ), + ] else ...[ + Text( + kMsgPrivateKey, + style: TextStyle( + fontWeight: FontWeight.normal, + fontSize: 14, + ), + ), + SizedBox(height: 4), + TextField( + readOnly: widget.readOnly, + controller: _privateKeyController, + maxLines: 5, + decoration: InputDecoration( + filled: true, + fillColor: Theme.of(context).colorScheme.surfaceContainerLowest, + constraints: BoxConstraints( + maxWidth: MediaQuery.sizeOf(context).width - 100, + ), + contentPadding: const EdgeInsets.all(18), + hintText: kHintRSA, + hintStyle: Theme.of(context).textTheme.bodyMedium, + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(8), + ), + focusedBorder: OutlineInputBorder( + borderSide: BorderSide( + color: Theme.of(context).colorScheme.outline, + ), + ), + enabledBorder: OutlineInputBorder( + borderSide: BorderSide( + color: Theme.of(context).colorScheme.surfaceContainerHighest, + ), + ), + ), + onChanged: (value) => _updateJwtAuth(), + ), + ], + const SizedBox(height: 16), + Text( + kMsgPayload, + style: TextStyle( + fontWeight: FontWeight.normal, + fontSize: 14, + ), + ), + SizedBox(height: 4), + TextField( + readOnly: widget.readOnly, + controller: _payloadController, + maxLines: 4, + decoration: InputDecoration( + filled: true, + fillColor: Theme.of(context).colorScheme.surfaceContainerLowest, + constraints: BoxConstraints( + maxWidth: MediaQuery.sizeOf(context).width - 100, + ), + contentPadding: const EdgeInsets.all(18), + hintText: kHintJson, + hintStyle: Theme.of(context).textTheme.bodyMedium, + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(8), + ), + focusedBorder: OutlineInputBorder( + borderSide: BorderSide( + color: Theme.of(context).colorScheme.outline, + ), + ), + enabledBorder: OutlineInputBorder( + borderSide: BorderSide( + color: Theme.of(context).colorScheme.surfaceContainerHighest, + ), + ), + ), + onChanged: (value) => _updateJwtAuth(), + ), + ], + ); + } + + void _updateJwtAuth() { + final jwt = AuthJwtModel( + secret: _secretController.text.trim(), + privateKey: _privateKeyController.text.trim(), + payload: _payloadController.text.trim(), + addTokenTo: _addTokenTo, + algorithm: _algorithm, + isSecretBase64Encoded: _isSecretBase64Encoded, + headerPrefix: kHeaderPrefix, + queryParamKey: kQueryParamKey, + header: '', + ); + widget.updateAuth?.call( + widget.authData?.copyWith( + type: APIAuthType.jwt, + jwt: jwt, + ) ?? + AuthModel( + type: APIAuthType.jwt, + jwt: jwt, + ), + ); + } +} diff --git a/lib/screens/common_widgets/common_widgets.dart b/lib/screens/common_widgets/common_widgets.dart index fc4c66b84..dcbf9c70f 100644 --- a/lib/screens/common_widgets/common_widgets.dart +++ b/lib/screens/common_widgets/common_widgets.dart @@ -1,3 +1,4 @@ +export 'auth/auth.dart'; export 'api_type_dropdown.dart'; export 'button_navbar.dart'; export 'code_pane.dart'; diff --git a/lib/screens/history/history_widgets/his_request_pane.dart b/lib/screens/history/history_widgets/his_request_pane.dart index c1e8995d5..6766ad1b0 100644 --- a/lib/screens/history/history_widgets/his_request_pane.dart +++ b/lib/screens/history/history_widgets/his_request_pane.dart @@ -5,6 +5,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:apidash/providers/providers.dart'; import 'package:apidash/widgets/widgets.dart'; import 'package:apidash/consts.dart'; +import '../../common_widgets/common_widgets.dart'; import 'his_scripts_tab.dart'; class HistoryRequestPane extends ConsumerWidget { @@ -45,6 +46,12 @@ class HistoryRequestPane extends ConsumerWidget { .select((value) => value?.postRequestScript?.length)) ?? 0; + final hasAuth = ref.watch(selectedHistoryRequestModelProvider + .select((value) => value?.authModel?.type != APIAuthType.none)); + + final authModel = ref.watch(selectedHistoryRequestModelProvider + .select((value) => value?.authModel)); + return switch (apiType) { APIType.rest => RequestPane( key: const Key("history-request-pane-rest"), @@ -57,12 +64,14 @@ class HistoryRequestPane extends ConsumerWidget { showViewCodeButton: !isCompact, showIndicators: [ paramLength > 0, + hasAuth, headerLength > 0, hasBody, - scriptsLength > 0 + scriptsLength > 0, ], tabLabels: const [ kLabelURLParams, + kLabelAuth, kLabelHeaders, kLabelBody, kLabelScripts, @@ -72,6 +81,10 @@ class HistoryRequestPane extends ConsumerWidget { rows: paramsMap, keyName: kNameURLParam, ), + AuthPage( + authModel: authModel, + readOnly: true, + ), RequestDataTable( rows: headersMap, keyName: kNameHeader, @@ -89,9 +102,15 @@ class HistoryRequestPane extends ConsumerWidget { !codePaneVisible; }, showViewCodeButton: !isCompact, - showIndicators: [headerLength > 0, hasQuery, scriptsLength > 0], + showIndicators: [ + headerLength > 0, + hasAuth, + hasQuery, + scriptsLength > 0 + ], tabLabels: const [ kLabelHeaders, + kLabelAuth, kLabelQuery, kLabelScripts, ], @@ -100,6 +119,10 @@ class HistoryRequestPane extends ConsumerWidget { rows: headersMap, keyName: kNameHeader, ), + AuthPage( + authModel: authModel, + readOnly: true, + ), const HisRequestBody(), const HistoryScriptsTab(), ], diff --git a/lib/screens/home_page/editor_pane/details_card/request_pane/request_auth.dart b/lib/screens/home_page/editor_pane/details_card/request_pane/request_auth.dart new file mode 100644 index 000000000..daf0e0d10 --- /dev/null +++ b/lib/screens/home_page/editor_pane/details_card/request_pane/request_auth.dart @@ -0,0 +1,53 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:apidash_core/apidash_core.dart'; +import 'package:apidash/providers/providers.dart'; +import '../../../../common_widgets/common_widgets.dart'; + +class EditAuthType extends ConsumerWidget { + final bool readOnly; + + const EditAuthType({ + super.key, + this.readOnly = false, + }); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final selectedRequest = ref.read(selectedRequestModelProvider); + if (selectedRequest == null) { + return const SizedBox.shrink(); + } + + ref.watch( + selectedRequestModelProvider.select((request) => + request?.httpRequestModel?.authModel?.type ?? APIAuthType.none), + ); + final currentAuthData = selectedRequest.httpRequestModel?.authModel; + + return AuthPage( + authModel: currentAuthData, + readOnly: readOnly, + onChangedAuthType: (newType) { + final selectedRequest = ref.read(selectedRequestModelProvider); + if (newType != null) { + ref.read(collectionStateNotifierProvider.notifier).update( + authModel: selectedRequest?.httpRequestModel?.authModel + ?.copyWith(type: newType) ?? + AuthModel(type: newType), + ); + } + }, + updateAuthData: (model) { + if (model == null) { + ref.read(collectionStateNotifierProvider.notifier).update( + authModel: AuthModel(type: APIAuthType.none), + ); + } + ref.read(collectionStateNotifierProvider.notifier).update( + authModel: model, + ); + }, + ); + } +} diff --git a/lib/screens/home_page/editor_pane/details_card/request_pane/request_pane_graphql.dart b/lib/screens/home_page/editor_pane/details_card/request_pane/request_pane_graphql.dart index fb8d7bc98..87ed31040 100644 --- a/lib/screens/home_page/editor_pane/details_card/request_pane/request_pane_graphql.dart +++ b/lib/screens/home_page/editor_pane/details_card/request_pane/request_pane_graphql.dart @@ -1,8 +1,10 @@ import 'package:apidash/consts.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:apidash_core/apidash_core.dart'; import 'package:apidash/providers/providers.dart'; import 'package:apidash/widgets/widgets.dart'; +import 'request_auth.dart'; import 'request_headers.dart'; import 'request_body.dart'; import 'request_scripts.dart'; @@ -29,6 +31,9 @@ class EditGraphQLRequestPane extends ConsumerWidget { .select((value) => value?.postRequestScript?.length)) ?? 0; + final hasAuth = ref.watch(selectedRequestModelProvider.select((value) => + value?.httpRequestModel?.authModel?.type != APIAuthType.none)); + if (tabIndex >= 3) { tabIndex = 0; } @@ -47,16 +52,19 @@ class EditGraphQLRequestPane extends ConsumerWidget { }, showIndicators: [ headerLength > 0, + hasAuth, hasQuery, scriptsLength > 0, ], tabLabels: const [ kLabelHeaders, + kLabelAuth, kLabelQuery, kLabelScripts, ], children: const [ EditRequestHeaders(), + EditAuthType(), EditRequestBody(), EditRequestScripts(), ], diff --git a/lib/screens/home_page/editor_pane/details_card/request_pane/request_pane_rest.dart b/lib/screens/home_page/editor_pane/details_card/request_pane/request_pane_rest.dart index 27d675676..aef0e67a1 100644 --- a/lib/screens/home_page/editor_pane/details_card/request_pane/request_pane_rest.dart +++ b/lib/screens/home_page/editor_pane/details_card/request_pane/request_pane_rest.dart @@ -1,4 +1,5 @@ import 'package:apidash/consts.dart'; +import 'package:apidash_core/apidash_core.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:apidash/providers/providers.dart'; @@ -6,6 +7,7 @@ import 'package:apidash/widgets/widgets.dart'; import 'request_headers.dart'; import 'request_params.dart'; import 'request_body.dart'; +import 'request_auth.dart'; import 'request_scripts.dart'; class EditRestRequestPane extends ConsumerWidget { @@ -34,6 +36,9 @@ class EditRestRequestPane extends ConsumerWidget { .select((value) => value?.postRequestScript?.length)) ?? 0; + final hasAuth = ref.watch(selectedRequestModelProvider.select((value) => + value?.httpRequestModel?.authModel?.type != APIAuthType.none)); + return RequestPane( selectedId: selectedId, codePaneVisible: codePaneVisible, @@ -49,18 +54,21 @@ class EditRestRequestPane extends ConsumerWidget { }, showIndicators: [ paramLength > 0, + hasAuth, headerLength > 0, hasBody, scriptsLength > 0, ], tabLabels: const [ kLabelURLParams, + kLabelAuth, kLabelHeaders, kLabelBody, kLabelScripts, ], children: const [ EditRequestURLParams(), + EditAuthType(), EditRequestHeaders(), EditRequestBody(), EditRequestScripts(), diff --git a/lib/widgets/field_auth.dart b/lib/widgets/field_auth.dart new file mode 100644 index 000000000..ef60bec0e --- /dev/null +++ b/lib/widgets/field_auth.dart @@ -0,0 +1,118 @@ +import 'package:flutter/material.dart'; +import 'package:apidash_design_system/apidash_design_system.dart'; + +class AuthTextField extends StatefulWidget { + final String hintText; + final String? title; + final TextEditingController controller; + final bool isObscureText; + final Function(String)? onChanged; + final bool readOnly; + final String? infoText; + + const AuthTextField( + {super.key, + this.title, + required this.hintText, + required this.controller, + required this.onChanged, + this.readOnly = false, + this.isObscureText = false, + this.infoText}); + + @override + State createState() => _AuthFieldState(); +} + +class _AuthFieldState extends State { + late bool _obscureText; + + @override + void initState() { + super.initState(); + _obscureText = widget.isObscureText; + } + + void _toggleVisibility() { + setState(() { + _obscureText = !_obscureText; + }); + } + + @override + Widget build(BuildContext context) { + return AutofillGroup( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + Text(widget.title ?? widget.hintText), + if (widget.infoText != null) + Tooltip( + message: widget.infoText!, + triggerMode: TooltipTriggerMode.tap, + showDuration: Duration(seconds: 5), + child: Padding( + padding: const EdgeInsets.only(left: 4.0), + child: Icon( + Icons.help_outline_rounded, + color: Theme.of(context).colorScheme.primaryFixedDim, + size: 14, + ), + ), + ), + ], + ), + const SizedBox(height: 6), + TextFormField( + readOnly: widget.readOnly, + controller: widget.controller, + style: kCodeStyle.copyWith( + color: Theme.of(context).colorScheme.onSurface, + fontSize: Theme.of(context).textTheme.bodyMedium?.fontSize, + ), + decoration: InputDecoration( + filled: true, + fillColor: Theme.of(context).colorScheme.surfaceContainerLowest, + constraints: BoxConstraints( + maxWidth: MediaQuery.sizeOf(context).width - 80), + contentPadding: kP10, + hintText: widget.hintText, + hintStyle: Theme.of(context).textTheme.bodySmall, + suffixIcon: widget.isObscureText + ? IconButton( + icon: Icon( + _obscureText ? Icons.visibility_off : Icons.visibility, + size: 20, + ), + onPressed: _toggleVisibility, + ) + : null, + focusedBorder: OutlineInputBorder( + borderSide: BorderSide( + color: Theme.of(context).colorScheme.outline, + ), + ), + enabledBorder: OutlineInputBorder( + borderSide: BorderSide( + color: Theme.of(context).colorScheme.surfaceContainerHighest, + ), + ), + ), + validator: (value) { + if (value!.isEmpty) { + return "${widget.hintText} cannot be empty!"; + } + return null; + }, + obscureText: _obscureText, + onChanged: widget.onChanged, + ), + ], + ), + ); + } +} diff --git a/lib/widgets/widgets.dart b/lib/widgets/widgets.dart index aea1f5b19..68c12aaf1 100644 --- a/lib/widgets/widgets.dart +++ b/lib/widgets/widgets.dart @@ -28,6 +28,7 @@ export 'editor_code.dart'; export 'editor_json.dart'; export 'editor.dart'; export 'error_message.dart'; +export 'field_auth.dart'; export 'field_cell_obscurable.dart'; export 'field_cell.dart'; export 'field_json_search.dart'; diff --git a/packages/apidash_core/lib/models/models.dart b/packages/apidash_core/lib/models/models.dart index c3206fd90..5bfff79e1 100644 --- a/packages/apidash_core/lib/models/models.dart +++ b/packages/apidash_core/lib/models/models.dart @@ -1 +1 @@ -export 'environment_model.dart'; +export 'environment_model.dart'; \ No newline at end of file diff --git a/packages/better_networking/better_networking_example/lib/main.dart b/packages/better_networking/better_networking_example/lib/main.dart index 61bb42bb4..996867e44 100644 --- a/packages/better_networking/better_networking_example/lib/main.dart +++ b/packages/better_networking/better_networking_example/lib/main.dart @@ -54,6 +54,7 @@ class _BetterNetworkingExampleState extends State { final (resp, duration, err) = await sendHttpRequest( 'G1', APIType.rest, + AuthModel(type: APIAuthType.none), HttpRequestModel( url: 'https://reqres.in/api/users/2', method: HTTPVerb.get, @@ -80,6 +81,7 @@ class _BetterNetworkingExampleState extends State { final (resp, duration, err) = await sendHttpRequest( 'P1', APIType.rest, + AuthModel(type: APIAuthType.none), HttpRequestModel( url: 'https://reqres.in/api/users', method: HTTPVerb.post, diff --git a/packages/better_networking/better_networking_example/pubspec.lock b/packages/better_networking/better_networking_example/pubspec.lock index 366394abb..10c4eb736 100644 --- a/packages/better_networking/better_networking_example/pubspec.lock +++ b/packages/better_networking/better_networking_example/pubspec.lock @@ -1,6 +1,14 @@ # Generated by pub # See https://dart.dev/tools/pub/glossary#lockfile packages: + adaptive_number: + dependency: transitive + description: + name: adaptive_number + sha256: "3a567544e9b5c9c803006f51140ad544aedc79604fd4f3f2c1380003f97c1d77" + url: "https://pub.dev" + source: hosted + version: "1.0.0" async: dependency: transitive description: @@ -48,6 +56,22 @@ packages: url: "https://pub.dev" source: hosted version: "1.19.1" + convert: + dependency: transitive + description: + name: convert + sha256: b30acd5944035672bc15c6b7a8b47d773e41e2f17de064350988c5d02adb1c68 + url: "https://pub.dev" + source: hosted + version: "3.1.2" + crypto: + dependency: transitive + description: + name: crypto + sha256: "1e445881f28f22d6140f181e07737b22f1e099a5e1ff94b0af2f9e4a463f4855" + url: "https://pub.dev" + source: hosted + version: "3.0.6" cupertino_icons: dependency: "direct main" description: @@ -56,6 +80,22 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.8" + dart_jsonwebtoken: + dependency: transitive + description: + name: dart_jsonwebtoken + sha256: "21ce9f8a8712f741e8d6876a9c82c0f8a257fe928c4378a91d8527b92a3fd413" + url: "https://pub.dev" + source: hosted + version: "3.2.0" + ed25519_edwards: + dependency: transitive + description: + name: ed25519_edwards + sha256: "6ce0112d131327ec6d42beede1e5dfd526069b18ad45dcf654f15074ad9276cd" + url: "https://pub.dev" + source: hosted + version: "0.3.1" fake_async: dependency: transitive description: @@ -64,6 +104,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.3.3" + fixnum: + dependency: transitive + description: + name: fixnum + sha256: b6dc7065e46c974bc7c5f143080a6764ec7a4be6da1285ececdc37be96de53be + url: "https://pub.dev" + source: hosted + version: "1.1.1" flutter: dependency: "direct main" description: flutter @@ -194,6 +242,14 @@ packages: url: "https://pub.dev" source: hosted version: "6.1.0" + pointycastle: + dependency: transitive + description: + name: pointycastle + sha256: "92aa3841d083cc4b0f4709b5c74fd6409a3e6ba833ffc7dc6a8fee096366acf5" + url: "https://pub.dev" + source: hosted + version: "4.0.0" seed: dependency: "direct overridden" description: diff --git a/packages/better_networking/lib/consts.dart b/packages/better_networking/lib/consts.dart index 61fe1e506..778dba2df 100644 --- a/packages/better_networking/lib/consts.dart +++ b/packages/better_networking/lib/consts.dart @@ -9,6 +9,40 @@ enum APIType { final String abbr; } +enum APIAuthType { + none("None"), + basic("Basic Auth"), + apiKey("API Key"), + bearer("Bearer Token"), + jwt("JWT Bearer"), + digest("Digest Auth"), + oauth1("OAuth 1.0"), + oauth2("OAuth 2.0"); + + const APIAuthType(this.displayType); + final String displayType; +} + +const kDigestAlgos = ['MD5', 'MD5-sess', 'SHA-256', 'SHA-256-sess']; +const kQop = ['auth', 'auth-int']; + +const kJwtAlgos = [ + 'HS256', + 'HS384', + 'HS512', + 'RS256', + 'RS384', + 'RS512', + 'PS256', + 'PS384', + 'PS512', + 'ES256', + 'ES256K', + 'ES384', + 'ES512', + 'EdDSA', +]; + enum HTTPVerb { get("GET"), head("HEAD"), @@ -96,4 +130,5 @@ const LineSplitter kSplitter = LineSplitter(); const kCodeCharsPerLineLimit = 200; const kHeaderContentType = "Content-Type"; +const kHeaderWwwAuthenticate = 'www-authenticate'; const kMsgRequestCancelled = 'Request Cancelled'; diff --git a/packages/better_networking/lib/models/auth/api_auth_model.dart b/packages/better_networking/lib/models/auth/api_auth_model.dart new file mode 100644 index 000000000..824ab543a --- /dev/null +++ b/packages/better_networking/lib/models/auth/api_auth_model.dart @@ -0,0 +1,26 @@ +import 'package:freezed_annotation/freezed_annotation.dart'; +import '../../consts.dart'; +import 'auth_api_key_model.dart'; +import 'auth_basic_model.dart'; +import 'auth_bearer_model.dart'; +import 'auth_jwt_model.dart'; +import 'auth_digest_model.dart'; + +part 'api_auth_model.g.dart'; +part 'api_auth_model.freezed.dart'; + +@freezed +class AuthModel with _$AuthModel { + @JsonSerializable(explicitToJson: true, anyMap: true) + const factory AuthModel({ + required APIAuthType type, + AuthApiKeyModel? apikey, + AuthBearerModel? bearer, + AuthBasicAuthModel? basic, + AuthJwtModel? jwt, + AuthDigestModel? digest, + }) = _AuthModel; + + factory AuthModel.fromJson(Map json) => + _$AuthModelFromJson(json); +} diff --git a/packages/better_networking/lib/models/auth/api_auth_model.freezed.dart b/packages/better_networking/lib/models/auth/api_auth_model.freezed.dart new file mode 100644 index 000000000..4e592fcc8 --- /dev/null +++ b/packages/better_networking/lib/models/auth/api_auth_model.freezed.dart @@ -0,0 +1,366 @@ +// coverage:ignore-file +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint +// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark + +part of 'api_auth_model.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +T _$identity(T value) => value; + +final _privateConstructorUsedError = UnsupportedError( + 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models', +); + +AuthModel _$AuthModelFromJson(Map json) { + return _AuthModel.fromJson(json); +} + +/// @nodoc +mixin _$AuthModel { + APIAuthType get type => throw _privateConstructorUsedError; + AuthApiKeyModel? get apikey => throw _privateConstructorUsedError; + AuthBearerModel? get bearer => throw _privateConstructorUsedError; + AuthBasicAuthModel? get basic => throw _privateConstructorUsedError; + AuthJwtModel? get jwt => throw _privateConstructorUsedError; + AuthDigestModel? get digest => throw _privateConstructorUsedError; + + /// Serializes this AuthModel to a JSON map. + Map toJson() => throw _privateConstructorUsedError; + + /// Create a copy of AuthModel + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + $AuthModelCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $AuthModelCopyWith<$Res> { + factory $AuthModelCopyWith(AuthModel value, $Res Function(AuthModel) then) = + _$AuthModelCopyWithImpl<$Res, AuthModel>; + @useResult + $Res call({ + APIAuthType type, + AuthApiKeyModel? apikey, + AuthBearerModel? bearer, + AuthBasicAuthModel? basic, + AuthJwtModel? jwt, + AuthDigestModel? digest, + }); + + $AuthApiKeyModelCopyWith<$Res>? get apikey; + $AuthBearerModelCopyWith<$Res>? get bearer; + $AuthBasicAuthModelCopyWith<$Res>? get basic; + $AuthJwtModelCopyWith<$Res>? get jwt; + $AuthDigestModelCopyWith<$Res>? get digest; +} + +/// @nodoc +class _$AuthModelCopyWithImpl<$Res, $Val extends AuthModel> + implements $AuthModelCopyWith<$Res> { + _$AuthModelCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + /// Create a copy of AuthModel + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? type = null, + Object? apikey = freezed, + Object? bearer = freezed, + Object? basic = freezed, + Object? jwt = freezed, + Object? digest = freezed, + }) { + return _then( + _value.copyWith( + type: null == type + ? _value.type + : type // ignore: cast_nullable_to_non_nullable + as APIAuthType, + apikey: freezed == apikey + ? _value.apikey + : apikey // ignore: cast_nullable_to_non_nullable + as AuthApiKeyModel?, + bearer: freezed == bearer + ? _value.bearer + : bearer // ignore: cast_nullable_to_non_nullable + as AuthBearerModel?, + basic: freezed == basic + ? _value.basic + : basic // ignore: cast_nullable_to_non_nullable + as AuthBasicAuthModel?, + jwt: freezed == jwt + ? _value.jwt + : jwt // ignore: cast_nullable_to_non_nullable + as AuthJwtModel?, + digest: freezed == digest + ? _value.digest + : digest // ignore: cast_nullable_to_non_nullable + as AuthDigestModel?, + ) + as $Val, + ); + } + + /// Create a copy of AuthModel + /// with the given fields replaced by the non-null parameter values. + @override + @pragma('vm:prefer-inline') + $AuthApiKeyModelCopyWith<$Res>? get apikey { + if (_value.apikey == null) { + return null; + } + + return $AuthApiKeyModelCopyWith<$Res>(_value.apikey!, (value) { + return _then(_value.copyWith(apikey: value) as $Val); + }); + } + + /// Create a copy of AuthModel + /// with the given fields replaced by the non-null parameter values. + @override + @pragma('vm:prefer-inline') + $AuthBearerModelCopyWith<$Res>? get bearer { + if (_value.bearer == null) { + return null; + } + + return $AuthBearerModelCopyWith<$Res>(_value.bearer!, (value) { + return _then(_value.copyWith(bearer: value) as $Val); + }); + } + + /// Create a copy of AuthModel + /// with the given fields replaced by the non-null parameter values. + @override + @pragma('vm:prefer-inline') + $AuthBasicAuthModelCopyWith<$Res>? get basic { + if (_value.basic == null) { + return null; + } + + return $AuthBasicAuthModelCopyWith<$Res>(_value.basic!, (value) { + return _then(_value.copyWith(basic: value) as $Val); + }); + } + + /// Create a copy of AuthModel + /// with the given fields replaced by the non-null parameter values. + @override + @pragma('vm:prefer-inline') + $AuthJwtModelCopyWith<$Res>? get jwt { + if (_value.jwt == null) { + return null; + } + + return $AuthJwtModelCopyWith<$Res>(_value.jwt!, (value) { + return _then(_value.copyWith(jwt: value) as $Val); + }); + } + + /// Create a copy of AuthModel + /// with the given fields replaced by the non-null parameter values. + @override + @pragma('vm:prefer-inline') + $AuthDigestModelCopyWith<$Res>? get digest { + if (_value.digest == null) { + return null; + } + + return $AuthDigestModelCopyWith<$Res>(_value.digest!, (value) { + return _then(_value.copyWith(digest: value) as $Val); + }); + } +} + +/// @nodoc +abstract class _$$AuthModelImplCopyWith<$Res> + implements $AuthModelCopyWith<$Res> { + factory _$$AuthModelImplCopyWith( + _$AuthModelImpl value, + $Res Function(_$AuthModelImpl) then, + ) = __$$AuthModelImplCopyWithImpl<$Res>; + @override + @useResult + $Res call({ + APIAuthType type, + AuthApiKeyModel? apikey, + AuthBearerModel? bearer, + AuthBasicAuthModel? basic, + AuthJwtModel? jwt, + AuthDigestModel? digest, + }); + + @override + $AuthApiKeyModelCopyWith<$Res>? get apikey; + @override + $AuthBearerModelCopyWith<$Res>? get bearer; + @override + $AuthBasicAuthModelCopyWith<$Res>? get basic; + @override + $AuthJwtModelCopyWith<$Res>? get jwt; + @override + $AuthDigestModelCopyWith<$Res>? get digest; +} + +/// @nodoc +class __$$AuthModelImplCopyWithImpl<$Res> + extends _$AuthModelCopyWithImpl<$Res, _$AuthModelImpl> + implements _$$AuthModelImplCopyWith<$Res> { + __$$AuthModelImplCopyWithImpl( + _$AuthModelImpl _value, + $Res Function(_$AuthModelImpl) _then, + ) : super(_value, _then); + + /// Create a copy of AuthModel + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? type = null, + Object? apikey = freezed, + Object? bearer = freezed, + Object? basic = freezed, + Object? jwt = freezed, + Object? digest = freezed, + }) { + return _then( + _$AuthModelImpl( + type: null == type + ? _value.type + : type // ignore: cast_nullable_to_non_nullable + as APIAuthType, + apikey: freezed == apikey + ? _value.apikey + : apikey // ignore: cast_nullable_to_non_nullable + as AuthApiKeyModel?, + bearer: freezed == bearer + ? _value.bearer + : bearer // ignore: cast_nullable_to_non_nullable + as AuthBearerModel?, + basic: freezed == basic + ? _value.basic + : basic // ignore: cast_nullable_to_non_nullable + as AuthBasicAuthModel?, + jwt: freezed == jwt + ? _value.jwt + : jwt // ignore: cast_nullable_to_non_nullable + as AuthJwtModel?, + digest: freezed == digest + ? _value.digest + : digest // ignore: cast_nullable_to_non_nullable + as AuthDigestModel?, + ), + ); + } +} + +/// @nodoc + +@JsonSerializable(explicitToJson: true, anyMap: true) +class _$AuthModelImpl implements _AuthModel { + const _$AuthModelImpl({ + required this.type, + this.apikey, + this.bearer, + this.basic, + this.jwt, + this.digest, + }); + + factory _$AuthModelImpl.fromJson(Map json) => + _$$AuthModelImplFromJson(json); + + @override + final APIAuthType type; + @override + final AuthApiKeyModel? apikey; + @override + final AuthBearerModel? bearer; + @override + final AuthBasicAuthModel? basic; + @override + final AuthJwtModel? jwt; + @override + final AuthDigestModel? digest; + + @override + String toString() { + return 'AuthModel(type: $type, apikey: $apikey, bearer: $bearer, basic: $basic, jwt: $jwt, digest: $digest)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$AuthModelImpl && + (identical(other.type, type) || other.type == type) && + (identical(other.apikey, apikey) || other.apikey == apikey) && + (identical(other.bearer, bearer) || other.bearer == bearer) && + (identical(other.basic, basic) || other.basic == basic) && + (identical(other.jwt, jwt) || other.jwt == jwt) && + (identical(other.digest, digest) || other.digest == digest)); + } + + @JsonKey(includeFromJson: false, includeToJson: false) + @override + int get hashCode => + Object.hash(runtimeType, type, apikey, bearer, basic, jwt, digest); + + /// Create a copy of AuthModel + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$AuthModelImplCopyWith<_$AuthModelImpl> get copyWith => + __$$AuthModelImplCopyWithImpl<_$AuthModelImpl>(this, _$identity); + + @override + Map toJson() { + return _$$AuthModelImplToJson(this); + } +} + +abstract class _AuthModel implements AuthModel { + const factory _AuthModel({ + required final APIAuthType type, + final AuthApiKeyModel? apikey, + final AuthBearerModel? bearer, + final AuthBasicAuthModel? basic, + final AuthJwtModel? jwt, + final AuthDigestModel? digest, + }) = _$AuthModelImpl; + + factory _AuthModel.fromJson(Map json) = + _$AuthModelImpl.fromJson; + + @override + APIAuthType get type; + @override + AuthApiKeyModel? get apikey; + @override + AuthBearerModel? get bearer; + @override + AuthBasicAuthModel? get basic; + @override + AuthJwtModel? get jwt; + @override + AuthDigestModel? get digest; + + /// Create a copy of AuthModel + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) + _$$AuthModelImplCopyWith<_$AuthModelImpl> get copyWith => + throw _privateConstructorUsedError; +} diff --git a/packages/better_networking/lib/models/auth/api_auth_model.g.dart b/packages/better_networking/lib/models/auth/api_auth_model.g.dart new file mode 100644 index 000000000..7b6ac4182 --- /dev/null +++ b/packages/better_networking/lib/models/auth/api_auth_model.g.dart @@ -0,0 +1,55 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'api_auth_model.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +_$AuthModelImpl _$$AuthModelImplFromJson(Map json) => _$AuthModelImpl( + type: $enumDecode(_$APIAuthTypeEnumMap, json['type']), + apikey: json['apikey'] == null + ? null + : AuthApiKeyModel.fromJson( + Map.from(json['apikey'] as Map), + ), + bearer: json['bearer'] == null + ? null + : AuthBearerModel.fromJson( + Map.from(json['bearer'] as Map), + ), + basic: json['basic'] == null + ? null + : AuthBasicAuthModel.fromJson( + Map.from(json['basic'] as Map), + ), + jwt: json['jwt'] == null + ? null + : AuthJwtModel.fromJson(Map.from(json['jwt'] as Map)), + digest: json['digest'] == null + ? null + : AuthDigestModel.fromJson( + Map.from(json['digest'] as Map), + ), +); + +Map _$$AuthModelImplToJson(_$AuthModelImpl instance) => + { + 'type': _$APIAuthTypeEnumMap[instance.type]!, + 'apikey': instance.apikey?.toJson(), + 'bearer': instance.bearer?.toJson(), + 'basic': instance.basic?.toJson(), + 'jwt': instance.jwt?.toJson(), + 'digest': instance.digest?.toJson(), + }; + +const _$APIAuthTypeEnumMap = { + APIAuthType.none: 'none', + APIAuthType.basic: 'basic', + APIAuthType.apiKey: 'apiKey', + APIAuthType.bearer: 'bearer', + APIAuthType.jwt: 'jwt', + APIAuthType.digest: 'digest', + APIAuthType.oauth1: 'oauth1', + APIAuthType.oauth2: 'oauth2', +}; diff --git a/packages/better_networking/lib/models/auth/auth_api_key_model.dart b/packages/better_networking/lib/models/auth/auth_api_key_model.dart new file mode 100644 index 000000000..c65837a8b --- /dev/null +++ b/packages/better_networking/lib/models/auth/auth_api_key_model.dart @@ -0,0 +1,16 @@ +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'auth_api_key_model.g.dart'; +part 'auth_api_key_model.freezed.dart'; + +@freezed +class AuthApiKeyModel with _$AuthApiKeyModel { + const factory AuthApiKeyModel({ + required String key, + @Default('header') String location, // 'header' or 'query' + @Default('x-api-key') String name, + }) = _AuthApiKeyModel; + + factory AuthApiKeyModel.fromJson(Map json) => + _$AuthApiKeyModelFromJson(json); +} diff --git a/packages/better_networking/lib/models/auth/auth_api_key_model.freezed.dart b/packages/better_networking/lib/models/auth/auth_api_key_model.freezed.dart new file mode 100644 index 000000000..525e00913 --- /dev/null +++ b/packages/better_networking/lib/models/auth/auth_api_key_model.freezed.dart @@ -0,0 +1,219 @@ +// coverage:ignore-file +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint +// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark + +part of 'auth_api_key_model.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +T _$identity(T value) => value; + +final _privateConstructorUsedError = UnsupportedError( + 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models', +); + +AuthApiKeyModel _$AuthApiKeyModelFromJson(Map json) { + return _AuthApiKeyModel.fromJson(json); +} + +/// @nodoc +mixin _$AuthApiKeyModel { + String get key => throw _privateConstructorUsedError; + String get location => + throw _privateConstructorUsedError; // 'header' or 'query' + String get name => throw _privateConstructorUsedError; + + /// Serializes this AuthApiKeyModel to a JSON map. + Map toJson() => throw _privateConstructorUsedError; + + /// Create a copy of AuthApiKeyModel + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + $AuthApiKeyModelCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $AuthApiKeyModelCopyWith<$Res> { + factory $AuthApiKeyModelCopyWith( + AuthApiKeyModel value, + $Res Function(AuthApiKeyModel) then, + ) = _$AuthApiKeyModelCopyWithImpl<$Res, AuthApiKeyModel>; + @useResult + $Res call({String key, String location, String name}); +} + +/// @nodoc +class _$AuthApiKeyModelCopyWithImpl<$Res, $Val extends AuthApiKeyModel> + implements $AuthApiKeyModelCopyWith<$Res> { + _$AuthApiKeyModelCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + /// Create a copy of AuthApiKeyModel + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? key = null, + Object? location = null, + Object? name = null, + }) { + return _then( + _value.copyWith( + key: null == key + ? _value.key + : key // ignore: cast_nullable_to_non_nullable + as String, + location: null == location + ? _value.location + : location // ignore: cast_nullable_to_non_nullable + as String, + name: null == name + ? _value.name + : name // ignore: cast_nullable_to_non_nullable + as String, + ) + as $Val, + ); + } +} + +/// @nodoc +abstract class _$$AuthApiKeyModelImplCopyWith<$Res> + implements $AuthApiKeyModelCopyWith<$Res> { + factory _$$AuthApiKeyModelImplCopyWith( + _$AuthApiKeyModelImpl value, + $Res Function(_$AuthApiKeyModelImpl) then, + ) = __$$AuthApiKeyModelImplCopyWithImpl<$Res>; + @override + @useResult + $Res call({String key, String location, String name}); +} + +/// @nodoc +class __$$AuthApiKeyModelImplCopyWithImpl<$Res> + extends _$AuthApiKeyModelCopyWithImpl<$Res, _$AuthApiKeyModelImpl> + implements _$$AuthApiKeyModelImplCopyWith<$Res> { + __$$AuthApiKeyModelImplCopyWithImpl( + _$AuthApiKeyModelImpl _value, + $Res Function(_$AuthApiKeyModelImpl) _then, + ) : super(_value, _then); + + /// Create a copy of AuthApiKeyModel + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? key = null, + Object? location = null, + Object? name = null, + }) { + return _then( + _$AuthApiKeyModelImpl( + key: null == key + ? _value.key + : key // ignore: cast_nullable_to_non_nullable + as String, + location: null == location + ? _value.location + : location // ignore: cast_nullable_to_non_nullable + as String, + name: null == name + ? _value.name + : name // ignore: cast_nullable_to_non_nullable + as String, + ), + ); + } +} + +/// @nodoc +@JsonSerializable() +class _$AuthApiKeyModelImpl implements _AuthApiKeyModel { + const _$AuthApiKeyModelImpl({ + required this.key, + this.location = 'header', + this.name = 'x-api-key', + }); + + factory _$AuthApiKeyModelImpl.fromJson(Map json) => + _$$AuthApiKeyModelImplFromJson(json); + + @override + final String key; + @override + @JsonKey() + final String location; + // 'header' or 'query' + @override + @JsonKey() + final String name; + + @override + String toString() { + return 'AuthApiKeyModel(key: $key, location: $location, name: $name)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$AuthApiKeyModelImpl && + (identical(other.key, key) || other.key == key) && + (identical(other.location, location) || + other.location == location) && + (identical(other.name, name) || other.name == name)); + } + + @JsonKey(includeFromJson: false, includeToJson: false) + @override + int get hashCode => Object.hash(runtimeType, key, location, name); + + /// Create a copy of AuthApiKeyModel + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$AuthApiKeyModelImplCopyWith<_$AuthApiKeyModelImpl> get copyWith => + __$$AuthApiKeyModelImplCopyWithImpl<_$AuthApiKeyModelImpl>( + this, + _$identity, + ); + + @override + Map toJson() { + return _$$AuthApiKeyModelImplToJson(this); + } +} + +abstract class _AuthApiKeyModel implements AuthApiKeyModel { + const factory _AuthApiKeyModel({ + required final String key, + final String location, + final String name, + }) = _$AuthApiKeyModelImpl; + + factory _AuthApiKeyModel.fromJson(Map json) = + _$AuthApiKeyModelImpl.fromJson; + + @override + String get key; + @override + String get location; // 'header' or 'query' + @override + String get name; + + /// Create a copy of AuthApiKeyModel + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) + _$$AuthApiKeyModelImplCopyWith<_$AuthApiKeyModelImpl> get copyWith => + throw _privateConstructorUsedError; +} diff --git a/packages/better_networking/lib/models/auth/auth_api_key_model.g.dart b/packages/better_networking/lib/models/auth/auth_api_key_model.g.dart new file mode 100644 index 000000000..a88f104cb --- /dev/null +++ b/packages/better_networking/lib/models/auth/auth_api_key_model.g.dart @@ -0,0 +1,23 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'auth_api_key_model.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +_$AuthApiKeyModelImpl _$$AuthApiKeyModelImplFromJson( + Map json, +) => _$AuthApiKeyModelImpl( + key: json['key'] as String, + location: json['location'] as String? ?? 'header', + name: json['name'] as String? ?? 'x-api-key', +); + +Map _$$AuthApiKeyModelImplToJson( + _$AuthApiKeyModelImpl instance, +) => { + 'key': instance.key, + 'location': instance.location, + 'name': instance.name, +}; diff --git a/packages/better_networking/lib/models/auth/auth_basic_model.dart b/packages/better_networking/lib/models/auth/auth_basic_model.dart new file mode 100644 index 000000000..5030652cc --- /dev/null +++ b/packages/better_networking/lib/models/auth/auth_basic_model.dart @@ -0,0 +1,15 @@ +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'auth_basic_model.g.dart'; +part 'auth_basic_model.freezed.dart'; + +@freezed +class AuthBasicAuthModel with _$AuthBasicAuthModel { + const factory AuthBasicAuthModel({ + required String username, + required String password, + }) = _AuthBasicAuthModel; + + factory AuthBasicAuthModel.fromJson(Map json) => + _$AuthBasicAuthModelFromJson(json); +} diff --git a/packages/better_networking/lib/models/auth/auth_basic_model.freezed.dart b/packages/better_networking/lib/models/auth/auth_basic_model.freezed.dart new file mode 100644 index 000000000..62e2399d6 --- /dev/null +++ b/packages/better_networking/lib/models/auth/auth_basic_model.freezed.dart @@ -0,0 +1,192 @@ +// coverage:ignore-file +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint +// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark + +part of 'auth_basic_model.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +T _$identity(T value) => value; + +final _privateConstructorUsedError = UnsupportedError( + 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models', +); + +AuthBasicAuthModel _$AuthBasicAuthModelFromJson(Map json) { + return _AuthBasicAuthModel.fromJson(json); +} + +/// @nodoc +mixin _$AuthBasicAuthModel { + String get username => throw _privateConstructorUsedError; + String get password => throw _privateConstructorUsedError; + + /// Serializes this AuthBasicAuthModel to a JSON map. + Map toJson() => throw _privateConstructorUsedError; + + /// Create a copy of AuthBasicAuthModel + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + $AuthBasicAuthModelCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $AuthBasicAuthModelCopyWith<$Res> { + factory $AuthBasicAuthModelCopyWith( + AuthBasicAuthModel value, + $Res Function(AuthBasicAuthModel) then, + ) = _$AuthBasicAuthModelCopyWithImpl<$Res, AuthBasicAuthModel>; + @useResult + $Res call({String username, String password}); +} + +/// @nodoc +class _$AuthBasicAuthModelCopyWithImpl<$Res, $Val extends AuthBasicAuthModel> + implements $AuthBasicAuthModelCopyWith<$Res> { + _$AuthBasicAuthModelCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + /// Create a copy of AuthBasicAuthModel + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({Object? username = null, Object? password = null}) { + return _then( + _value.copyWith( + username: null == username + ? _value.username + : username // ignore: cast_nullable_to_non_nullable + as String, + password: null == password + ? _value.password + : password // ignore: cast_nullable_to_non_nullable + as String, + ) + as $Val, + ); + } +} + +/// @nodoc +abstract class _$$AuthBasicAuthModelImplCopyWith<$Res> + implements $AuthBasicAuthModelCopyWith<$Res> { + factory _$$AuthBasicAuthModelImplCopyWith( + _$AuthBasicAuthModelImpl value, + $Res Function(_$AuthBasicAuthModelImpl) then, + ) = __$$AuthBasicAuthModelImplCopyWithImpl<$Res>; + @override + @useResult + $Res call({String username, String password}); +} + +/// @nodoc +class __$$AuthBasicAuthModelImplCopyWithImpl<$Res> + extends _$AuthBasicAuthModelCopyWithImpl<$Res, _$AuthBasicAuthModelImpl> + implements _$$AuthBasicAuthModelImplCopyWith<$Res> { + __$$AuthBasicAuthModelImplCopyWithImpl( + _$AuthBasicAuthModelImpl _value, + $Res Function(_$AuthBasicAuthModelImpl) _then, + ) : super(_value, _then); + + /// Create a copy of AuthBasicAuthModel + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({Object? username = null, Object? password = null}) { + return _then( + _$AuthBasicAuthModelImpl( + username: null == username + ? _value.username + : username // ignore: cast_nullable_to_non_nullable + as String, + password: null == password + ? _value.password + : password // ignore: cast_nullable_to_non_nullable + as String, + ), + ); + } +} + +/// @nodoc +@JsonSerializable() +class _$AuthBasicAuthModelImpl implements _AuthBasicAuthModel { + const _$AuthBasicAuthModelImpl({ + required this.username, + required this.password, + }); + + factory _$AuthBasicAuthModelImpl.fromJson(Map json) => + _$$AuthBasicAuthModelImplFromJson(json); + + @override + final String username; + @override + final String password; + + @override + String toString() { + return 'AuthBasicAuthModel(username: $username, password: $password)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$AuthBasicAuthModelImpl && + (identical(other.username, username) || + other.username == username) && + (identical(other.password, password) || + other.password == password)); + } + + @JsonKey(includeFromJson: false, includeToJson: false) + @override + int get hashCode => Object.hash(runtimeType, username, password); + + /// Create a copy of AuthBasicAuthModel + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$AuthBasicAuthModelImplCopyWith<_$AuthBasicAuthModelImpl> get copyWith => + __$$AuthBasicAuthModelImplCopyWithImpl<_$AuthBasicAuthModelImpl>( + this, + _$identity, + ); + + @override + Map toJson() { + return _$$AuthBasicAuthModelImplToJson(this); + } +} + +abstract class _AuthBasicAuthModel implements AuthBasicAuthModel { + const factory _AuthBasicAuthModel({ + required final String username, + required final String password, + }) = _$AuthBasicAuthModelImpl; + + factory _AuthBasicAuthModel.fromJson(Map json) = + _$AuthBasicAuthModelImpl.fromJson; + + @override + String get username; + @override + String get password; + + /// Create a copy of AuthBasicAuthModel + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) + _$$AuthBasicAuthModelImplCopyWith<_$AuthBasicAuthModelImpl> get copyWith => + throw _privateConstructorUsedError; +} diff --git a/packages/better_networking/lib/models/auth/auth_basic_model.g.dart b/packages/better_networking/lib/models/auth/auth_basic_model.g.dart new file mode 100644 index 000000000..6291328b4 --- /dev/null +++ b/packages/better_networking/lib/models/auth/auth_basic_model.g.dart @@ -0,0 +1,21 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'auth_basic_model.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +_$AuthBasicAuthModelImpl _$$AuthBasicAuthModelImplFromJson( + Map json, +) => _$AuthBasicAuthModelImpl( + username: json['username'] as String, + password: json['password'] as String, +); + +Map _$$AuthBasicAuthModelImplToJson( + _$AuthBasicAuthModelImpl instance, +) => { + 'username': instance.username, + 'password': instance.password, +}; diff --git a/packages/better_networking/lib/models/auth/auth_bearer_model.dart b/packages/better_networking/lib/models/auth/auth_bearer_model.dart new file mode 100644 index 000000000..ab4002acb --- /dev/null +++ b/packages/better_networking/lib/models/auth/auth_bearer_model.dart @@ -0,0 +1,14 @@ +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'auth_bearer_model.g.dart'; +part 'auth_bearer_model.freezed.dart'; + +@freezed +class AuthBearerModel with _$AuthBearerModel { + const factory AuthBearerModel({ + required String token, + }) = _AuthBearerModel; + + factory AuthBearerModel.fromJson(Map json) => + _$AuthBearerModelFromJson(json); +} diff --git a/packages/better_networking/lib/models/auth/auth_bearer_model.freezed.dart b/packages/better_networking/lib/models/auth/auth_bearer_model.freezed.dart new file mode 100644 index 000000000..a590fcce5 --- /dev/null +++ b/packages/better_networking/lib/models/auth/auth_bearer_model.freezed.dart @@ -0,0 +1,171 @@ +// coverage:ignore-file +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint +// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark + +part of 'auth_bearer_model.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +T _$identity(T value) => value; + +final _privateConstructorUsedError = UnsupportedError( + 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models', +); + +AuthBearerModel _$AuthBearerModelFromJson(Map json) { + return _AuthBearerModel.fromJson(json); +} + +/// @nodoc +mixin _$AuthBearerModel { + String get token => throw _privateConstructorUsedError; + + /// Serializes this AuthBearerModel to a JSON map. + Map toJson() => throw _privateConstructorUsedError; + + /// Create a copy of AuthBearerModel + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + $AuthBearerModelCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $AuthBearerModelCopyWith<$Res> { + factory $AuthBearerModelCopyWith( + AuthBearerModel value, + $Res Function(AuthBearerModel) then, + ) = _$AuthBearerModelCopyWithImpl<$Res, AuthBearerModel>; + @useResult + $Res call({String token}); +} + +/// @nodoc +class _$AuthBearerModelCopyWithImpl<$Res, $Val extends AuthBearerModel> + implements $AuthBearerModelCopyWith<$Res> { + _$AuthBearerModelCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + /// Create a copy of AuthBearerModel + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({Object? token = null}) { + return _then( + _value.copyWith( + token: null == token + ? _value.token + : token // ignore: cast_nullable_to_non_nullable + as String, + ) + as $Val, + ); + } +} + +/// @nodoc +abstract class _$$AuthBearerModelImplCopyWith<$Res> + implements $AuthBearerModelCopyWith<$Res> { + factory _$$AuthBearerModelImplCopyWith( + _$AuthBearerModelImpl value, + $Res Function(_$AuthBearerModelImpl) then, + ) = __$$AuthBearerModelImplCopyWithImpl<$Res>; + @override + @useResult + $Res call({String token}); +} + +/// @nodoc +class __$$AuthBearerModelImplCopyWithImpl<$Res> + extends _$AuthBearerModelCopyWithImpl<$Res, _$AuthBearerModelImpl> + implements _$$AuthBearerModelImplCopyWith<$Res> { + __$$AuthBearerModelImplCopyWithImpl( + _$AuthBearerModelImpl _value, + $Res Function(_$AuthBearerModelImpl) _then, + ) : super(_value, _then); + + /// Create a copy of AuthBearerModel + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({Object? token = null}) { + return _then( + _$AuthBearerModelImpl( + token: null == token + ? _value.token + : token // ignore: cast_nullable_to_non_nullable + as String, + ), + ); + } +} + +/// @nodoc +@JsonSerializable() +class _$AuthBearerModelImpl implements _AuthBearerModel { + const _$AuthBearerModelImpl({required this.token}); + + factory _$AuthBearerModelImpl.fromJson(Map json) => + _$$AuthBearerModelImplFromJson(json); + + @override + final String token; + + @override + String toString() { + return 'AuthBearerModel(token: $token)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$AuthBearerModelImpl && + (identical(other.token, token) || other.token == token)); + } + + @JsonKey(includeFromJson: false, includeToJson: false) + @override + int get hashCode => Object.hash(runtimeType, token); + + /// Create a copy of AuthBearerModel + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$AuthBearerModelImplCopyWith<_$AuthBearerModelImpl> get copyWith => + __$$AuthBearerModelImplCopyWithImpl<_$AuthBearerModelImpl>( + this, + _$identity, + ); + + @override + Map toJson() { + return _$$AuthBearerModelImplToJson(this); + } +} + +abstract class _AuthBearerModel implements AuthBearerModel { + const factory _AuthBearerModel({required final String token}) = + _$AuthBearerModelImpl; + + factory _AuthBearerModel.fromJson(Map json) = + _$AuthBearerModelImpl.fromJson; + + @override + String get token; + + /// Create a copy of AuthBearerModel + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) + _$$AuthBearerModelImplCopyWith<_$AuthBearerModelImpl> get copyWith => + throw _privateConstructorUsedError; +} diff --git a/packages/better_networking/lib/models/auth/auth_bearer_model.g.dart b/packages/better_networking/lib/models/auth/auth_bearer_model.g.dart new file mode 100644 index 000000000..34c150d85 --- /dev/null +++ b/packages/better_networking/lib/models/auth/auth_bearer_model.g.dart @@ -0,0 +1,15 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'auth_bearer_model.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +_$AuthBearerModelImpl _$$AuthBearerModelImplFromJson( + Map json, +) => _$AuthBearerModelImpl(token: json['token'] as String); + +Map _$$AuthBearerModelImplToJson( + _$AuthBearerModelImpl instance, +) => {'token': instance.token}; diff --git a/packages/better_networking/lib/models/auth/auth_digest_model.dart b/packages/better_networking/lib/models/auth/auth_digest_model.dart new file mode 100644 index 000000000..2c2bc3ac6 --- /dev/null +++ b/packages/better_networking/lib/models/auth/auth_digest_model.dart @@ -0,0 +1,20 @@ +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'auth_digest_model.g.dart'; +part 'auth_digest_model.freezed.dart'; + +@freezed +class AuthDigestModel with _$AuthDigestModel { + const factory AuthDigestModel({ + required String username, + required String password, + required String realm, + required String nonce, + required String algorithm, + required String qop, + required String opaque, + }) = _AuthDigestModel; + + factory AuthDigestModel.fromJson(Map json) => + _$AuthDigestModelFromJson(json); +} diff --git a/packages/better_networking/lib/models/auth/auth_digest_model.freezed.dart b/packages/better_networking/lib/models/auth/auth_digest_model.freezed.dart new file mode 100644 index 000000000..2dd2f05ff --- /dev/null +++ b/packages/better_networking/lib/models/auth/auth_digest_model.freezed.dart @@ -0,0 +1,314 @@ +// coverage:ignore-file +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint +// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark + +part of 'auth_digest_model.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +T _$identity(T value) => value; + +final _privateConstructorUsedError = UnsupportedError( + 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models', +); + +AuthDigestModel _$AuthDigestModelFromJson(Map json) { + return _AuthDigestModel.fromJson(json); +} + +/// @nodoc +mixin _$AuthDigestModel { + String get username => throw _privateConstructorUsedError; + String get password => throw _privateConstructorUsedError; + String get realm => throw _privateConstructorUsedError; + String get nonce => throw _privateConstructorUsedError; + String get algorithm => throw _privateConstructorUsedError; + String get qop => throw _privateConstructorUsedError; + String get opaque => throw _privateConstructorUsedError; + + /// Serializes this AuthDigestModel to a JSON map. + Map toJson() => throw _privateConstructorUsedError; + + /// Create a copy of AuthDigestModel + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + $AuthDigestModelCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $AuthDigestModelCopyWith<$Res> { + factory $AuthDigestModelCopyWith( + AuthDigestModel value, + $Res Function(AuthDigestModel) then, + ) = _$AuthDigestModelCopyWithImpl<$Res, AuthDigestModel>; + @useResult + $Res call({ + String username, + String password, + String realm, + String nonce, + String algorithm, + String qop, + String opaque, + }); +} + +/// @nodoc +class _$AuthDigestModelCopyWithImpl<$Res, $Val extends AuthDigestModel> + implements $AuthDigestModelCopyWith<$Res> { + _$AuthDigestModelCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + /// Create a copy of AuthDigestModel + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? username = null, + Object? password = null, + Object? realm = null, + Object? nonce = null, + Object? algorithm = null, + Object? qop = null, + Object? opaque = null, + }) { + return _then( + _value.copyWith( + username: null == username + ? _value.username + : username // ignore: cast_nullable_to_non_nullable + as String, + password: null == password + ? _value.password + : password // ignore: cast_nullable_to_non_nullable + as String, + realm: null == realm + ? _value.realm + : realm // ignore: cast_nullable_to_non_nullable + as String, + nonce: null == nonce + ? _value.nonce + : nonce // ignore: cast_nullable_to_non_nullable + as String, + algorithm: null == algorithm + ? _value.algorithm + : algorithm // ignore: cast_nullable_to_non_nullable + as String, + qop: null == qop + ? _value.qop + : qop // ignore: cast_nullable_to_non_nullable + as String, + opaque: null == opaque + ? _value.opaque + : opaque // ignore: cast_nullable_to_non_nullable + as String, + ) + as $Val, + ); + } +} + +/// @nodoc +abstract class _$$AuthDigestModelImplCopyWith<$Res> + implements $AuthDigestModelCopyWith<$Res> { + factory _$$AuthDigestModelImplCopyWith( + _$AuthDigestModelImpl value, + $Res Function(_$AuthDigestModelImpl) then, + ) = __$$AuthDigestModelImplCopyWithImpl<$Res>; + @override + @useResult + $Res call({ + String username, + String password, + String realm, + String nonce, + String algorithm, + String qop, + String opaque, + }); +} + +/// @nodoc +class __$$AuthDigestModelImplCopyWithImpl<$Res> + extends _$AuthDigestModelCopyWithImpl<$Res, _$AuthDigestModelImpl> + implements _$$AuthDigestModelImplCopyWith<$Res> { + __$$AuthDigestModelImplCopyWithImpl( + _$AuthDigestModelImpl _value, + $Res Function(_$AuthDigestModelImpl) _then, + ) : super(_value, _then); + + /// Create a copy of AuthDigestModel + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? username = null, + Object? password = null, + Object? realm = null, + Object? nonce = null, + Object? algorithm = null, + Object? qop = null, + Object? opaque = null, + }) { + return _then( + _$AuthDigestModelImpl( + username: null == username + ? _value.username + : username // ignore: cast_nullable_to_non_nullable + as String, + password: null == password + ? _value.password + : password // ignore: cast_nullable_to_non_nullable + as String, + realm: null == realm + ? _value.realm + : realm // ignore: cast_nullable_to_non_nullable + as String, + nonce: null == nonce + ? _value.nonce + : nonce // ignore: cast_nullable_to_non_nullable + as String, + algorithm: null == algorithm + ? _value.algorithm + : algorithm // ignore: cast_nullable_to_non_nullable + as String, + qop: null == qop + ? _value.qop + : qop // ignore: cast_nullable_to_non_nullable + as String, + opaque: null == opaque + ? _value.opaque + : opaque // ignore: cast_nullable_to_non_nullable + as String, + ), + ); + } +} + +/// @nodoc +@JsonSerializable() +class _$AuthDigestModelImpl implements _AuthDigestModel { + const _$AuthDigestModelImpl({ + required this.username, + required this.password, + required this.realm, + required this.nonce, + required this.algorithm, + required this.qop, + required this.opaque, + }); + + factory _$AuthDigestModelImpl.fromJson(Map json) => + _$$AuthDigestModelImplFromJson(json); + + @override + final String username; + @override + final String password; + @override + final String realm; + @override + final String nonce; + @override + final String algorithm; + @override + final String qop; + @override + final String opaque; + + @override + String toString() { + return 'AuthDigestModel(username: $username, password: $password, realm: $realm, nonce: $nonce, algorithm: $algorithm, qop: $qop, opaque: $opaque)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$AuthDigestModelImpl && + (identical(other.username, username) || + other.username == username) && + (identical(other.password, password) || + other.password == password) && + (identical(other.realm, realm) || other.realm == realm) && + (identical(other.nonce, nonce) || other.nonce == nonce) && + (identical(other.algorithm, algorithm) || + other.algorithm == algorithm) && + (identical(other.qop, qop) || other.qop == qop) && + (identical(other.opaque, opaque) || other.opaque == opaque)); + } + + @JsonKey(includeFromJson: false, includeToJson: false) + @override + int get hashCode => Object.hash( + runtimeType, + username, + password, + realm, + nonce, + algorithm, + qop, + opaque, + ); + + /// Create a copy of AuthDigestModel + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$AuthDigestModelImplCopyWith<_$AuthDigestModelImpl> get copyWith => + __$$AuthDigestModelImplCopyWithImpl<_$AuthDigestModelImpl>( + this, + _$identity, + ); + + @override + Map toJson() { + return _$$AuthDigestModelImplToJson(this); + } +} + +abstract class _AuthDigestModel implements AuthDigestModel { + const factory _AuthDigestModel({ + required final String username, + required final String password, + required final String realm, + required final String nonce, + required final String algorithm, + required final String qop, + required final String opaque, + }) = _$AuthDigestModelImpl; + + factory _AuthDigestModel.fromJson(Map json) = + _$AuthDigestModelImpl.fromJson; + + @override + String get username; + @override + String get password; + @override + String get realm; + @override + String get nonce; + @override + String get algorithm; + @override + String get qop; + @override + String get opaque; + + /// Create a copy of AuthDigestModel + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) + _$$AuthDigestModelImplCopyWith<_$AuthDigestModelImpl> get copyWith => + throw _privateConstructorUsedError; +} diff --git a/packages/better_networking/lib/models/auth/auth_digest_model.g.dart b/packages/better_networking/lib/models/auth/auth_digest_model.g.dart new file mode 100644 index 000000000..ebbf878b1 --- /dev/null +++ b/packages/better_networking/lib/models/auth/auth_digest_model.g.dart @@ -0,0 +1,31 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'auth_digest_model.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +_$AuthDigestModelImpl _$$AuthDigestModelImplFromJson( + Map json, +) => _$AuthDigestModelImpl( + username: json['username'] as String, + password: json['password'] as String, + realm: json['realm'] as String, + nonce: json['nonce'] as String, + algorithm: json['algorithm'] as String, + qop: json['qop'] as String, + opaque: json['opaque'] as String, +); + +Map _$$AuthDigestModelImplToJson( + _$AuthDigestModelImpl instance, +) => { + 'username': instance.username, + 'password': instance.password, + 'realm': instance.realm, + 'nonce': instance.nonce, + 'algorithm': instance.algorithm, + 'qop': instance.qop, + 'opaque': instance.opaque, +}; diff --git a/packages/better_networking/lib/models/auth/auth_jwt_model.dart b/packages/better_networking/lib/models/auth/auth_jwt_model.dart new file mode 100644 index 000000000..a2e82e053 --- /dev/null +++ b/packages/better_networking/lib/models/auth/auth_jwt_model.dart @@ -0,0 +1,22 @@ +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'auth_jwt_model.freezed.dart'; +part 'auth_jwt_model.g.dart'; + +@freezed +class AuthJwtModel with _$AuthJwtModel { + const factory AuthJwtModel({ + required String secret, + String? privateKey, + required String payload, + required String addTokenTo, + required String algorithm, + required bool isSecretBase64Encoded, + required String headerPrefix, + required String queryParamKey, + required String header, + }) = _AuthJwtModel; + + factory AuthJwtModel.fromJson(Map json) => + _$AuthJwtModelFromJson(json); +} diff --git a/packages/better_networking/lib/models/auth/auth_jwt_model.freezed.dart b/packages/better_networking/lib/models/auth/auth_jwt_model.freezed.dart new file mode 100644 index 000000000..151e18f53 --- /dev/null +++ b/packages/better_networking/lib/models/auth/auth_jwt_model.freezed.dart @@ -0,0 +1,356 @@ +// coverage:ignore-file +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint +// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark + +part of 'auth_jwt_model.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +T _$identity(T value) => value; + +final _privateConstructorUsedError = UnsupportedError( + 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models', +); + +AuthJwtModel _$AuthJwtModelFromJson(Map json) { + return _AuthJwtModel.fromJson(json); +} + +/// @nodoc +mixin _$AuthJwtModel { + String get secret => throw _privateConstructorUsedError; + String? get privateKey => throw _privateConstructorUsedError; + String get payload => throw _privateConstructorUsedError; + String get addTokenTo => throw _privateConstructorUsedError; + String get algorithm => throw _privateConstructorUsedError; + bool get isSecretBase64Encoded => throw _privateConstructorUsedError; + String get headerPrefix => throw _privateConstructorUsedError; + String get queryParamKey => throw _privateConstructorUsedError; + String get header => throw _privateConstructorUsedError; + + /// Serializes this AuthJwtModel to a JSON map. + Map toJson() => throw _privateConstructorUsedError; + + /// Create a copy of AuthJwtModel + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + $AuthJwtModelCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $AuthJwtModelCopyWith<$Res> { + factory $AuthJwtModelCopyWith( + AuthJwtModel value, + $Res Function(AuthJwtModel) then, + ) = _$AuthJwtModelCopyWithImpl<$Res, AuthJwtModel>; + @useResult + $Res call({ + String secret, + String? privateKey, + String payload, + String addTokenTo, + String algorithm, + bool isSecretBase64Encoded, + String headerPrefix, + String queryParamKey, + String header, + }); +} + +/// @nodoc +class _$AuthJwtModelCopyWithImpl<$Res, $Val extends AuthJwtModel> + implements $AuthJwtModelCopyWith<$Res> { + _$AuthJwtModelCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + /// Create a copy of AuthJwtModel + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? secret = null, + Object? privateKey = freezed, + Object? payload = null, + Object? addTokenTo = null, + Object? algorithm = null, + Object? isSecretBase64Encoded = null, + Object? headerPrefix = null, + Object? queryParamKey = null, + Object? header = null, + }) { + return _then( + _value.copyWith( + secret: null == secret + ? _value.secret + : secret // ignore: cast_nullable_to_non_nullable + as String, + privateKey: freezed == privateKey + ? _value.privateKey + : privateKey // ignore: cast_nullable_to_non_nullable + as String?, + payload: null == payload + ? _value.payload + : payload // ignore: cast_nullable_to_non_nullable + as String, + addTokenTo: null == addTokenTo + ? _value.addTokenTo + : addTokenTo // ignore: cast_nullable_to_non_nullable + as String, + algorithm: null == algorithm + ? _value.algorithm + : algorithm // ignore: cast_nullable_to_non_nullable + as String, + isSecretBase64Encoded: null == isSecretBase64Encoded + ? _value.isSecretBase64Encoded + : isSecretBase64Encoded // ignore: cast_nullable_to_non_nullable + as bool, + headerPrefix: null == headerPrefix + ? _value.headerPrefix + : headerPrefix // ignore: cast_nullable_to_non_nullable + as String, + queryParamKey: null == queryParamKey + ? _value.queryParamKey + : queryParamKey // ignore: cast_nullable_to_non_nullable + as String, + header: null == header + ? _value.header + : header // ignore: cast_nullable_to_non_nullable + as String, + ) + as $Val, + ); + } +} + +/// @nodoc +abstract class _$$AuthJwtModelImplCopyWith<$Res> + implements $AuthJwtModelCopyWith<$Res> { + factory _$$AuthJwtModelImplCopyWith( + _$AuthJwtModelImpl value, + $Res Function(_$AuthJwtModelImpl) then, + ) = __$$AuthJwtModelImplCopyWithImpl<$Res>; + @override + @useResult + $Res call({ + String secret, + String? privateKey, + String payload, + String addTokenTo, + String algorithm, + bool isSecretBase64Encoded, + String headerPrefix, + String queryParamKey, + String header, + }); +} + +/// @nodoc +class __$$AuthJwtModelImplCopyWithImpl<$Res> + extends _$AuthJwtModelCopyWithImpl<$Res, _$AuthJwtModelImpl> + implements _$$AuthJwtModelImplCopyWith<$Res> { + __$$AuthJwtModelImplCopyWithImpl( + _$AuthJwtModelImpl _value, + $Res Function(_$AuthJwtModelImpl) _then, + ) : super(_value, _then); + + /// Create a copy of AuthJwtModel + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? secret = null, + Object? privateKey = freezed, + Object? payload = null, + Object? addTokenTo = null, + Object? algorithm = null, + Object? isSecretBase64Encoded = null, + Object? headerPrefix = null, + Object? queryParamKey = null, + Object? header = null, + }) { + return _then( + _$AuthJwtModelImpl( + secret: null == secret + ? _value.secret + : secret // ignore: cast_nullable_to_non_nullable + as String, + privateKey: freezed == privateKey + ? _value.privateKey + : privateKey // ignore: cast_nullable_to_non_nullable + as String?, + payload: null == payload + ? _value.payload + : payload // ignore: cast_nullable_to_non_nullable + as String, + addTokenTo: null == addTokenTo + ? _value.addTokenTo + : addTokenTo // ignore: cast_nullable_to_non_nullable + as String, + algorithm: null == algorithm + ? _value.algorithm + : algorithm // ignore: cast_nullable_to_non_nullable + as String, + isSecretBase64Encoded: null == isSecretBase64Encoded + ? _value.isSecretBase64Encoded + : isSecretBase64Encoded // ignore: cast_nullable_to_non_nullable + as bool, + headerPrefix: null == headerPrefix + ? _value.headerPrefix + : headerPrefix // ignore: cast_nullable_to_non_nullable + as String, + queryParamKey: null == queryParamKey + ? _value.queryParamKey + : queryParamKey // ignore: cast_nullable_to_non_nullable + as String, + header: null == header + ? _value.header + : header // ignore: cast_nullable_to_non_nullable + as String, + ), + ); + } +} + +/// @nodoc +@JsonSerializable() +class _$AuthJwtModelImpl implements _AuthJwtModel { + const _$AuthJwtModelImpl({ + required this.secret, + this.privateKey, + required this.payload, + required this.addTokenTo, + required this.algorithm, + required this.isSecretBase64Encoded, + required this.headerPrefix, + required this.queryParamKey, + required this.header, + }); + + factory _$AuthJwtModelImpl.fromJson(Map json) => + _$$AuthJwtModelImplFromJson(json); + + @override + final String secret; + @override + final String? privateKey; + @override + final String payload; + @override + final String addTokenTo; + @override + final String algorithm; + @override + final bool isSecretBase64Encoded; + @override + final String headerPrefix; + @override + final String queryParamKey; + @override + final String header; + + @override + String toString() { + return 'AuthJwtModel(secret: $secret, privateKey: $privateKey, payload: $payload, addTokenTo: $addTokenTo, algorithm: $algorithm, isSecretBase64Encoded: $isSecretBase64Encoded, headerPrefix: $headerPrefix, queryParamKey: $queryParamKey, header: $header)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$AuthJwtModelImpl && + (identical(other.secret, secret) || other.secret == secret) && + (identical(other.privateKey, privateKey) || + other.privateKey == privateKey) && + (identical(other.payload, payload) || other.payload == payload) && + (identical(other.addTokenTo, addTokenTo) || + other.addTokenTo == addTokenTo) && + (identical(other.algorithm, algorithm) || + other.algorithm == algorithm) && + (identical(other.isSecretBase64Encoded, isSecretBase64Encoded) || + other.isSecretBase64Encoded == isSecretBase64Encoded) && + (identical(other.headerPrefix, headerPrefix) || + other.headerPrefix == headerPrefix) && + (identical(other.queryParamKey, queryParamKey) || + other.queryParamKey == queryParamKey) && + (identical(other.header, header) || other.header == header)); + } + + @JsonKey(includeFromJson: false, includeToJson: false) + @override + int get hashCode => Object.hash( + runtimeType, + secret, + privateKey, + payload, + addTokenTo, + algorithm, + isSecretBase64Encoded, + headerPrefix, + queryParamKey, + header, + ); + + /// Create a copy of AuthJwtModel + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$AuthJwtModelImplCopyWith<_$AuthJwtModelImpl> get copyWith => + __$$AuthJwtModelImplCopyWithImpl<_$AuthJwtModelImpl>(this, _$identity); + + @override + Map toJson() { + return _$$AuthJwtModelImplToJson(this); + } +} + +abstract class _AuthJwtModel implements AuthJwtModel { + const factory _AuthJwtModel({ + required final String secret, + final String? privateKey, + required final String payload, + required final String addTokenTo, + required final String algorithm, + required final bool isSecretBase64Encoded, + required final String headerPrefix, + required final String queryParamKey, + required final String header, + }) = _$AuthJwtModelImpl; + + factory _AuthJwtModel.fromJson(Map json) = + _$AuthJwtModelImpl.fromJson; + + @override + String get secret; + @override + String? get privateKey; + @override + String get payload; + @override + String get addTokenTo; + @override + String get algorithm; + @override + bool get isSecretBase64Encoded; + @override + String get headerPrefix; + @override + String get queryParamKey; + @override + String get header; + + /// Create a copy of AuthJwtModel + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) + _$$AuthJwtModelImplCopyWith<_$AuthJwtModelImpl> get copyWith => + throw _privateConstructorUsedError; +} diff --git a/packages/better_networking/lib/models/auth/auth_jwt_model.g.dart b/packages/better_networking/lib/models/auth/auth_jwt_model.g.dart new file mode 100644 index 000000000..cdd58a283 --- /dev/null +++ b/packages/better_networking/lib/models/auth/auth_jwt_model.g.dart @@ -0,0 +1,33 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'auth_jwt_model.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +_$AuthJwtModelImpl _$$AuthJwtModelImplFromJson(Map json) => + _$AuthJwtModelImpl( + secret: json['secret'] as String, + privateKey: json['privateKey'] as String?, + payload: json['payload'] as String, + addTokenTo: json['addTokenTo'] as String, + algorithm: json['algorithm'] as String, + isSecretBase64Encoded: json['isSecretBase64Encoded'] as bool, + headerPrefix: json['headerPrefix'] as String, + queryParamKey: json['queryParamKey'] as String, + header: json['header'] as String, + ); + +Map _$$AuthJwtModelImplToJson(_$AuthJwtModelImpl instance) => + { + 'secret': instance.secret, + 'privateKey': instance.privateKey, + 'payload': instance.payload, + 'addTokenTo': instance.addTokenTo, + 'algorithm': instance.algorithm, + 'isSecretBase64Encoded': instance.isSecretBase64Encoded, + 'headerPrefix': instance.headerPrefix, + 'queryParamKey': instance.queryParamKey, + 'header': instance.header, + }; diff --git a/packages/better_networking/lib/models/http_request_model.dart b/packages/better_networking/lib/models/http_request_model.dart index 2d915cfa6..7fbaa6481 100644 --- a/packages/better_networking/lib/models/http_request_model.dart +++ b/packages/better_networking/lib/models/http_request_model.dart @@ -5,6 +5,7 @@ import '../extensions/extensions.dart'; import '../utils/utils.dart' show rowsToFormDataMapList, rowsToMap, getEnabledRows; import '../consts.dart'; +import 'auth/api_auth_model.dart'; part 'http_request_model.freezed.dart'; part 'http_request_model.g.dart'; @@ -19,6 +20,7 @@ class HttpRequestModel with _$HttpRequestModel { @Default("") String url, List? headers, List? params, + @Default(AuthModel(type: APIAuthType.none)) AuthModel? authModel, List? isHeaderEnabledList, List? isParamEnabledList, @Default(ContentType.json) ContentType bodyContentType, diff --git a/packages/better_networking/lib/models/http_request_model.freezed.dart b/packages/better_networking/lib/models/http_request_model.freezed.dart index 864ae7e0b..871d6012c 100644 --- a/packages/better_networking/lib/models/http_request_model.freezed.dart +++ b/packages/better_networking/lib/models/http_request_model.freezed.dart @@ -25,6 +25,7 @@ mixin _$HttpRequestModel { String get url => throw _privateConstructorUsedError; List? get headers => throw _privateConstructorUsedError; List? get params => throw _privateConstructorUsedError; + AuthModel? get authModel => throw _privateConstructorUsedError; List? get isHeaderEnabledList => throw _privateConstructorUsedError; List? get isParamEnabledList => throw _privateConstructorUsedError; ContentType get bodyContentType => throw _privateConstructorUsedError; @@ -54,6 +55,7 @@ abstract class $HttpRequestModelCopyWith<$Res> { String url, List? headers, List? params, + AuthModel? authModel, List? isHeaderEnabledList, List? isParamEnabledList, ContentType bodyContentType, @@ -61,6 +63,8 @@ abstract class $HttpRequestModelCopyWith<$Res> { String? query, List? formData, }); + + $AuthModelCopyWith<$Res>? get authModel; } /// @nodoc @@ -82,6 +86,7 @@ class _$HttpRequestModelCopyWithImpl<$Res, $Val extends HttpRequestModel> Object? url = null, Object? headers = freezed, Object? params = freezed, + Object? authModel = freezed, Object? isHeaderEnabledList = freezed, Object? isParamEnabledList = freezed, Object? bodyContentType = null, @@ -107,6 +112,10 @@ class _$HttpRequestModelCopyWithImpl<$Res, $Val extends HttpRequestModel> ? _value.params : params // ignore: cast_nullable_to_non_nullable as List?, + authModel: freezed == authModel + ? _value.authModel + : authModel // ignore: cast_nullable_to_non_nullable + as AuthModel?, isHeaderEnabledList: freezed == isHeaderEnabledList ? _value.isHeaderEnabledList : isHeaderEnabledList // ignore: cast_nullable_to_non_nullable @@ -135,6 +144,20 @@ class _$HttpRequestModelCopyWithImpl<$Res, $Val extends HttpRequestModel> as $Val, ); } + + /// Create a copy of HttpRequestModel + /// with the given fields replaced by the non-null parameter values. + @override + @pragma('vm:prefer-inline') + $AuthModelCopyWith<$Res>? get authModel { + if (_value.authModel == null) { + return null; + } + + return $AuthModelCopyWith<$Res>(_value.authModel!, (value) { + return _then(_value.copyWith(authModel: value) as $Val); + }); + } } /// @nodoc @@ -151,6 +174,7 @@ abstract class _$$HttpRequestModelImplCopyWith<$Res> String url, List? headers, List? params, + AuthModel? authModel, List? isHeaderEnabledList, List? isParamEnabledList, ContentType bodyContentType, @@ -158,6 +182,9 @@ abstract class _$$HttpRequestModelImplCopyWith<$Res> String? query, List? formData, }); + + @override + $AuthModelCopyWith<$Res>? get authModel; } /// @nodoc @@ -178,6 +205,7 @@ class __$$HttpRequestModelImplCopyWithImpl<$Res> Object? url = null, Object? headers = freezed, Object? params = freezed, + Object? authModel = freezed, Object? isHeaderEnabledList = freezed, Object? isParamEnabledList = freezed, Object? bodyContentType = null, @@ -203,6 +231,10 @@ class __$$HttpRequestModelImplCopyWithImpl<$Res> ? _value._params : params // ignore: cast_nullable_to_non_nullable as List?, + authModel: freezed == authModel + ? _value.authModel + : authModel // ignore: cast_nullable_to_non_nullable + as AuthModel?, isHeaderEnabledList: freezed == isHeaderEnabledList ? _value._isHeaderEnabledList : isHeaderEnabledList // ignore: cast_nullable_to_non_nullable @@ -241,6 +273,7 @@ class _$HttpRequestModelImpl extends _HttpRequestModel { this.url = "", final List? headers, final List? params, + this.authModel = const AuthModel(type: APIAuthType.none), final List? isHeaderEnabledList, final List? isParamEnabledList, this.bodyContentType = ContentType.json, @@ -283,6 +316,9 @@ class _$HttpRequestModelImpl extends _HttpRequestModel { return EqualUnmodifiableListView(value); } + @override + @JsonKey() + final AuthModel? authModel; final List? _isHeaderEnabledList; @override List? get isHeaderEnabledList { @@ -324,7 +360,7 @@ class _$HttpRequestModelImpl extends _HttpRequestModel { @override String toString() { - return 'HttpRequestModel(method: $method, url: $url, headers: $headers, params: $params, isHeaderEnabledList: $isHeaderEnabledList, isParamEnabledList: $isParamEnabledList, bodyContentType: $bodyContentType, body: $body, query: $query, formData: $formData)'; + return 'HttpRequestModel(method: $method, url: $url, headers: $headers, params: $params, authModel: $authModel, isHeaderEnabledList: $isHeaderEnabledList, isParamEnabledList: $isParamEnabledList, bodyContentType: $bodyContentType, body: $body, query: $query, formData: $formData)'; } @override @@ -336,6 +372,8 @@ class _$HttpRequestModelImpl extends _HttpRequestModel { (identical(other.url, url) || other.url == url) && const DeepCollectionEquality().equals(other._headers, _headers) && const DeepCollectionEquality().equals(other._params, _params) && + (identical(other.authModel, authModel) || + other.authModel == authModel) && const DeepCollectionEquality().equals( other._isHeaderEnabledList, _isHeaderEnabledList, @@ -359,6 +397,7 @@ class _$HttpRequestModelImpl extends _HttpRequestModel { url, const DeepCollectionEquality().hash(_headers), const DeepCollectionEquality().hash(_params), + authModel, const DeepCollectionEquality().hash(_isHeaderEnabledList), const DeepCollectionEquality().hash(_isParamEnabledList), bodyContentType, @@ -390,6 +429,7 @@ abstract class _HttpRequestModel extends HttpRequestModel { final String url, final List? headers, final List? params, + final AuthModel? authModel, final List? isHeaderEnabledList, final List? isParamEnabledList, final ContentType bodyContentType, @@ -411,6 +451,8 @@ abstract class _HttpRequestModel extends HttpRequestModel { @override List? get params; @override + AuthModel? get authModel; + @override List? get isHeaderEnabledList; @override List? get isParamEnabledList; diff --git a/packages/better_networking/lib/models/http_request_model.g.dart b/packages/better_networking/lib/models/http_request_model.g.dart index bd024acc3..48e7a17aa 100644 --- a/packages/better_networking/lib/models/http_request_model.g.dart +++ b/packages/better_networking/lib/models/http_request_model.g.dart @@ -18,6 +18,9 @@ _$HttpRequestModelImpl _$$HttpRequestModelImplFromJson( params: (json['params'] as List?) ?.map((e) => NameValueModel.fromJson(Map.from(e as Map))) .toList(), + authModel: json['authModel'] == null + ? const AuthModel(type: APIAuthType.none) + : AuthModel.fromJson(Map.from(json['authModel'] as Map)), isHeaderEnabledList: (json['isHeaderEnabledList'] as List?) ?.map((e) => e as bool) .toList(), @@ -41,6 +44,7 @@ Map _$$HttpRequestModelImplToJson( 'url': instance.url, 'headers': instance.headers?.map((e) => e.toJson()).toList(), 'params': instance.params?.map((e) => e.toJson()).toList(), + 'authModel': instance.authModel?.toJson(), 'isHeaderEnabledList': instance.isHeaderEnabledList, 'isParamEnabledList': instance.isParamEnabledList, 'bodyContentType': _$ContentTypeEnumMap[instance.bodyContentType]!, diff --git a/packages/better_networking/lib/models/models.dart b/packages/better_networking/lib/models/models.dart index a33c6fddc..2987393d9 100644 --- a/packages/better_networking/lib/models/models.dart +++ b/packages/better_networking/lib/models/models.dart @@ -1,2 +1,8 @@ export 'http_request_model.dart'; export 'http_response_model.dart'; +export 'auth/api_auth_model.dart'; +export 'auth/auth_api_key_model.dart'; +export 'auth/auth_basic_model.dart'; +export 'auth/auth_bearer_model.dart'; +export 'auth/auth_jwt_model.dart'; +export 'auth/auth_digest_model.dart'; diff --git a/packages/better_networking/lib/services/http_service.dart b/packages/better_networking/lib/services/http_service.dart index 31c7ef06f..844ef69be 100644 --- a/packages/better_networking/lib/services/http_service.dart +++ b/packages/better_networking/lib/services/http_service.dart @@ -16,6 +16,7 @@ final httpClientManager = HttpClientManager(); Future<(HttpResponse?, Duration?, String?)> sendHttpRequest( String requestId, APIType apiType, + AuthModel? authData, HttpRequestModel requestModel, { SupportedUriSchemes defaultUriScheme = kDefaultUriScheme, bool noSSL = false, @@ -25,15 +26,25 @@ Future<(HttpResponse?, Duration?, String?)> sendHttpRequest( } final client = httpClientManager.createClient(requestId, noSSL: noSSL); + HttpRequestModel authenticatedRequestModel = requestModel.copyWith(); + + try { + if (authData != null && authData.type != APIAuthType.none) { + authenticatedRequestModel = await handleAuth(requestModel, authData); + } + } catch (e) { + return (null, null, e.toString()); + } + (Uri?, String?) uriRec = getValidRequestUri( - requestModel.url, - requestModel.enabledParams, + authenticatedRequestModel.url, + authenticatedRequestModel.enabledParams, defaultUriScheme: defaultUriScheme, ); if (uriRec.$1 != null) { Uri requestUrl = uriRec.$1!; - Map headers = requestModel.enabledHeadersMap; + Map headers = authenticatedRequestModel.enabledHeadersMap; bool overrideContentType = false; HttpResponse? response; String? body; @@ -43,26 +54,26 @@ Future<(HttpResponse?, Duration?, String?)> sendHttpRequest( var isMultiPartRequest = requestModel.bodyContentType == ContentType.formdata; - if (kMethodsWithBody.contains(requestModel.method)) { - var requestBody = requestModel.body; + if (kMethodsWithBody.contains(authenticatedRequestModel.method)) { + var requestBody = authenticatedRequestModel.body; if (requestBody != null && !isMultiPartRequest && requestBody.isNotEmpty) { body = requestBody; - if (requestModel.hasContentTypeHeader) { + if (authenticatedRequestModel.hasContentTypeHeader) { overrideContentType = true; } else { headers[HttpHeaders.contentTypeHeader] = - requestModel.bodyContentType.header; + authenticatedRequestModel.bodyContentType.header; } } if (isMultiPartRequest) { var multiPartRequest = http.MultipartRequest( - requestModel.method.name.toUpperCase(), + authenticatedRequestModel.method.name.toUpperCase(), requestUrl, ); multiPartRequest.headers.addAll(headers); - for (var formData in requestModel.formDataList) { + for (var formData in authenticatedRequestModel.formDataList) { if (formData.type == FormDataType.text) { multiPartRequest.fields.addAll({formData.name: formData.value}); } else { @@ -84,7 +95,7 @@ Future<(HttpResponse?, Duration?, String?)> sendHttpRequest( return (convertedMultiPartResponse, stopwatch.elapsed, null); } } - switch (requestModel.method) { + switch (authenticatedRequestModel.method) { case HTTPVerb.get: response = await client.get(requestUrl, headers: headers); break; @@ -98,7 +109,7 @@ Future<(HttpResponse?, Duration?, String?)> sendHttpRequest( case HTTPVerb.options: final request = prepareHttpRequest( url: requestUrl, - method: requestModel.method.name.toUpperCase(), + method: authenticatedRequestModel.method.name.toUpperCase(), headers: headers, body: body, overrideContentType: overrideContentType, @@ -109,13 +120,13 @@ Future<(HttpResponse?, Duration?, String?)> sendHttpRequest( } } if (apiType == APIType.graphql) { - var requestBody = getGraphQLBody(requestModel); + var requestBody = getGraphQLBody(authenticatedRequestModel); if (requestBody != null) { var contentLength = utf8.encode(requestBody).length; if (contentLength > 0) { body = requestBody; headers[HttpHeaders.contentLengthHeader] = contentLength.toString(); - if (!requestModel.hasContentTypeHeader) { + if (!authenticatedRequestModel.hasContentTypeHeader) { headers[HttpHeaders.contentTypeHeader] = ContentType.json.header; } } diff --git a/packages/better_networking/lib/utils/auth/digest_auth_utils.dart b/packages/better_networking/lib/utils/auth/digest_auth_utils.dart new file mode 100644 index 000000000..c6dd34b05 --- /dev/null +++ b/packages/better_networking/lib/utils/auth/digest_auth_utils.dart @@ -0,0 +1,223 @@ +import 'dart:convert'; +import 'dart:math' as math; +import 'package:convert/convert.dart'; +import 'package:crypto/crypto.dart' as crypto; +import '../../consts.dart'; +import '../../models/models.dart'; + +Map? splitAuthenticateHeader(String header) { + if (!header.startsWith('Digest ')) { + return null; + } + header = header.substring(7); // remove 'Digest ' + + var ret = {}; + + final components = header.split(',').map((token) => token.trim()); + for (var component in components) { + final kv = component.split('='); + ret[kv[0]] = kv.getRange(1, kv.length).join('=').replaceAll('"', ''); + } + return ret; +} + +String sha256Hash(String data) { + var content = const Utf8Encoder().convert(data); + var sha256 = crypto.sha256; + var digest = sha256.convert(content).toString(); + return digest; +} + +String md5Hash(String data) { + var content = const Utf8Encoder().convert(data); + var md5 = crypto.md5; + var digest = md5.convert(content).toString(); + return digest; +} + +String _formatNonceCount(int nc) { + return nc.toRadixString(16).padLeft(8, '0'); +} + +String _computeHA1( + String realm, + String? algorithm, + String username, + String password, + String? nonce, + String? cnonce, +) { + if (algorithm == 'MD5') { + final token1 = '$username:$realm:$password'; + return md5Hash(token1); + } else if (algorithm == 'MD5-sess') { + final token1 = '$username:$realm:$password'; + final md51 = md5Hash(token1); + final token2 = '$md51:$nonce:$cnonce'; + return md5Hash(token2); + } else if (algorithm == 'SHA-256') { + final token1 = '$username:$realm:$password'; + return sha256Hash(token1); + } else if (algorithm == 'SHA-256-sess') { + final token1 = '$username:$realm:$password'; + final sha256_1 = sha256Hash(token1); + final token2 = '$sha256_1:$nonce:$cnonce'; + return sha256Hash(token2); + } else { + throw ArgumentError.value(algorithm, 'algorithm', 'Unsupported algorithm'); + } +} + +Map computeResponse( + String method, + String path, + String body, + String? algorithm, + String? qop, + String? opaque, + String realm, + String? cnonce, + String? nonce, + int nc, + String username, + String password, +) { + var ret = {}; + + algorithm ??= 'MD5'; + final ha1 = _computeHA1(realm, algorithm, username, password, nonce, cnonce); + + String ha2; + + if (algorithm.startsWith('MD5')) { + if (qop == 'auth-int') { + final bodyHash = md5Hash(body); + final token2 = '$method:$path:$bodyHash'; + ha2 = md5Hash(token2); + } else { + // qop in [null, auth] + final token2 = '$method:$path'; + ha2 = md5Hash(token2); + } + } else { + if (qop == 'auth-int') { + final bodyHash = sha256Hash(body); + final token2 = '$method:$path:$bodyHash'; + ha2 = sha256Hash(token2); + } else { + // qop in [null, auth] + final token2 = '$method:$path'; + ha2 = sha256Hash(token2); + } + } + + final nonceCount = _formatNonceCount(nc); + ret['username'] = username; + ret['realm'] = realm; + ret['nonce'] = nonce; + ret['uri'] = path; + if (qop != null) { + ret['qop'] = qop; + } + ret['nc'] = nonceCount; + ret['cnonce'] = cnonce; + if (opaque != null) { + ret['opaque'] = opaque; + } + ret['algorithm'] = algorithm; + + if (algorithm.startsWith('MD5')) { + if (qop == null) { + final token3 = '$ha1:$nonce:$ha2'; + ret['response'] = md5Hash(token3); + } else if (kQop.contains(qop)) { + final token3 = '$ha1:$nonce:$nonceCount:$cnonce:$qop:$ha2'; + ret['response'] = md5Hash(token3); + } + } else { + if (qop == null) { + final token3 = '$ha1:$nonce:$ha2'; + ret['response'] = sha256Hash(token3); + } else if (kQop.contains(qop)) { + final token3 = '$ha1:$nonce:$nonceCount:$cnonce:$qop:$ha2'; + ret['response'] = sha256Hash(token3); + } + } + + return ret; +} + +class DigestAuth { + String username; + String password; + + // must get from first response + String? _algorithm; + String? _qop; + String? _realm; + String? _nonce; + String? _opaque; + + int _nc = 0; // request counter + + DigestAuth(this.username, this.password); + + // Constructor that takes an AuthDigestModel + DigestAuth.fromModel(AuthDigestModel model) + : username = model.username, + password = model.password, + _realm = model.realm, + _nonce = model.nonce, + _algorithm = model.algorithm, + _qop = model.qop, + _opaque = model.opaque.isNotEmpty ? model.opaque : null; + + String _computeNonce() { + final rnd = math.Random.secure(); + + final values = List.generate(16, (i) => rnd.nextInt(256)); + + return hex.encode(values); + } + + String getAuthString(HttpRequestModel res) { + final cnonce = _computeNonce(); + final url = Uri.parse(res.url); + final method = res.method.name.toUpperCase(); + final body = res.body ?? ''; + _nc += 1; + // if url has query parameters, append query to path + final path = url.hasQuery ? '${url.path}?${url.query}' : url.path; + + // after the first request we have the nonce, so we can provide credentials + final authValues = computeResponse( + method, + path, + body, + _algorithm, + _qop, + _opaque, + _realm!, + cnonce, + _nonce, + _nc, + username, + password, + ); + final authValuesString = authValues.entries + .where((e) => e.value != null) + .map( + (e) => [ + e.key, + '=', + ['algorithm', 'qop', 'nc'].contains(e.key) ? '' : '"', + e.value, + ['algorithm', 'qop', 'nc'].contains(e.key) ? '' : '"', + ].join(''), + ) + .toList() + .join(', '); + final authString = 'Digest $authValuesString'; + return authString; + } +} diff --git a/packages/better_networking/lib/utils/auth/handle_auth.dart b/packages/better_networking/lib/utils/auth/handle_auth.dart new file mode 100644 index 000000000..b18c3dab4 --- /dev/null +++ b/packages/better_networking/lib/utils/auth/handle_auth.dart @@ -0,0 +1,170 @@ +import 'dart:convert'; +import 'dart:math'; +import 'package:better_networking/utils/auth/jwt_auth_utils.dart'; +import 'package:better_networking/utils/auth/digest_auth_utils.dart'; +import 'package:better_networking/better_networking.dart'; + +Future handleAuth( + HttpRequestModel httpRequestModel, + AuthModel? authData, +) async { + if (authData == null || authData.type == APIAuthType.none) { + return httpRequestModel; + } + + List updatedHeaders = List.from( + httpRequestModel.headers ?? [], + ); + List updatedParams = List.from(httpRequestModel.params ?? []); + List updatedHeaderEnabledList = List.from( + httpRequestModel.isHeaderEnabledList ?? [], + ); + List updatedParamEnabledList = List.from( + httpRequestModel.isParamEnabledList ?? [], + ); + + switch (authData.type) { + case APIAuthType.basic: + if (authData.basic != null) { + final basicAuth = authData.basic!; + final encoded = base64Encode( + utf8.encode('${basicAuth.username}:${basicAuth.password}'), + ); + updatedHeaders.add( + NameValueModel(name: 'Authorization', value: 'Basic $encoded'), + ); + updatedHeaderEnabledList.add(true); + } + break; + + case APIAuthType.bearer: + if (authData.bearer != null) { + final bearerAuth = authData.bearer!; + updatedHeaders.add( + NameValueModel( + name: 'Authorization', + value: 'Bearer ${bearerAuth.token}', + ), + ); + updatedHeaderEnabledList.add(true); + } + break; + + case APIAuthType.jwt: + if (authData.jwt != null) { + final jwtAuth = authData.jwt!; + final jwtToken = generateJWT(jwtAuth); + + if (jwtAuth.addTokenTo == 'header') { + final headerValue = jwtAuth.headerPrefix.isNotEmpty + ? '${jwtAuth.headerPrefix} $jwtToken' + : jwtToken; + updatedHeaders.add( + NameValueModel(name: 'Authorization', value: headerValue), + ); + updatedHeaderEnabledList.add(true); + } else if (jwtAuth.addTokenTo == 'query') { + final paramKey = jwtAuth.queryParamKey.isNotEmpty + ? jwtAuth.queryParamKey + : 'token'; + updatedParams.add(NameValueModel(name: paramKey, value: jwtToken)); + updatedParamEnabledList.add(true); + } + } + break; + + case APIAuthType.apiKey: + if (authData.apikey != null) { + final apiKeyAuth = authData.apikey!; + if (apiKeyAuth.location == 'header') { + updatedHeaders.add( + NameValueModel(name: apiKeyAuth.name, value: apiKeyAuth.key), + ); + updatedHeaderEnabledList.add(true); + } else if (apiKeyAuth.location == 'query') { + updatedParams.add( + NameValueModel(name: apiKeyAuth.name, value: apiKeyAuth.key), + ); + updatedParamEnabledList.add(true); + } + } + break; + + case APIAuthType.none: + break; + case APIAuthType.digest: + if (authData.digest != null) { + final digestAuthModel = authData.digest!; + + if (digestAuthModel.realm.isNotEmpty && + digestAuthModel.nonce.isNotEmpty) { + final digestAuth = DigestAuth.fromModel(digestAuthModel); + final authString = digestAuth.getAuthString(httpRequestModel); + + updatedHeaders.add( + NameValueModel(name: 'Authorization', value: authString), + ); + updatedHeaderEnabledList.add(true); + } else { + final httpResult = await sendHttpRequest( + "digest-${Random.secure()}", + APIType.rest, + null, + httpRequestModel, + ); + final httpResponse = httpResult.$1; + + if (httpResponse == null) { + throw Exception("Initial Digest request failed: no response"); + } + + if (httpResponse.statusCode == 401) { + final wwwAuthHeader = httpResponse.headers[kHeaderWwwAuthenticate]; + + if (wwwAuthHeader == null) { + throw Exception("401 response missing www-authenticate header"); + } + + final authParams = splitAuthenticateHeader(wwwAuthHeader); + + if (authParams == null) { + throw Exception("Invalid Digest header format"); + } + + final updatedDigestModel = digestAuthModel.copyWith( + realm: authParams['realm'] ?? '', + nonce: authParams['nonce'] ?? '', + algorithm: authParams['algorithm'] ?? 'MD5', + qop: authParams['qop'] ?? 'auth', + opaque: authParams['opaque'] ?? '', + ); + + final digestAuth = DigestAuth.fromModel(updatedDigestModel); + final authString = digestAuth.getAuthString(httpRequestModel); + updatedHeaders.add( + NameValueModel(name: 'Authorization', value: authString), + ); + updatedHeaderEnabledList.add(true); + } else { + throw Exception( + "Initial Digest request failed due to unexpected status code: ${httpResponse.body}. Status Code: ${httpResponse.statusCode}", + ); + } + } + } + break; + case APIAuthType.oauth1: + // TODO: Handle this case. + throw UnimplementedError(); + case APIAuthType.oauth2: + // TODO: Handle this case. + throw UnimplementedError(); + } + + return httpRequestModel.copyWith( + headers: updatedHeaders, + params: updatedParams, + isHeaderEnabledList: updatedHeaderEnabledList, + isParamEnabledList: updatedParamEnabledList, + ); +} diff --git a/packages/better_networking/lib/utils/auth/jwt_auth_utils.dart b/packages/better_networking/lib/utils/auth/jwt_auth_utils.dart new file mode 100644 index 000000000..3a81a07aa --- /dev/null +++ b/packages/better_networking/lib/utils/auth/jwt_auth_utils.dart @@ -0,0 +1,93 @@ +import 'dart:convert'; +import 'package:better_networking/models/auth/auth_jwt_model.dart'; +import 'package:dart_jsonwebtoken/dart_jsonwebtoken.dart'; + +String generateJWT(AuthJwtModel jwtAuth) { + try { + // Parse header if provided + Map headerMap = {}; + if (jwtAuth.header.isNotEmpty) { + try { + headerMap = json.decode(jwtAuth.header) as Map; + } catch (e) { + // If header parsing fails, use empty header + headerMap = {}; + } + } + + // Parse payload if provided + Map payloadMap = {}; + if (jwtAuth.payload.isNotEmpty) { + try { + payloadMap = json.decode(jwtAuth.payload) as Map; + } catch (e) { + // If payload parsing fails, use empty payload + payloadMap = {}; + } + } + + // Add issued at time if not present + if (!payloadMap.containsKey('iat')) { + payloadMap['iat'] = DateTime.now().millisecondsSinceEpoch ~/ 1000; + } + final jwt = JWT(payloadMap, header: headerMap); + + final key = _createKey( + jwtAuth.secret, + jwtAuth.algorithm, + jwtAuth.isSecretBase64Encoded, + jwtAuth.privateKey, + ); + final token = jwt.sign( + key, + algorithm: JWTAlgorithm.fromName(jwtAuth.algorithm), + ); + + return token; + } catch (e) { + throw Exception('Failed to generate JSON Wweb Token: $e'); + } +} + +JWTKey _createKey( + String secret, + String algorithm, + bool isSecretBase64Encoded, + String? privateKey, +) { + if (algorithm.startsWith('HS')) { + if (isSecretBase64Encoded) { + final decodedSecret = base64.decode(secret); + return SecretKey(String.fromCharCodes(decodedSecret)); + } else { + return SecretKey(secret); + } + } + if (algorithm.startsWith('RS') || algorithm.startsWith('PS')) { + if (privateKey == null) { + throw Exception( + 'Failed to generate JSON Wweb Token: Private Key not Found', + ); + } + return RSAPrivateKey(privateKey); + } + if (algorithm.startsWith('ES')) { + if (privateKey == null) { + throw Exception( + 'Failed to generate JSON Wweb Token: Private Key not Found', + ); + } + return ECPrivateKey(privateKey); + } + + if (algorithm == 'EdDSA') { + if (privateKey == null) { + throw Exception( + 'Failed to generate JSON Wweb Token: Private Key not Found', + ); + } + return EdDSAPrivateKey.fromPEM(privateKey); + } + + return SecretKey(secret, isBase64Encoded: isSecretBase64Encoded); +} diff --git a/packages/better_networking/lib/utils/utils.dart b/packages/better_networking/lib/utils/utils.dart index d334a022f..7857ed81c 100644 --- a/packages/better_networking/lib/utils/utils.dart +++ b/packages/better_networking/lib/utils/utils.dart @@ -4,3 +4,4 @@ export 'http_request_utils.dart'; export 'http_response_utils.dart'; export 'string_utils.dart' hide RandomStringGenerator; export 'uri_utils.dart'; +export 'auth/handle_auth.dart'; diff --git a/packages/better_networking/pubspec.yaml b/packages/better_networking/pubspec.yaml index 1f91a5124..50caa33fa 100644 --- a/packages/better_networking/pubspec.yaml +++ b/packages/better_networking/pubspec.yaml @@ -18,6 +18,9 @@ dependencies: flutter: sdk: flutter collection: ^1.18.0 + convert: ^3.1.2 + crypto: ^3.0.6 + dart_jsonwebtoken: ^3.2.0 freezed_annotation: ^2.4.1 http: ^1.3.0 http_parser: ^4.1.2 diff --git a/packages/better_networking/test/models/auth/api_auth_model_test.dart b/packages/better_networking/test/models/auth/api_auth_model_test.dart new file mode 100644 index 000000000..6946ed372 --- /dev/null +++ b/packages/better_networking/test/models/auth/api_auth_model_test.dart @@ -0,0 +1,225 @@ +import 'package:better_networking/models/auth/api_auth_model.dart'; +import 'package:better_networking/models/auth/auth_basic_model.dart'; +import 'package:better_networking/models/auth/auth_bearer_model.dart'; +import 'package:better_networking/consts.dart'; +import 'package:test/test.dart'; +import 'auth_models.dart'; + +void main() { + group('Testing AuthModel (API Auth Model)', () { + test("Testing AuthModel copyWith", () { + var authModel = authModel1; + final authModelCopyWith = authModel.copyWith( + type: APIAuthType.bearer, + bearer: const AuthBearerModel(token: 'new-bearer-token'), + basic: null, + ); + expect(authModelCopyWith.type, APIAuthType.bearer); + expect(authModelCopyWith.bearer?.token, 'new-bearer-token'); + expect(authModelCopyWith.basic, null); + // original model unchanged + expect(authModel.type, APIAuthType.basic); + expect(authModel.basic?.username, 'john_doe'); + }); + + test("Testing AuthModel toJson", () { + var authModel = authModel1; + expect(authModel.toJson(), authModelJson1); + }); + + test("Testing AuthModel fromJson for basic authentication", () { + var authModel = authModel1; + final modelFromJson = AuthModel.fromJson(authModelJson1); + expect(modelFromJson, authModel); + expect(modelFromJson.type, APIAuthType.basic); + expect(modelFromJson.basic?.username, 'john_doe'); + expect(modelFromJson.basic?.password, 'secure_password'); + }); + + test("Testing AuthModel fromJson for bearer authentication", () { + var authModel = authModel2; + final modelFromJson = AuthModel.fromJson(authModelJson2); + expect(modelFromJson, authModel); + expect(modelFromJson.type, APIAuthType.bearer); + expect( + modelFromJson.bearer?.token, + 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c', + ); + }); + + test("Testing AuthModel fromJson for api key authentication", () { + var authModel = authModel3; + final apiKeyModelJson = { + "type": "apiKey", + "apikey": authApiKeyModelJson1, + "bearer": null, + "basic": null, + "jwt": null, + "digest": null, + }; + final modelFromJson = AuthModel.fromJson(apiKeyModelJson); + expect(modelFromJson, authModel); + expect(modelFromJson.type, APIAuthType.apiKey); + expect(modelFromJson.apikey?.key, 'ak-test-key-12345'); + expect(modelFromJson.apikey?.location, 'header'); + expect(modelFromJson.apikey?.name, 'x-api-key'); + }); + + test("Testing AuthModel fromJson for jwt authentication", () { + var authModel = authModel4; + final jwtModelJson = { + "type": "jwt", + "apikey": null, + "bearer": null, + "basic": null, + "jwt": authJwtModelJson1, + "digest": null, + }; + final modelFromJson = AuthModel.fromJson(jwtModelJson); + expect(modelFromJson, authModel); + expect(modelFromJson.type, APIAuthType.jwt); + expect(modelFromJson.jwt?.secret, 'jwt-secret-key'); + expect(modelFromJson.jwt?.algorithm, 'RS256'); + expect(modelFromJson.jwt?.isSecretBase64Encoded, true); + expect(modelFromJson.jwt?.headerPrefix, 'JWT'); + }); + + test("Testing AuthModel fromJson for digest authentication", () { + var authModel = authModel5; + final digestModelJson = { + "type": "digest", + "apikey": null, + "bearer": null, + "basic": null, + "jwt": null, + "digest": authDigestModelJson1, + }; + final modelFromJson = AuthModel.fromJson(digestModelJson); + expect(modelFromJson, authModel); + expect(modelFromJson.type, APIAuthType.digest); + expect(modelFromJson.digest?.algorithm, 'SHA-256'); + expect(modelFromJson.digest?.username, 'digest_user'); + expect(modelFromJson.digest?.password, 'digest_pass'); + expect(modelFromJson.digest?.realm, 'protected-area'); + expect(modelFromJson.digest?.qop, 'auth-int'); + }); + + test("Testing AuthModel getters for different auth types", () { + expect(authModelNone.type, APIAuthType.none); + expect(authModel1.type, APIAuthType.basic); + expect(authModel2.type, APIAuthType.bearer); + expect(authModel3.type, APIAuthType.apiKey); + expect(authModel4.type, APIAuthType.jwt); + expect(authModel5.type, APIAuthType.digest); + }); + + test("Testing AuthModel with basic authentication", () { + var authModel = authModel1; + expect(authModel.type, APIAuthType.basic); + expect(authModel.basic, isNotNull); + expect(authModel.basic?.username, 'john_doe'); + expect(authModel.basic?.password, 'secure_password'); + expect(authModel.bearer, null); + expect(authModel.apikey, null); + expect(authModel.jwt, null); + expect(authModel.digest, null); + }); + + test("Testing AuthModel with bearer authentication", () { + var authModel = authModel2; + expect(authModel.type, APIAuthType.bearer); + expect(authModel.bearer, isNotNull); + expect( + authModel.bearer?.token, + startsWith('eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9'), + ); + expect(authModel.basic, null); + expect(authModel.apikey, null); + expect(authModel.jwt, null); + expect(authModel.digest, null); + }); + + test("Testing AuthModel with API key authentication", () { + var authModel = authModel3; + expect(authModel.type, APIAuthType.apiKey); + expect(authModel.apikey, isNotNull); + expect(authModel.apikey?.key, 'ak-test-key-12345'); + expect(authModel.apikey?.location, 'header'); + expect(authModel.apikey?.name, 'x-api-key'); + expect(authModel.basic, null); + expect(authModel.bearer, null); + expect(authModel.jwt, null); + expect(authModel.digest, null); + }); + + test("Testing AuthModel with JWT authentication", () { + var authModel = authModel4; + expect(authModel.type, APIAuthType.jwt); + expect(authModel.jwt, isNotNull); + expect(authModel.jwt?.secret, 'jwt-secret-key'); + expect(authModel.jwt?.algorithm, 'RS256'); + expect(authModel.jwt?.isSecretBase64Encoded, true); + expect(authModel.basic, null); + expect(authModel.bearer, null); + expect(authModel.apikey, null); + expect(authModel.digest, null); + }); + + test("Testing AuthModel with digest authentication", () { + var authModel = authModel5; + expect(authModel.type, APIAuthType.digest); + expect(authModel.digest, isNotNull); + expect(authModel.digest?.username, 'digest_user'); + expect(authModel.digest?.algorithm, 'SHA-256'); + expect(authModel.digest?.qop, 'auth-int'); + expect(authModel.basic, null); + expect(authModel.bearer, null); + expect(authModel.apikey, null); + expect(authModel.jwt, null); + }); + + test("Testing AuthModel with none authentication", () { + var authModel = authModelNone; + expect(authModel.type, APIAuthType.none); + expect(authModel.basic, null); + expect(authModel.bearer, null); + expect(authModel.apikey, null); + expect(authModel.jwt, null); + expect(authModel.digest, null); + }); + + test("Testing AuthModel equality", () { + const authModel1Copy = AuthModel( + type: APIAuthType.basic, + basic: AuthBasicAuthModel( + username: 'john_doe', + password: 'secure_password', + ), + ); + expect(authModel1, authModel1Copy); + expect(authModel1, isNot(authModel2)); + expect(authModelNone, const AuthModel(type: APIAuthType.none)); + }); + + test("Testing AuthModel JSON serialization for different types", () { + var bearerModel = authModel2; + var bearerJson = bearerModel.toJson(); + expect(bearerJson['type'], 'bearer'); + expect(bearerJson['bearer'], isNotNull); + expect(bearerJson['basic'], null); + + final modelFromJson = AuthModel.fromJson(authModelJson2); + expect(modelFromJson, bearerModel); + }); + + test("Testing AuthModel JSON serialization for none type", () { + var noneModel = authModelNone; + var noneJson = noneModel.toJson(); + expect(noneJson, authModelNoneJson); + + final modelFromJson = AuthModel.fromJson(authModelNoneJson); + expect(modelFromJson, noneModel); + expect(modelFromJson.type, APIAuthType.none); + }); + }); +} diff --git a/packages/better_networking/test/models/auth/auth_api_key_model_test.dart b/packages/better_networking/test/models/auth/auth_api_key_model_test.dart new file mode 100644 index 000000000..8a121b0c7 --- /dev/null +++ b/packages/better_networking/test/models/auth/auth_api_key_model_test.dart @@ -0,0 +1,67 @@ +import 'package:better_networking/models/auth/auth_api_key_model.dart'; +import 'package:test/test.dart'; +import 'auth_models.dart'; + +void main() { + group('Testing AuthApiKeyModel', () { + test("Testing AuthApiKeyModel copyWith", () { + var authApiKeyModel = authApiKeyModel1; + final authApiKeyModelCopyWith = authApiKeyModel.copyWith( + key: 'new_api_key', + location: 'query', + ); + expect(authApiKeyModelCopyWith.key, 'new_api_key'); + expect(authApiKeyModelCopyWith.location, 'query'); + // original model unchanged + expect(authApiKeyModel.key, 'ak-test-key-12345'); + expect(authApiKeyModel.location, 'header'); + expect(authApiKeyModel.name, 'x-api-key'); + }); + + test("Testing AuthApiKeyModel toJson", () { + var authApiKeyModel = authApiKeyModel1; + expect(authApiKeyModel.toJson(), authApiKeyModelJson1); + }); + + test("Testing AuthApiKeyModel fromJson", () { + var authApiKeyModel = authApiKeyModel1; + final modelFromJson = AuthApiKeyModel.fromJson(authApiKeyModelJson1); + expect(modelFromJson, authApiKeyModel); + expect(modelFromJson.key, 'ak-test-key-12345'); + expect(modelFromJson.location, 'header'); + expect(modelFromJson.name, 'x-api-key'); + }); + + test("Testing AuthApiKeyModel getters", () { + var authApiKeyModel = authApiKeyModel1; + expect(authApiKeyModel.key, 'ak-test-key-12345'); + expect(authApiKeyModel.location, 'header'); + expect(authApiKeyModel.name, 'x-api-key'); + }); + + test("Testing AuthApiKeyModel default values", () { + const authApiKeyModelMinimal = AuthApiKeyModel(key: 'test-key'); + expect(authApiKeyModelMinimal.key, 'test-key'); + expect(authApiKeyModelMinimal.location, 'header'); // default value + expect(authApiKeyModelMinimal.name, 'x-api-key'); // default value + }); + + test("Testing AuthApiKeyModel equality", () { + const authApiKeyModel1Copy = AuthApiKeyModel( + key: 'ak-test-key-12345', + location: 'header', + name: 'x-api-key', + ); + expect(authApiKeyModel1, authApiKeyModel1Copy); + expect(authApiKeyModel1, isNot(authApiKeyModel2)); + }); + + test("Testing AuthApiKeyModel with different configurations", () { + expect(authApiKeyModel2.key, 'query-api-key-67890'); + expect(authApiKeyModel2.location, 'query'); + expect(authApiKeyModel2.name, 'api_key'); + expect(authApiKeyModel1.location, isNot(authApiKeyModel2.location)); + expect(authApiKeyModel1.name, isNot(authApiKeyModel2.name)); + }); + }); +} diff --git a/packages/better_networking/test/models/auth/auth_basic_model_test.dart b/packages/better_networking/test/models/auth/auth_basic_model_test.dart new file mode 100644 index 000000000..221c9b150 --- /dev/null +++ b/packages/better_networking/test/models/auth/auth_basic_model_test.dart @@ -0,0 +1,53 @@ +import 'package:better_networking/models/auth/auth_basic_model.dart'; +import 'package:test/test.dart'; +import 'auth_models.dart'; + +void main() { + group('Testing AuthBasicAuthModel', () { + test("Testing AuthBasicAuthModel copyWith", () { + var authBasicModel = authBasicModel1; + final authBasicModelCopyWith = authBasicModel.copyWith( + password: 'new_password', + ); + expect(authBasicModelCopyWith.password, 'new_password'); + // original model unchanged + expect(authBasicModel.username, 'john_doe'); + expect(authBasicModel.password, 'secure_password'); + }); + + test("Testing AuthBasicAuthModel toJson", () { + var authBasicModel = authBasicModel1; + expect(authBasicModel.toJson(), authBasicModelJson1); + }); + + test("Testing AuthBasicAuthModel fromJson", () { + var authBasicModel = authBasicModel1; + final modelFromJson = AuthBasicAuthModel.fromJson(authBasicModelJson1); + expect(modelFromJson, authBasicModel); + expect(modelFromJson.username, 'john_doe'); + expect(modelFromJson.password, 'secure_password'); + }); + + test("Testing AuthBasicAuthModel getters", () { + var authBasicModel = authBasicModel1; + expect(authBasicModel.username, 'john_doe'); + expect(authBasicModel.password, 'secure_password'); + }); + + test("Testing AuthBasicAuthModel equality", () { + const authBasicModel1Copy = AuthBasicAuthModel( + username: 'john_doe', + password: 'secure_password', + ); + expect(authBasicModel1, authBasicModel1Copy); + expect(authBasicModel1, isNot(authBasicModel2)); + }); + + test("Testing AuthBasicAuthModel with different values", () { + expect(authBasicModel2.username, 'jane_smith'); + expect(authBasicModel2.password, 'another_password'); + expect(authBasicModel1.username, isNot(authBasicModel2.username)); + expect(authBasicModel1.password, isNot(authBasicModel2.password)); + }); + }); +} diff --git a/packages/better_networking/test/models/auth/auth_bearer_model_test.dart b/packages/better_networking/test/models/auth/auth_bearer_model_test.dart new file mode 100644 index 000000000..389934ed0 --- /dev/null +++ b/packages/better_networking/test/models/auth/auth_bearer_model_test.dart @@ -0,0 +1,57 @@ +import 'package:better_networking/models/auth/auth_bearer_model.dart'; +import 'package:test/test.dart'; +import 'auth_models.dart'; + +void main() { + group('Testing AuthBearerModel', () { + test("Testing AuthBearerModel copyWith", () { + var authBearerModel = authBearerModel1; + final authBearerModelCopyWith = authBearerModel.copyWith( + token: 'new_bearer_token', + ); + expect(authBearerModelCopyWith.token, 'new_bearer_token'); + // original model unchanged + expect( + authBearerModel.token, + 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c', + ); + }); + + test("Testing AuthBearerModel toJson", () { + var authBearerModel = authBearerModel1; + expect(authBearerModel.toJson(), authBearerModelJson1); + }); + + test("Testing AuthBearerModel fromJson", () { + var authBearerModel = authBearerModel1; + final modelFromJson = AuthBearerModel.fromJson(authBearerModelJson1); + expect(modelFromJson, authBearerModel); + expect( + modelFromJson.token, + 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c', + ); + }); + + test("Testing AuthBearerModel getters", () { + var authBearerModel = authBearerModel1; + expect( + authBearerModel.token, + 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c', + ); + }); + + test("Testing AuthBearerModel equality", () { + const authBearerModel1Copy = AuthBearerModel( + token: + 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c', + ); + expect(authBearerModel1, authBearerModel1Copy); + expect(authBearerModel1, isNot(authBearerModel2)); + }); + + test("Testing AuthBearerModel with different tokens", () { + expect(authBearerModel2.token, 'different_bearer_token_value'); + expect(authBearerModel1.token, isNot(authBearerModel2.token)); + }); + }); +} diff --git a/packages/better_networking/test/models/auth/auth_digest_model_test.dart b/packages/better_networking/test/models/auth/auth_digest_model_test.dart new file mode 100644 index 000000000..15a31399b --- /dev/null +++ b/packages/better_networking/test/models/auth/auth_digest_model_test.dart @@ -0,0 +1,87 @@ +import 'package:better_networking/models/auth/auth_digest_model.dart'; +import 'package:test/test.dart'; +import 'auth_models.dart'; + +void main() { + group('Testing AuthDigestModel', () { + test("Testing AuthDigestModel copyWith", () { + var authDigestModel = authDigestModel1; + final authDigestModelCopyWith = authDigestModel.copyWith( + username: 'new_user', + algorithm: 'MD5', + qop: 'auth', + ); + expect(authDigestModelCopyWith.username, 'new_user'); + expect(authDigestModelCopyWith.algorithm, 'MD5'); + expect(authDigestModelCopyWith.qop, 'auth'); + // original model unchanged + expect(authDigestModel.username, 'digest_user'); + expect(authDigestModel.algorithm, 'SHA-256'); + expect(authDigestModel.qop, 'auth-int'); + }); + + test("Testing AuthDigestModel toJson", () { + var authDigestModel = authDigestModel1; + expect(authDigestModel.toJson(), authDigestModelJson1); + }); + + test("Testing AuthDigestModel fromJson", () { + var authDigestModel = authDigestModel1; + final modelFromJson = AuthDigestModel.fromJson(authDigestModelJson1); + expect(modelFromJson, authDigestModel); + expect(modelFromJson.username, 'digest_user'); + expect(modelFromJson.password, 'digest_pass'); + expect(modelFromJson.realm, 'protected-area'); + expect(modelFromJson.algorithm, 'SHA-256'); + }); + + test("Testing AuthDigestModel getters", () { + var authDigestModel = authDigestModel1; + expect(authDigestModel.username, 'digest_user'); + expect(authDigestModel.password, 'digest_pass'); + expect(authDigestModel.realm, 'protected-area'); + expect(authDigestModel.nonce, 'dcd98b7102dd2f0e8b11d0f600bfb0c093'); + expect(authDigestModel.algorithm, 'SHA-256'); + expect(authDigestModel.qop, 'auth-int'); + expect(authDigestModel.opaque, '5ccc069c403ebaf9f0171e9517f40e41'); + }); + + test("Testing AuthDigestModel equality", () { + const authDigestModel1Copy = AuthDigestModel( + username: 'digest_user', + password: 'digest_pass', + realm: 'protected-area', + nonce: 'dcd98b7102dd2f0e8b11d0f600bfb0c093', + algorithm: 'SHA-256', + qop: 'auth-int', + opaque: '5ccc069c403ebaf9f0171e9517f40e41', + ); + expect(authDigestModel1, authDigestModel1Copy); + expect(authDigestModel1, isNot(authDigestModel2)); + }); + + test("Testing AuthDigestModel with different configurations", () { + expect(authDigestModel2.username, 'another_digest_user'); + expect(authDigestModel2.password, 'another_digest_pass'); + expect(authDigestModel2.realm, 'different-realm'); + expect(authDigestModel2.nonce, 'abc12345678901234567890abcdef012'); + expect(authDigestModel2.algorithm, 'MD5'); + expect(authDigestModel2.qop, 'auth'); + expect(authDigestModel2.opaque, 'fedcba0987654321098765432109876543'); + + // Compare differences + expect(authDigestModel1.username, isNot(authDigestModel2.username)); + expect(authDigestModel1.algorithm, isNot(authDigestModel2.algorithm)); + expect(authDigestModel1.qop, isNot(authDigestModel2.qop)); + expect(authDigestModel1.realm, isNot(authDigestModel2.realm)); + }); + + test("Testing AuthDigestModel nonce and opaque values", () { + var authDigestModel = authDigestModel1; + expect(authDigestModel.nonce.length, 34); + expect(authDigestModel.opaque.length, 32); + expect(authDigestModel.nonce, matches(RegExp(r'^[a-f0-9]+$'))); + expect(authDigestModel.opaque, matches(RegExp(r'^[a-f0-9]+$'))); + }); + }); +} diff --git a/packages/better_networking/test/models/auth/auth_jwt_model_test.dart b/packages/better_networking/test/models/auth/auth_jwt_model_test.dart new file mode 100644 index 000000000..f6c475272 --- /dev/null +++ b/packages/better_networking/test/models/auth/auth_jwt_model_test.dart @@ -0,0 +1,112 @@ +import 'package:better_networking/models/auth/auth_jwt_model.dart'; +import 'package:test/test.dart'; +import 'auth_models.dart'; + +void main() { + group('Testing AuthJwtModel', () { + test("Testing AuthJwtModel copyWith", () { + var authJwtModel = authJwtModel1; + final authJwtModelCopyWith = authJwtModel.copyWith( + secret: 'new_secret', + algorithm: 'HS256', + isSecretBase64Encoded: false, + ); + expect(authJwtModelCopyWith.secret, 'new_secret'); + expect(authJwtModelCopyWith.algorithm, 'HS256'); + expect(authJwtModelCopyWith.isSecretBase64Encoded, false); + // original model unchanged + expect(authJwtModel.secret, 'jwt-secret-key'); + expect(authJwtModel.algorithm, 'RS256'); + expect(authJwtModel.isSecretBase64Encoded, true); + }); + + test("Testing AuthJwtModel toJson", () { + var authJwtModel = authJwtModel1; + expect(authJwtModel.toJson(), authJwtModelJson1); + }); + + test("Testing AuthJwtModel fromJson", () { + var authJwtModel = authJwtModel1; + final modelFromJson = AuthJwtModel.fromJson(authJwtModelJson1); + expect(modelFromJson, authJwtModel); + expect(modelFromJson.secret, 'jwt-secret-key'); + expect(modelFromJson.algorithm, 'RS256'); + expect(modelFromJson.isSecretBase64Encoded, true); + }); + + test("Testing AuthJwtModel getters", () { + var authJwtModel = authJwtModel1; + expect(authJwtModel.secret, 'jwt-secret-key'); + expect( + authJwtModel.privateKey, + startsWith('-----BEGIN RSA PRIVATE KEY-----'), + ); + expect(authJwtModel.payload, '{"user_id": 123, "exp": 1735689600}'); + expect(authJwtModel.addTokenTo, 'header'); + expect(authJwtModel.algorithm, 'RS256'); + expect(authJwtModel.isSecretBase64Encoded, true); + expect(authJwtModel.headerPrefix, 'JWT'); + expect(authJwtModel.queryParamKey, 'jwt_token'); + expect(authJwtModel.header, 'X-JWT-Token'); + }); + + test("Testing AuthJwtModel equality", () { + const authJwtModel1Copy = AuthJwtModel( + secret: 'jwt-secret-key', + privateKey: '''-----BEGIN RSA PRIVATE KEY----- +MIICWgIBAAKBgHa+iOFqaom/Eg1xlBapqu6JPDHMhsCLy06i4/yZ6KFTz8RWBDG8 +rRdhqSTOWCGtLq+unK/A1lkexaYE3lHBbn/2dzDjaXA48G/B4s4R6ixigQDWnZJd +e4GVKuLOZx82tDSl0yLQOzOzUMygj8IRBgp7CaL4WBRo5DwGRXAON9A7AgMBAAEC +gYAlotZ3u+bwqeLq5+jsFfLbkBvIHO9I8AYMcoyYb5/QImRj8m955Ddohce6prxA +UEfP3yRCgHhv3tT+feSJPSnsbPIpWnmnvDdy+NLij6rYKjga8oYyskg8wpYKSsgO +nNTWI8jLDTM2TFGXAR+Pn+yQ120fmcdhMKsnshnxitHhAQJBAM58Tz/SKb+Hgojs +Le3WJfs1meK0ecEHVZr9p+8mXmn1qUWddG/Mi1m2Zr3ycef+JMDp8CKexa/dacSV +00D+G6ECQQCTN/tEBBia1+eMy3+GKYVH/M7jVSPxjcTQF3qnBnd752AJNqHUpaFO +af8d1omyRY8DdCgTs/JjfesveaL0Uz5bAkB+bVCctBKJye/b5DhO+qLwyCX70CMI +VHRO3Oa5IBYI7LiC/mBvn57nBC6uOMcTk+FvGQ3GNM632mrLSi06CxxhAkA92BeS +xBG+ApL//4DL0GdwDVCwCVU3JTIXpLVeswXApDsgw7WKCiZQNZD5bOWdYUEp10L6 +u+5IQ15oLDX7Y3jfAkBtpWyAuhQwYLpiLPa82U9zus9IxYpfqohVBeiT5UasSssx +OUdMDWRxHzjEexN0nmD1nIPbKNJd0/rvj7jI82Kh +-----END RSA PRIVATE KEY-----''', + payload: '{"user_id": 123, "exp": 1735689600}', + addTokenTo: 'header', + algorithm: 'RS256', + isSecretBase64Encoded: true, + headerPrefix: 'JWT', + queryParamKey: 'jwt_token', + header: 'X-JWT-Token', + ); + expect(authJwtModel1, authJwtModel1Copy); + expect(authJwtModel1, isNot(authJwtModel2)); + }); + + test("Testing AuthJwtModel with different configurations", () { + expect(authJwtModel2.secret, 'different-jwt-secret'); + expect(authJwtModel2.algorithm, 'HS256'); + expect(authJwtModel2.isSecretBase64Encoded, false); + expect(authJwtModel2.addTokenTo, 'query'); + expect(authJwtModel2.headerPrefix, 'Bearer'); + expect(authJwtModel2.header, 'Authorization'); + + // Compare differences + expect(authJwtModel1.secret, isNot(authJwtModel2.secret)); + expect(authJwtModel1.algorithm, isNot(authJwtModel2.algorithm)); + expect( + authJwtModel1.isSecretBase64Encoded, + isNot(authJwtModel2.isSecretBase64Encoded), + ); + expect(authJwtModel1.addTokenTo, isNot(authJwtModel2.addTokenTo)); + }); + + test("Testing AuthJwtModel payload parsing", () { + var authJwtModel = authJwtModel1; + expect(authJwtModel.payload, contains('user_id')); + expect(authJwtModel.payload, contains('exp')); + + var authJwtModel2Local = authJwtModel2; + expect(authJwtModel2Local.payload, contains('sub')); + expect(authJwtModel2Local.payload, contains('name')); + expect(authJwtModel2Local.payload, contains('John Doe')); + }); + }); +} diff --git a/packages/better_networking/test/models/auth/auth_models.dart b/packages/better_networking/test/models/auth/auth_models.dart new file mode 100644 index 000000000..50990796d --- /dev/null +++ b/packages/better_networking/test/models/auth/auth_models.dart @@ -0,0 +1,212 @@ +import 'package:better_networking/models/auth/api_auth_model.dart'; +import 'package:better_networking/models/auth/auth_basic_model.dart'; +import 'package:better_networking/models/auth/auth_bearer_model.dart'; +import 'package:better_networking/models/auth/auth_api_key_model.dart'; +import 'package:better_networking/models/auth/auth_jwt_model.dart'; +import 'package:better_networking/models/auth/auth_digest_model.dart'; +import 'package:better_networking/consts.dart'; + +/// Auth models for testing + +const authBasicModel1 = AuthBasicAuthModel( + username: 'john_doe', + password: 'secure_password', +); + +const authBasicModel2 = AuthBasicAuthModel( + username: 'jane_smith', + password: 'another_password', +); + +const authBearerModel1 = AuthBearerModel( + token: + 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c', +); + +const authBearerModel2 = AuthBearerModel(token: 'different_bearer_token_value'); + +const authApiKeyModel1 = AuthApiKeyModel( + key: 'ak-test-key-12345', + location: 'header', + name: 'x-api-key', +); + +const authApiKeyModel2 = AuthApiKeyModel( + key: 'query-api-key-67890', + location: 'query', + name: 'api_key', +); + +const authJwtModel1 = AuthJwtModel( + secret: 'jwt-secret-key', + privateKey: '''-----BEGIN RSA PRIVATE KEY----- +MIICWgIBAAKBgHa+iOFqaom/Eg1xlBapqu6JPDHMhsCLy06i4/yZ6KFTz8RWBDG8 +rRdhqSTOWCGtLq+unK/A1lkexaYE3lHBbn/2dzDjaXA48G/B4s4R6ixigQDWnZJd +e4GVKuLOZx82tDSl0yLQOzOzUMygj8IRBgp7CaL4WBRo5DwGRXAON9A7AgMBAAEC +gYAlotZ3u+bwqeLq5+jsFfLbkBvIHO9I8AYMcoyYb5/QImRj8m955Ddohce6prxA +UEfP3yRCgHhv3tT+feSJPSnsbPIpWnmnvDdy+NLij6rYKjga8oYyskg8wpYKSsgO +nNTWI8jLDTM2TFGXAR+Pn+yQ120fmcdhMKsnshnxitHhAQJBAM58Tz/SKb+Hgojs +Le3WJfs1meK0ecEHVZr9p+8mXmn1qUWddG/Mi1m2Zr3ycef+JMDp8CKexa/dacSV +00D+G6ECQQCTN/tEBBia1+eMy3+GKYVH/M7jVSPxjcTQF3qnBnd752AJNqHUpaFO +af8d1omyRY8DdCgTs/JjfesveaL0Uz5bAkB+bVCctBKJye/b5DhO+qLwyCX70CMI +VHRO3Oa5IBYI7LiC/mBvn57nBC6uOMcTk+FvGQ3GNM632mrLSi06CxxhAkA92BeS +xBG+ApL//4DL0GdwDVCwCVU3JTIXpLVeswXApDsgw7WKCiZQNZD5bOWdYUEp10L6 +u+5IQ15oLDX7Y3jfAkBtpWyAuhQwYLpiLPa82U9zus9IxYpfqohVBeiT5UasSssx +OUdMDWRxHzjEexN0nmD1nIPbKNJd0/rvj7jI82Kh +-----END RSA PRIVATE KEY-----''', + payload: '{"user_id": 123, "exp": 1735689600}', + addTokenTo: 'header', + algorithm: 'RS256', + isSecretBase64Encoded: true, + headerPrefix: 'JWT', + queryParamKey: 'jwt_token', + header: 'X-JWT-Token', +); + +const authJwtModel2 = AuthJwtModel( + secret: 'different-jwt-secret', + payload: '{"sub": "1234567890", "name": "John Doe", "iat": 1516239022}', + addTokenTo: 'query', + algorithm: 'HS256', + isSecretBase64Encoded: false, + headerPrefix: 'Bearer', + queryParamKey: 'token', + header: 'Authorization', +); + +const authDigestModel1 = AuthDigestModel( + username: 'digest_user', + password: 'digest_pass', + realm: 'protected-area', + nonce: 'dcd98b7102dd2f0e8b11d0f600bfb0c093', + algorithm: 'SHA-256', + qop: 'auth-int', + opaque: '5ccc069c403ebaf9f0171e9517f40e41', +); + +const authDigestModel2 = AuthDigestModel( + username: 'another_digest_user', + password: 'another_digest_pass', + realm: 'different-realm', + nonce: 'abc12345678901234567890abcdef012', + algorithm: 'MD5', + qop: 'auth', + opaque: 'fedcba0987654321098765432109876543', +); + +const authModel1 = AuthModel(type: APIAuthType.basic, basic: authBasicModel1); + +const authModel2 = AuthModel( + type: APIAuthType.bearer, + bearer: authBearerModel1, +); + +const authModel3 = AuthModel( + type: APIAuthType.apiKey, + apikey: authApiKeyModel1, +); + +const authModel4 = AuthModel(type: APIAuthType.jwt, jwt: authJwtModel1); + +const authModel5 = AuthModel( + type: APIAuthType.digest, + digest: authDigestModel1, +); + +const authModelNone = AuthModel(type: APIAuthType.none); + +/// JSON representations for testing + +final Map authBasicModelJson1 = { + "username": "john_doe", + "password": "secure_password", +}; + +final Map authBearerModelJson1 = { + "token": + "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c", +}; + +final Map authApiKeyModelJson1 = { + "key": "ak-test-key-12345", + "location": "header", + "name": "x-api-key", +}; + +final Map authJwtModelJson1 = { + "secret": "jwt-secret-key", + "privateKey": '''-----BEGIN RSA PRIVATE KEY----- +MIICWgIBAAKBgHa+iOFqaom/Eg1xlBapqu6JPDHMhsCLy06i4/yZ6KFTz8RWBDG8 +rRdhqSTOWCGtLq+unK/A1lkexaYE3lHBbn/2dzDjaXA48G/B4s4R6ixigQDWnZJd +e4GVKuLOZx82tDSl0yLQOzOzUMygj8IRBgp7CaL4WBRo5DwGRXAON9A7AgMBAAEC +gYAlotZ3u+bwqeLq5+jsFfLbkBvIHO9I8AYMcoyYb5/QImRj8m955Ddohce6prxA +UEfP3yRCgHhv3tT+feSJPSnsbPIpWnmnvDdy+NLij6rYKjga8oYyskg8wpYKSsgO +nNTWI8jLDTM2TFGXAR+Pn+yQ120fmcdhMKsnshnxitHhAQJBAM58Tz/SKb+Hgojs +Le3WJfs1meK0ecEHVZr9p+8mXmn1qUWddG/Mi1m2Zr3ycef+JMDp8CKexa/dacSV +00D+G6ECQQCTN/tEBBia1+eMy3+GKYVH/M7jVSPxjcTQF3qnBnd752AJNqHUpaFO +af8d1omyRY8DdCgTs/JjfesveaL0Uz5bAkB+bVCctBKJye/b5DhO+qLwyCX70CMI +VHRO3Oa5IBYI7LiC/mBvn57nBC6uOMcTk+FvGQ3GNM632mrLSi06CxxhAkA92BeS +xBG+ApL//4DL0GdwDVCwCVU3JTIXpLVeswXApDsgw7WKCiZQNZD5bOWdYUEp10L6 +u+5IQ15oLDX7Y3jfAkBtpWyAuhQwYLpiLPa82U9zus9IxYpfqohVBeiT5UasSssx +OUdMDWRxHzjEexN0nmD1nIPbKNJd0/rvj7jI82Kh +-----END RSA PRIVATE KEY-----''', + "payload": "{\"user_id\": 123, \"exp\": 1735689600}", + "addTokenTo": "header", + "algorithm": "RS256", + "isSecretBase64Encoded": true, + "headerPrefix": "JWT", + "queryParamKey": "jwt_token", + "header": "X-JWT-Token", +}; + +final Map authDigestModelJson1 = { + "username": "digest_user", + "password": "digest_pass", + "realm": "protected-area", + "nonce": "dcd98b7102dd2f0e8b11d0f600bfb0c093", + "algorithm": "SHA-256", + "qop": "auth-int", + "opaque": "5ccc069c403ebaf9f0171e9517f40e41", +}; + +final Map authModelJson1 = { + "type": "basic", + "apikey": null, + "bearer": null, + "basic": {"username": "john_doe", "password": "secure_password"}, + "jwt": null, + "digest": null, +}; + +final Map authModelJson2 = { + "type": "bearer", + "apikey": null, + "bearer": { + "token": + "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c", + }, + "basic": null, + "jwt": null, + "digest": null, +}; + +final Map authModelJson3 = { + "type": "", + "apikey": null, + "bearer": { + "token": + "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c", + }, + "basic": null, + "jwt": null, + "digest": null, +}; + +final Map authModelNoneJson = { + "type": "none", + "apikey": null, + "bearer": null, + "basic": null, + "jwt": null, + "digest": null, +}; diff --git a/packages/better_networking/test/utils/auth/auth_handling_test.dart b/packages/better_networking/test/utils/auth/auth_handling_test.dart new file mode 100644 index 000000000..4448da5ae --- /dev/null +++ b/packages/better_networking/test/utils/auth/auth_handling_test.dart @@ -0,0 +1,546 @@ +import 'package:better_networking/better_networking.dart'; +import 'package:test/test.dart'; + +void main() { + group('Authentication Handling Tests', () { + test( + 'given sendHttpRequest when no authentication is provided then it should not throw any error', + () async { + const httpRequestModel = HttpRequestModel( + method: HTTPVerb.get, + url: 'https://api.apidash.dev/users', + ); + + final result = await sendHttpRequest( + 'test-request', + APIType.rest, + null, + httpRequestModel, + ); + + expect( + result.$1?.request?.url.toString(), + equals('https://api.apidash.dev/users'), + ); + }, + ); + test( + 'given handleAuth when no authentication is provided then it should return the same httpRequestModel', + () async { + const httpRequestModel = HttpRequestModel( + method: HTTPVerb.get, + url: 'https://api.apidash.dev/users', + ); + + final result = await handleAuth(httpRequestModel, null); + + expect(result.headers, isNull); + expect(result.url, equals('https://api.apidash.dev/users')); + }, + ); + + test( + 'given handleAuth when none authentication type is provided then it should add any headers or throw errors', + () async { + const httpRequestModel = HttpRequestModel( + method: HTTPVerb.get, + url: 'https://api.apidash.dev/users', + ); + + const authModel = AuthModel(type: APIAuthType.none); + + final result = await handleAuth(httpRequestModel, authModel); + + expect(result.headers, isNull); + expect(result.url, equals('https://api.apidash.dev/users')); + }, + ); + + test( + 'given handleAuth when basic authentication fields are provided then it should add an authorization header', + () async { + const httpRequestModel = HttpRequestModel( + method: HTTPVerb.get, + url: 'https://api.apidash.dev/users', + ); + + const basicAuth = AuthBasicAuthModel( + username: 'testuser', + password: 'testpass', + ); + const authModel = AuthModel(type: APIAuthType.basic, basic: basicAuth); + + final result = await handleAuth(httpRequestModel, authModel); + + expect(result.headers, isNotEmpty); + expect( + result.headers?.any((h) => h.name.toLowerCase() == 'authorization'), + isTrue, + ); + expect(result.url, equals('https://api.apidash.dev/users')); + }, + ); + + test( + 'given handleAuth when handle bearer authentication fields are provided then it should add an authorization header', + () async { + const httpRequestModel = HttpRequestModel( + method: HTTPVerb.get, + url: 'https://api.apidash.dev/users', + ); + + const bearerAuth = AuthBearerModel(token: 'bearer-token-123'); + const authModel = AuthModel( + type: APIAuthType.bearer, + bearer: bearerAuth, + ); + + final result = await handleAuth(httpRequestModel, authModel); + + expect(result.headers, isNotEmpty); + expect( + result.headers?.any((h) => h.name.toLowerCase() == 'authorization'), + isTrue, + ); + expect(result.url, equals('https://api.apidash.dev/users')); + }, + ); + + test( + 'given handleAuth when API key authentication fields are provided then it should add an authorization header', + () async { + const httpRequestModel = HttpRequestModel( + method: HTTPVerb.get, + url: 'https://api.apidash.dev/users', + ); + + const apiKeyAuth = AuthApiKeyModel( + key: 'api-key-123', + location: 'header', + name: 'X-API-Key', + ); + const authModel = AuthModel( + type: APIAuthType.apiKey, + apikey: apiKeyAuth, + ); + + final result = await handleAuth(httpRequestModel, authModel); + + expect(result.headers, isNotEmpty); + expect( + result.headers?.any((h) => h.name.toLowerCase() == 'x-api-key'), + isTrue, + ); + expect(result.url, equals('https://api.apidash.dev/users')); + }, + ); + + test( + 'given handleAuth when API key authentication fields are provided then it should add an authorization query', + () async { + const httpRequestModel = HttpRequestModel( + method: HTTPVerb.get, + url: 'https://api.apidash.dev/users', + ); + + const apiKeyAuth = AuthApiKeyModel( + key: 'api-key-123', + location: 'query', + name: 'apikey', + ); + const authModel = AuthModel( + type: APIAuthType.apiKey, + apikey: apiKeyAuth, + ); + + final result = await handleAuth(httpRequestModel, authModel); + + expect(result.params, isNotEmpty); + expect(result.params?.any((p) => p.name == 'apikey'), isTrue); + expect(result.url, equals('https://api.apidash.dev/users')); + }, + ); + + test( + 'given handleAuth when JWT authentication fields are provided then it should add an authorization header', + () async { + const httpRequestModel = HttpRequestModel( + method: HTTPVerb.get, + url: 'https://api.apidash.dev/users', + ); + + const jwtAuth = AuthJwtModel( + secret: 'jwt-secret', + payload: '{"sub": "1234567890"}', + addTokenTo: 'header', + algorithm: 'HS256', + isSecretBase64Encoded: false, + headerPrefix: 'Bearer', + queryParamKey: 'token', + header: 'Authorization', + ); + const authModel = AuthModel(type: APIAuthType.jwt, jwt: jwtAuth); + + final result = await handleAuth(httpRequestModel, authModel); + + expect(result.headers, isNotEmpty); + expect( + result.headers?.any((h) => h.name.toLowerCase() == 'authorization'), + isTrue, + ); + expect(result.url, equals('https://api.apidash.dev/users')); + }, + ); + + test( + 'given handleAuth when digest authentication fields are provided then it should add an authorization header', + () async { + const httpRequestModel = HttpRequestModel( + method: HTTPVerb.get, + url: 'https://api.apidash.dev/users', + ); + + const digestAuth = AuthDigestModel( + username: 'digestuser', + password: 'digestpass', + realm: 'test-realm', + nonce: 'test-nonce', + algorithm: 'MD5', + qop: 'auth', + opaque: 'test-opaque', + ); + const authModel = AuthModel( + type: APIAuthType.digest, + digest: digestAuth, + ); + + final result = await handleAuth(httpRequestModel, authModel); + + expect(result.headers, isNotEmpty); + expect( + result.headers?.any((h) => h.name.toLowerCase() == 'authorization'), + isTrue, + ); + expect(result.url, equals('https://api.apidash.dev/users')); + }, + ); + + test( + 'given handleAuth when multiple headers are provided then it should add an authorization header to the existing headers', + () async { + const httpRequestModel = HttpRequestModel( + method: HTTPVerb.get, + url: 'https://api.apidash.dev/users', + headers: [ + NameValueModel(name: 'Content-Type', value: 'application/json'), + NameValueModel(name: 'Accept', value: 'application/json'), + ], + ); + + const bearerAuth = AuthBearerModel(token: 'bearer-token-123'); + const authModel = AuthModel( + type: APIAuthType.bearer, + bearer: bearerAuth, + ); + + final result = await handleAuth(httpRequestModel, authModel); + + expect(result.headers, isNotEmpty); + expect(result.headers?.any((h) => h.name == 'Content-Type'), isTrue); + expect(result.headers?.any((h) => h.name == 'Accept'), isTrue); + expect( + result.headers?.any((h) => h.name.toLowerCase() == 'authorization'), + isTrue, + ); + expect(result.url, equals('https://api.apidash.dev/users')); + }, + ); + + test( + 'given handleAuth when multiple params are provided then it should add it to the existing params', + () async { + const httpRequestModel = HttpRequestModel( + method: HTTPVerb.get, + url: 'https://api.apidash.dev/users', + params: [ + NameValueModel(name: 'limit', value: '10'), + NameValueModel(name: 'offset', value: '0'), + ], + ); + + const apiKeyAuth = AuthApiKeyModel( + key: 'api-key-123', + location: 'query', + name: 'apikey', + ); + const authModel = AuthModel( + type: APIAuthType.apiKey, + apikey: apiKeyAuth, + ); + + final result = await handleAuth(httpRequestModel, authModel); + + expect(result.params, isNotEmpty); + expect(result.params?.any((p) => p.name == 'limit'), isTrue); + expect(result.params?.any((p) => p.name == 'offset'), isTrue); + expect(result.params?.any((p) => p.name == 'apikey'), isTrue); + expect(result.url, equals('https://api.apidash.dev/users')); + }, + ); + + test( + 'given handleAuth when special characters are provided it should not throw an error', + () async { + const httpRequestModel = HttpRequestModel( + method: HTTPVerb.get, + url: 'https://api.apidash.dev/users', + ); + + const basicAuth = AuthBasicAuthModel( + username: 'user@domain.com', + password: r'P@ssw0rd!@#$%^&*()', + ); + const authModel = AuthModel(type: APIAuthType.basic, basic: basicAuth); + + final result = await handleAuth(httpRequestModel, authModel); + + expect(result.headers, isNotEmpty); + expect( + result.headers?.any((h) => h.name.toLowerCase() == 'authorization'), + isTrue, + ); + expect(result.url, equals('https://api.apidash.dev/users')); + }, + ); + + test( + 'given handleAuth when no values are provided it should not throw an error', + () async { + const httpRequestModel = HttpRequestModel( + method: HTTPVerb.get, + url: 'https://api.apidash.dev/users', + ); + + const basicAuth = AuthBasicAuthModel(username: '', password: ''); + const authModel = AuthModel(type: APIAuthType.basic, basic: basicAuth); + + final result = await handleAuth(httpRequestModel, authModel); + + expect(result.headers, isNotEmpty); + expect( + result.headers?.any((h) => h.name.toLowerCase() == 'authorization'), + isTrue, + ); + expect(result.url, equals('https://api.apidash.dev/users')); + }, + ); + + test( + 'given handleAuth when JWT authentication with query parameter is provided then it should add to query params', + () async { + const httpRequestModel = HttpRequestModel( + method: HTTPVerb.get, + url: 'https://api.apidash.dev/users', + ); + + const jwtAuth = AuthJwtModel( + secret: 'jwt-secret', + payload: '{"sub": "1234567890"}', + addTokenTo: 'query', + algorithm: 'HS256', + isSecretBase64Encoded: false, + headerPrefix: 'Bearer', + queryParamKey: 'access_token', + header: 'Authorization', + ); + const authModel = AuthModel(type: APIAuthType.jwt, jwt: jwtAuth); + + final result = await handleAuth(httpRequestModel, authModel); + + expect(result.params, isNotEmpty); + expect(result.params?.any((p) => p.name == 'access_token'), isTrue); + expect(result.url, equals('https://api.apidash.dev/users')); + }, + ); + + test( + 'given handleAuth when JWT authentication with empty query param key then it should use default "token"', + () async { + const httpRequestModel = HttpRequestModel( + method: HTTPVerb.get, + url: 'https://api.apidash.dev/users', + ); + + const jwtAuth = AuthJwtModel( + secret: 'jwt-secret', + payload: '{"sub": "1234567890"}', + addTokenTo: 'query', + algorithm: 'HS256', + isSecretBase64Encoded: false, + headerPrefix: 'Bearer', + queryParamKey: '', + header: 'Authorization', + ); + const authModel = AuthModel(type: APIAuthType.jwt, jwt: jwtAuth); + + final result = await handleAuth(httpRequestModel, authModel); + + expect(result.params, isNotEmpty); + expect(result.params?.any((p) => p.name == 'token'), isTrue); + expect(result.url, equals('https://api.apidash.dev/users')); + }, + ); + + test( + 'given handleAuth when JWT authentication with empty header prefix then it should not add prefix', + () async { + const httpRequestModel = HttpRequestModel( + method: HTTPVerb.get, + url: 'https://api.apidash.dev/users', + ); + + const jwtAuth = AuthJwtModel( + secret: 'jwt-secret', + payload: '{"sub": "1234567890"}', + addTokenTo: 'header', + algorithm: 'HS256', + isSecretBase64Encoded: false, + headerPrefix: '', + queryParamKey: 'token', + header: 'Authorization', + ); + const authModel = AuthModel(type: APIAuthType.jwt, jwt: jwtAuth); + + final result = await handleAuth(httpRequestModel, authModel); + + expect(result.headers, isNotEmpty); + final authHeader = result.headers?.firstWhere( + (h) => h.name.toLowerCase() == 'authorization', + ); + expect(authHeader?.value, isNotNull); + expect(authHeader?.value?.startsWith('Bearer '), isFalse); + expect(result.url, equals('https://api.apidash.dev/users')); + }, + ); + + test( + 'given handleAuth when OAuth1 authentication is provided then it should throw UnimplementedError', + () async { + const httpRequestModel = HttpRequestModel( + method: HTTPVerb.get, + url: 'https://api.apidash.dev/users', + ); + + const authModel = AuthModel(type: APIAuthType.oauth1); + + expect( + () async => await handleAuth(httpRequestModel, authModel), + throwsA(isA()), + ); + }, + ); + + test( + 'given handleAuth when OAuth2 authentication is provided then it should throw UnimplementedError', + () async { + const httpRequestModel = HttpRequestModel( + method: HTTPVerb.get, + url: 'https://api.apidash.dev/users', + ); + + const authModel = AuthModel(type: APIAuthType.oauth2); + + expect( + () async => await handleAuth(httpRequestModel, authModel), + throwsA(isA()), + ); + }, + ); + + test( + 'given handleAuth when basic auth model is null then it should not add headers', + () async { + const httpRequestModel = HttpRequestModel( + method: HTTPVerb.get, + url: 'https://api.apidash.dev/users', + ); + + const authModel = AuthModel(type: APIAuthType.basic, basic: null); + + final result = await handleAuth(httpRequestModel, authModel); + + expect(result.headers, isEmpty); + expect(result.url, equals('https://api.apidash.dev/users')); + }, + ); + + test( + 'given handleAuth when bearer auth model is null then it should not add headers', + () async { + const httpRequestModel = HttpRequestModel( + method: HTTPVerb.get, + url: 'https://api.apidash.dev/users', + ); + + const authModel = AuthModel(type: APIAuthType.bearer, bearer: null); + + final result = await handleAuth(httpRequestModel, authModel); + + expect(result.headers, isEmpty); + expect(result.url, equals('https://api.apidash.dev/users')); + }, + ); + + test( + 'given handleAuth when JWT auth model is null then it should not add headers', + () async { + const httpRequestModel = HttpRequestModel( + method: HTTPVerb.get, + url: 'https://api.apidash.dev/users', + ); + + const authModel = AuthModel(type: APIAuthType.jwt, jwt: null); + + final result = await handleAuth(httpRequestModel, authModel); + + expect(result.headers, isEmpty); + expect(result.url, equals('https://api.apidash.dev/users')); + }, + ); + + test( + 'given handleAuth when API key auth model is null then it should not add headers or params', + () async { + const httpRequestModel = HttpRequestModel( + method: HTTPVerb.get, + url: 'https://api.apidash.dev/users', + ); + + const authModel = AuthModel(type: APIAuthType.apiKey, apikey: null); + + final result = await handleAuth(httpRequestModel, authModel); + + expect(result.headers, isEmpty); + expect(result.params, isEmpty); + expect(result.url, equals('https://api.apidash.dev/users')); + }, + ); + + test( + 'given handleAuth when digest auth model is null then it should not add headers', + () async { + const httpRequestModel = HttpRequestModel( + method: HTTPVerb.get, + url: 'https://api.apidash.dev/users', + ); + + const authModel = AuthModel(type: APIAuthType.digest, digest: null); + + final result = await handleAuth(httpRequestModel, authModel); + + expect(result.headers, isEmpty); + expect(result.url, equals('https://api.apidash.dev/users')); + }, + ); + }); +} diff --git a/packages/better_networking/test/utils/auth/auth_models_test.dart b/packages/better_networking/test/utils/auth/auth_models_test.dart new file mode 100644 index 000000000..1c7b0cd23 --- /dev/null +++ b/packages/better_networking/test/utils/auth/auth_models_test.dart @@ -0,0 +1,340 @@ +import 'package:better_networking/better_networking.dart'; +import 'package:test/test.dart'; + +void main() { + group('AuthModel Tests', () { + test('should create AuthModel with none type', () { + const authModel = AuthModel(type: APIAuthType.none); + + expect(authModel.type, APIAuthType.none); + expect(authModel.basic, isNull); + expect(authModel.bearer, isNull); + expect(authModel.apikey, isNull); + expect(authModel.jwt, isNull); + expect(authModel.digest, isNull); + }); + + test('should create AuthModel with basic authentication', () { + const basicAuth = AuthBasicAuthModel( + username: 'testuser', + password: 'testpass', + ); + + const authModel = AuthModel( + type: APIAuthType.basic, + basic: basicAuth, + ); + + expect(authModel.type, APIAuthType.basic); + expect(authModel.basic, isNotNull); + expect(authModel.basic?.username, 'testuser'); + expect(authModel.basic?.password, 'testpass'); + }); + + test('should create AuthModel with bearer token', () { + const bearerAuth = AuthBearerModel(token: 'bearer-token-123'); + + const authModel = AuthModel( + type: APIAuthType.bearer, + bearer: bearerAuth, + ); + + expect(authModel.type, APIAuthType.bearer); + expect(authModel.bearer, isNotNull); + expect(authModel.bearer?.token, 'bearer-token-123'); + }); + + test('should create AuthModel with API key authentication', () { + const apiKeyAuth = AuthApiKeyModel( + key: 'api-key-123', + location: 'header', + name: 'X-API-Key', + ); + + const authModel = AuthModel( + type: APIAuthType.apiKey, + apikey: apiKeyAuth, + ); + + expect(authModel.type, APIAuthType.apiKey); + expect(authModel.apikey, isNotNull); + expect(authModel.apikey?.key, 'api-key-123'); + expect(authModel.apikey?.location, 'header'); + expect(authModel.apikey?.name, 'X-API-Key'); + }); + + test('should create AuthModel with JWT authentication', () { + const jwtAuth = AuthJwtModel( + secret: 'jwt-secret', + payload: '{"sub": "1234567890"}', + addTokenTo: 'header', + algorithm: 'HS256', + isSecretBase64Encoded: false, + headerPrefix: 'Bearer', + queryParamKey: 'token', + header: 'Authorization', + ); + + const authModel = AuthModel( + type: APIAuthType.jwt, + jwt: jwtAuth, + ); + + expect(authModel.type, APIAuthType.jwt); + expect(authModel.jwt, isNotNull); + expect(authModel.jwt?.secret, 'jwt-secret'); + expect(authModel.jwt?.algorithm, 'HS256'); + expect(authModel.jwt?.isSecretBase64Encoded, false); + }); + + test('should create AuthModel with digest authentication', () { + const digestAuth = AuthDigestModel( + username: 'digestuser', + password: 'digestpass', + realm: 'test-realm', + nonce: 'test-nonce', + algorithm: 'MD5', + qop: 'auth', + opaque: 'test-opaque', + ); + + const authModel = AuthModel( + type: APIAuthType.digest, + digest: digestAuth, + ); + + expect(authModel.type, APIAuthType.digest); + expect(authModel.digest, isNotNull); + expect(authModel.digest?.username, 'digestuser'); + expect(authModel.digest?.realm, 'test-realm'); + expect(authModel.digest?.algorithm, 'MD5'); + }); + + test('should serialize and deserialize AuthModel correctly', () { + const originalModel = AuthModel( + type: APIAuthType.basic, + basic: AuthBasicAuthModel( + username: 'testuser', + password: 'testpass', + ), + ); + + final json = originalModel.toJson(); + final deserializedModel = AuthModel.fromJson(json); + + expect(deserializedModel.type, originalModel.type); + expect(deserializedModel.basic?.username, originalModel.basic?.username); + expect(deserializedModel.basic?.password, originalModel.basic?.password); + }); + + test('should handle copyWith for AuthModel', () { + const originalModel = AuthModel( + type: APIAuthType.basic, + basic: AuthBasicAuthModel( + username: 'testuser', + password: 'testpass', + ), + ); + + const newBasicAuth = AuthBasicAuthModel( + username: 'newuser', + password: 'newpass', + ); + + final copiedModel = originalModel.copyWith( + type: APIAuthType.basic, + basic: newBasicAuth, + ); + + expect(copiedModel.type, APIAuthType.basic); + expect(copiedModel.basic?.username, 'newuser'); + expect(copiedModel.basic?.password, 'newpass'); + }); + + test('should handle API key with default values', () { + const apiKeyAuth = AuthApiKeyModel(key: 'test-key'); + + expect(apiKeyAuth.key, 'test-key'); + expect(apiKeyAuth.location, 'header'); + expect(apiKeyAuth.name, 'x-api-key'); + }); + + test('should handle API key with custom values', () { + const apiKeyAuth = AuthApiKeyModel( + key: 'custom-key', + location: 'query', + name: 'api_key', + ); + + expect(apiKeyAuth.key, 'custom-key'); + expect(apiKeyAuth.location, 'query'); + expect(apiKeyAuth.name, 'api_key'); + }); + + test('should handle JWT with private key', () { + const jwtAuth = AuthJwtModel( + secret: 'jwt-secret', + privateKey: 'private-key-content', + payload: '{"sub": "1234567890"}', + addTokenTo: 'header', + algorithm: 'RS256', + isSecretBase64Encoded: true, + headerPrefix: 'Bearer', + queryParamKey: 'token', + header: 'Authorization', + ); + + expect(jwtAuth.secret, 'jwt-secret'); + expect(jwtAuth.privateKey, 'private-key-content'); + expect(jwtAuth.algorithm, 'RS256'); + expect(jwtAuth.isSecretBase64Encoded, true); + }); + + test('should handle edge cases with empty strings', () { + const basicAuth = AuthBasicAuthModel( + username: '', + password: '', + ); + + const authModel = AuthModel( + type: APIAuthType.basic, + basic: basicAuth, + ); + + expect(authModel.basic?.username, ''); + expect(authModel.basic?.password, ''); + }); + + test('should handle JSON serialization with null values', () { + const authModel = AuthModel(type: APIAuthType.none); + + final json = authModel.toJson(); + final deserializedModel = AuthModel.fromJson(json); + + expect(deserializedModel.type, APIAuthType.none); + expect(deserializedModel.basic, isNull); + expect(deserializedModel.bearer, isNull); + expect(deserializedModel.apikey, isNull); + expect(deserializedModel.jwt, isNull); + expect(deserializedModel.digest, isNull); + }); + + test('should handle complex JWT payload', () { + const complexPayload = ''' + { + "sub": "1234567890", + "name": "John Doe", + "iat": 1516239022, + "exp": 1516242622, + "roles": ["admin", "user"], + "permissions": { + "read": true, + "write": false + } + } + '''; + + const jwtAuth = AuthJwtModel( + secret: 'complex-secret', + payload: complexPayload, + addTokenTo: 'header', + algorithm: 'HS512', + isSecretBase64Encoded: false, + headerPrefix: 'JWT', + queryParamKey: 'jwt_token', + header: 'X-JWT-Token', + ); + + expect(jwtAuth.payload, complexPayload); + expect(jwtAuth.headerPrefix, 'JWT'); + expect(jwtAuth.queryParamKey, 'jwt_token'); + expect(jwtAuth.header, 'X-JWT-Token'); + }); + + test('should handle digest auth with all parameters', () { + const digestAuth = AuthDigestModel( + username: 'digestuser', + password: 'digestpass', + realm: 'api.example.com', + nonce: 'dcd98b7102dd2f0e8b11d0f600bfb0c093', + algorithm: 'SHA-256', + qop: 'auth-int', + opaque: '5ccc069c403ebaf9f0171e9517f40e41', + ); + + expect(digestAuth.username, 'digestuser'); + expect(digestAuth.password, 'digestpass'); + expect(digestAuth.realm, 'api.example.com'); + expect(digestAuth.nonce, 'dcd98b7102dd2f0e8b11d0f600bfb0c093'); + expect(digestAuth.algorithm, 'SHA-256'); + expect(digestAuth.qop, 'auth-int'); + expect(digestAuth.opaque, '5ccc069c403ebaf9f0171e9517f40e41'); + }); + }); + + test('should handle type mismatch scenarios', () { + // Test when type is basic but bearer data is provided + const authModel = AuthModel( + type: APIAuthType.basic, + bearer: AuthBearerModel(token: 'token'), + ); + + expect(authModel.type, APIAuthType.basic); + expect(authModel.bearer?.token, 'token'); + expect(authModel.basic, isNull); + }); + + test('should handle multiple auth types provided', () { + const authModel = AuthModel( + type: APIAuthType.bearer, + basic: AuthBasicAuthModel(username: 'user', password: 'pass'), + bearer: AuthBearerModel(token: 'token'), + apikey: AuthApiKeyModel(key: 'key'), + ); + + expect(authModel.type, APIAuthType.bearer); + expect(authModel.basic, isNotNull); + expect(authModel.bearer, isNotNull); + expect(authModel.apikey, isNotNull); + }); + + test('should handle serialization with special characters', () { + const basicAuth = AuthBasicAuthModel( + username: 'user@domain.com', + password: r'P@ssw0rd!@#$%^&*()', + ); + + const authModel = AuthModel(type: APIAuthType.basic, basic: basicAuth); + + final json = authModel.toJson(); + final deserializedModel = AuthModel.fromJson(json); + + expect(deserializedModel.basic?.username, 'user@domain.com'); + expect(deserializedModel.basic?.password, r'P@ssw0rd!@#$%^&*()'); + }); + + test('should handle very long strings', () { + final longString = 'a' * 1000; + + final bearerAuth = AuthBearerModel(token: longString); + final authModel = AuthModel(type: APIAuthType.bearer, bearer: bearerAuth); + + expect(authModel.bearer?.token, longString); + expect(authModel.bearer?.token.length, 1000); + }); + + test('should handle Unicode characters', () { + const basicAuth = AuthBasicAuthModel( + username: 'user_测试_тест_テスト', + password: 'password_🔑_🚀_💻', + ); + + const authModel = AuthModel(type: APIAuthType.basic, basic: basicAuth); + + final json = authModel.toJson(); + final deserializedModel = AuthModel.fromJson(json); + + expect(deserializedModel.basic?.username, 'user_测试_тест_テスト'); + expect(deserializedModel.basic?.password, 'password_🔑_🚀_💻'); + }); +} diff --git a/packages/better_networking/test/utils/auth/digest_auth_utils_test.dart b/packages/better_networking/test/utils/auth/digest_auth_utils_test.dart new file mode 100644 index 000000000..f92deb527 --- /dev/null +++ b/packages/better_networking/test/utils/auth/digest_auth_utils_test.dart @@ -0,0 +1,424 @@ +import 'package:better_networking/utils/auth/digest_auth_utils.dart'; +import 'package:better_networking/better_networking.dart'; +import 'package:test/test.dart'; + +void main() { + group('Digest Auth Utils Tests', () { + group('splitAuthenticateHeader', () { + test('should parse valid Digest header correctly', () { + const header = + 'Digest realm="test", nonce="123", algorithm=MD5, qop="auth"'; + final result = splitAuthenticateHeader(header); + + expect(result, isNotNull); + expect(result!['realm'], equals('test')); + expect(result['nonce'], equals('123')); + expect(result['algorithm'], equals('MD5')); + expect(result['qop'], equals('auth')); + }); + + test('should return null for non-Digest header', () { + const header = 'Basic realm="test"'; + final result = splitAuthenticateHeader(header); + + expect(result, isNull); + }); + + test('should return null for empty header', () { + const header = ''; + final result = splitAuthenticateHeader(header); + + expect(result, isNull); + }); + + test('should return null for header without Digest prefix', () { + const header = 'realm="test", nonce="123"'; + final result = splitAuthenticateHeader(header); + + expect(result, isNull); + }); + + test('should handle header with quotes around values', () { + const header = + 'Digest realm="my realm", nonce="abc123", opaque="xyz789"'; + final result = splitAuthenticateHeader(header); + + expect(result, isNotNull); + expect(result!['realm'], equals('my realm')); + expect(result['nonce'], equals('abc123')); + expect(result['opaque'], equals('xyz789')); + }); + + test('should handle header with equals in values', () { + const header = 'Digest realm="test=value", nonce="123=456"'; + final result = splitAuthenticateHeader(header); + + expect(result, isNotNull); + expect(result!['realm'], equals('test=value')); + expect(result['nonce'], equals('123=456')); + }); + }); + + group('Hash Functions', () { + test('should compute SHA-256 hash correctly', () { + const input = 'test data'; + final result = sha256Hash(input); + + expect(result, isNotEmpty); + expect( + result, + hasLength(64), + ); // SHA-256 produces 64 character hex string + expect( + result, + equals( + '916f0027a575074ce72a331777c3478d6513f786a591bd892da1a577bf2335f9', + ), + ); + }); + + test('should compute MD5 hash correctly', () { + const input = 'test data'; + final result = md5Hash(input); + + expect(result, isNotEmpty); + expect(result, hasLength(32)); // MD5 produces 32 character hex string + expect(result, equals('eb733a00c0c9d336e65691a37ab54293')); + }); + + test('should handle empty string in SHA-256', () { + const input = ''; + final result = sha256Hash(input); + + expect(result, isNotEmpty); + expect( + result, + equals( + 'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855', + ), + ); + }); + + test('should handle empty string in MD5', () { + const input = ''; + final result = md5Hash(input); + + expect(result, isNotEmpty); + expect(result, equals('d41d8cd98f00b204e9800998ecf8427e')); + }); + }); + + group('computeResponse', () { + test('should compute response with MD5 algorithm and auth qop', () { + final result = computeResponse( + 'GET', + '/api/users', + '', + 'MD5', + 'auth', + 'opaque123', + 'test-realm', + 'cnonce123', + 'nonce123', + 1, + 'testuser', + 'testpass', + ); + + expect(result['username'], equals('testuser')); + expect(result['realm'], equals('test-realm')); + expect(result['nonce'], equals('nonce123')); + expect(result['uri'], equals('/api/users')); + expect(result['qop'], equals('auth')); + expect(result['nc'], equals('00000001')); + expect(result['cnonce'], equals('cnonce123')); + expect(result['opaque'], equals('opaque123')); + expect(result['algorithm'], equals('MD5')); + expect(result['response'], isNotNull); + }); + + test('should compute response with MD5 algorithm and auth-int qop', () { + final result = computeResponse( + 'POST', + '/api/users', + '{"name": "test"}', + 'MD5', + 'auth-int', + null, + 'test-realm', + 'cnonce123', + 'nonce123', + 1, + 'testuser', + 'testpass', + ); + + expect(result['qop'], equals('auth-int')); + expect(result['response'], isNotNull); + expect(result['opaque'], isNull); + }); + + test('should compute response with MD5 algorithm and no qop', () { + final result = computeResponse( + 'GET', + '/api/users', + '', + 'MD5', + null, + null, + 'test-realm', + 'cnonce123', + 'nonce123', + 1, + 'testuser', + 'testpass', + ); + + expect(result['qop'], isNull); + expect(result['response'], isNotNull); + }); + + test('should compute response with SHA-256 algorithm and auth qop', () { + final result = computeResponse( + 'GET', + '/api/users', + '', + 'SHA-256', + 'auth', + null, + 'test-realm', + 'cnonce123', + 'nonce123', + 1, + 'testuser', + 'testpass', + ); + + expect(result['algorithm'], equals('SHA-256')); + expect(result['response'], isNotNull); + }); + + test( + 'should compute response with SHA-256 algorithm and auth-int qop', + () { + final result = computeResponse( + 'POST', + '/api/users', + '{"data": "test"}', + 'SHA-256', + 'auth-int', + null, + 'test-realm', + 'cnonce123', + 'nonce123', + 1, + 'testuser', + 'testpass', + ); + + expect(result['algorithm'], equals('SHA-256')); + expect(result['qop'], equals('auth-int')); + expect(result['response'], isNotNull); + }, + ); + + test('should compute response with SHA-256 algorithm and no qop', () { + final result = computeResponse( + 'GET', + '/api/users', + '', + 'SHA-256', + null, + null, + 'test-realm', + 'cnonce123', + 'nonce123', + 1, + 'testuser', + 'testpass', + ); + + expect(result['algorithm'], equals('SHA-256')); + expect(result['qop'], isNull); + expect(result['response'], isNotNull); + }); + + test('should compute response with MD5-sess algorithm', () { + final result = computeResponse( + 'GET', + '/api/users', + '', + 'MD5-sess', + 'auth', + null, + 'test-realm', + 'cnonce123', + 'nonce123', + 1, + 'testuser', + 'testpass', + ); + + expect(result['algorithm'], equals('MD5-sess')); + expect(result['response'], isNotNull); + }); + + test('should compute response with SHA-256-sess algorithm', () { + final result = computeResponse( + 'GET', + '/api/users', + '', + 'SHA-256-sess', + 'auth', + null, + 'test-realm', + 'cnonce123', + 'nonce123', + 1, + 'testuser', + 'testpass', + ); + + expect(result['algorithm'], equals('SHA-256-sess')); + expect(result['response'], isNotNull); + }); + + test('should throw error for unsupported algorithm', () { + expect( + () => computeResponse( + 'GET', + '/api/users', + '', + 'UNSUPPORTED', + 'auth', + null, + 'test-realm', + 'cnonce123', + 'nonce123', + 1, + 'testuser', + 'testpass', + ), + throwsArgumentError, + ); + }); + }); + + group('DigestAuth class', () { + test('should create DigestAuth from model', () { + const model = AuthDigestModel( + username: 'testuser', + password: 'testpass', + realm: 'test-realm', + nonce: 'test-nonce', + algorithm: 'MD5', + qop: 'auth', + opaque: 'test-opaque', + ); + + final digestAuth = DigestAuth.fromModel(model); + + expect(digestAuth.username, equals('testuser')); + expect(digestAuth.password, equals('testpass')); + }); + + test('should generate auth string correctly', () { + const model = AuthDigestModel( + username: 'testuser', + password: 'testpass', + realm: 'test-realm', + nonce: 'test-nonce', + algorithm: 'MD5', + qop: 'auth', + opaque: 'test-opaque', + ); + + final digestAuth = DigestAuth.fromModel(model); + const httpRequest = HttpRequestModel( + method: HTTPVerb.get, + url: 'https://api.example.com/users', + ); + + final authString = digestAuth.getAuthString(httpRequest); + + expect(authString, startsWith('Digest ')); + expect(authString, contains('username="testuser"')); + expect(authString, contains('realm="test-realm"')); + expect(authString, contains('nonce="test-nonce"')); + expect(authString, contains('uri="/users"')); + expect(authString, contains('algorithm=MD5')); + expect(authString, contains('qop=auth')); + expect(authString, contains('opaque="test-opaque"')); + expect(authString, contains('response="')); + }); + + test('should handle URL with query parameters', () { + const model = AuthDigestModel( + username: 'testuser', + password: 'testpass', + realm: 'test-realm', + nonce: 'test-nonce', + algorithm: 'MD5', + qop: 'auth', + opaque: '', + ); + + final digestAuth = DigestAuth.fromModel(model); + const httpRequest = HttpRequestModel( + method: HTTPVerb.get, + url: 'https://api.example.com/users?limit=10&offset=0', + ); + + final authString = digestAuth.getAuthString(httpRequest); + + expect(authString, contains('uri="/users?limit=10&offset=0"')); + }); + + test('should handle empty opaque value', () { + const model = AuthDigestModel( + username: 'testuser', + password: 'testpass', + realm: 'test-realm', + nonce: 'test-nonce', + algorithm: 'MD5', + qop: 'auth', + opaque: '', + ); + + final digestAuth = DigestAuth.fromModel(model); + const httpRequest = HttpRequestModel( + method: HTTPVerb.get, + url: 'https://api.example.com/users', + ); + + final authString = digestAuth.getAuthString(httpRequest); + + expect(authString, isNot(contains('opaque='))); + }); + + test('should increment nonce count with each call', () { + const model = AuthDigestModel( + username: 'testuser', + password: 'testpass', + realm: 'test-realm', + nonce: 'test-nonce', + algorithm: 'MD5', + qop: 'auth', + opaque: '', + ); + + final digestAuth = DigestAuth.fromModel(model); + const httpRequest = HttpRequestModel( + method: HTTPVerb.get, + url: 'https://api.example.com/users', + ); + + final authString1 = digestAuth.getAuthString(httpRequest); + final authString2 = digestAuth.getAuthString(httpRequest); + + expect(authString1, contains('nc=00000001')); + expect(authString2, contains('nc=00000002')); + }); + }); + }); +} diff --git a/packages/better_networking/test/utils/auth/jwt_auth_utils_test.dart b/packages/better_networking/test/utils/auth/jwt_auth_utils_test.dart new file mode 100644 index 000000000..7bf43694a --- /dev/null +++ b/packages/better_networking/test/utils/auth/jwt_auth_utils_test.dart @@ -0,0 +1,298 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:better_networking/models/auth/auth_jwt_model.dart'; +import 'package:better_networking/utils/auth/jwt_auth_utils.dart'; +import 'package:dart_jsonwebtoken/dart_jsonwebtoken.dart'; + +void main() { + group('JWT Auth Utils Tests', () { + test('should generate JWT with HS256 algorithm', () { + const jwtAuth = AuthJwtModel( + secret: 'test_secret', + payload: '{"user_id": 123, "username": "testuser"}', + addTokenTo: 'header', + algorithm: 'HS256', + isSecretBase64Encoded: false, + headerPrefix: 'Bearer', + queryParamKey: 'token', + header: '{"typ": "JWT"}', + ); + + final token = generateJWT(jwtAuth); + expect(token, isNotEmpty); + expect(token.split('.').length, equals(3)); // JWT has 3 parts + + // Verify the token can be decoded + final decoded = JWT.decode(token); + expect(decoded.payload['user_id'], equals(123)); + expect(decoded.payload['username'], equals('testuser')); + }); + + test('should generate JWT with HS384 algorithm', () { + const jwtAuth = AuthJwtModel( + secret: 'test_secret_384', + payload: '{"role": "admin"}', + addTokenTo: 'header', + algorithm: 'HS384', + isSecretBase64Encoded: false, + headerPrefix: 'Bearer', + queryParamKey: 'token', + header: '', + ); + + final token = generateJWT(jwtAuth); + expect(token, isNotEmpty); + expect(token.split('.').length, equals(3)); + + // Verify the token can be decoded + final decoded = JWT.decode(token); + expect(decoded.payload['role'], equals('admin')); + }); + + test('should generate JWT with HS512 algorithm', () { + const jwtAuth = AuthJwtModel( + secret: 'test_secret_512', + payload: '{"exp": 1234567890}', + addTokenTo: 'header', + algorithm: 'HS512', + isSecretBase64Encoded: false, + headerPrefix: 'Bearer', + queryParamKey: 'token', + header: '', + ); + + final token = generateJWT(jwtAuth); + expect(token, isNotEmpty); + expect(token.split('.').length, equals(3)); + + // Verify the token can be decoded + final decoded = JWT.decode(token); + expect(decoded.payload['exp'], equals(1234567890)); + }); + + test('should generate JWT with base64 encoded secret', () { + const secretBase64 = 'dGVzdF9zZWNyZXQ='; // base64 encoded "test_secret" + const jwtAuth = AuthJwtModel( + secret: secretBase64, + payload: '{"test": "value"}', + addTokenTo: 'header', + algorithm: 'HS256', + isSecretBase64Encoded: true, + headerPrefix: 'Bearer', + queryParamKey: 'token', + header: '', + ); + + final token = generateJWT(jwtAuth); + expect(token, isNotEmpty); + expect(token.split('.').length, equals(3)); + + // Verify the token can be decoded + final decoded = JWT.decode(token); + expect(decoded.payload['test'], equals('value')); + }); + + test('should handle empty payload', () { + const jwtAuth = AuthJwtModel( + secret: 'test_secret', + payload: '', + addTokenTo: 'header', + algorithm: 'HS256', + isSecretBase64Encoded: false, + headerPrefix: 'Bearer', + queryParamKey: 'token', + header: '', + ); + + final token = generateJWT(jwtAuth); + expect(token, isNotEmpty); + expect(token.split('.').length, equals(3)); + + // Verify the token can be decoded and has iat + final decoded = JWT.decode(token); + expect(decoded.payload['iat'], isNotNull); + }); + + test('should handle invalid JSON payload gracefully', () { + const jwtAuth = AuthJwtModel( + secret: 'test_secret', + payload: 'invalid json', + addTokenTo: 'header', + algorithm: 'HS256', + isSecretBase64Encoded: false, + headerPrefix: 'Bearer', + queryParamKey: 'token', + header: '', + ); + + final token = generateJWT(jwtAuth); + expect(token, isNotEmpty); + expect(token.split('.').length, equals(3)); + + // Should have at least iat in payload + final decoded = JWT.decode(token); + expect(decoded.payload['iat'], isNotNull); + }); + + test('should verify generated JWT with correct secret', () { + const secret = 'verification_secret'; + const jwtAuth = AuthJwtModel( + secret: secret, + payload: '{"user": "test"}', + addTokenTo: 'header', + algorithm: 'HS256', + isSecretBase64Encoded: false, + headerPrefix: 'Bearer', + queryParamKey: 'token', + header: '', + ); + + final token = generateJWT(jwtAuth); + + // Verify with correct secret + expect(() => JWT.verify(token, SecretKey(secret)), returnsNormally); + }); + + test('should fail verification with wrong secret', () { + const secret = 'correct_secret'; + const wrongSecret = 'wrong_secret'; + const jwtAuth = AuthJwtModel( + secret: secret, + payload: '{"user": "test"}', + addTokenTo: 'header', + algorithm: 'HS256', + isSecretBase64Encoded: false, + headerPrefix: 'Bearer', + queryParamKey: 'token', + header: '', + ); + + final token = generateJWT(jwtAuth); + + // Verify with wrong secret should throw + expect( + () => JWT.verify(token, SecretKey(wrongSecret)), + throwsA(isA()), + ); + }); + + test( + 'should throw error when generating JWT fails due to invalid payload', + () { + const jwtAuth = AuthJwtModel( + secret: 'test_secret', + payload: + '{"invalid": json}', // Malformed JSON that will cause parsing to fail but createKey to succeed + addTokenTo: 'header', + algorithm: 'HS256', + isSecretBase64Encoded: false, + headerPrefix: 'Bearer', + queryParamKey: 'token', + header: '', + ); + + // This should not throw since we handle JSON parsing gracefully + expect(() => generateJWT(jwtAuth), returnsNormally); + }, + ); + + test('should throw exception for RSA algorithm without private key', () { + const jwtAuth = AuthJwtModel( + secret: 'test_secret', + payload: '{"user": "test"}', + addTokenTo: 'header', + algorithm: 'RS256', + isSecretBase64Encoded: false, + headerPrefix: 'Bearer', + queryParamKey: 'token', + header: '', + privateKey: null, + ); + + expect(() => generateJWT(jwtAuth), throwsA(isA())); + }); + + test('should throw exception for ECDSA algorithm without private key', () { + const jwtAuth = AuthJwtModel( + secret: 'test_secret', + payload: '{"user": "test"}', + addTokenTo: 'header', + algorithm: 'ES256', + isSecretBase64Encoded: false, + headerPrefix: 'Bearer', + queryParamKey: 'token', + header: '', + privateKey: null, + ); + + expect(() => generateJWT(jwtAuth), throwsA(isA())); + }); + + test('should throw exception for PS algorithm without private key', () { + const jwtAuth = AuthJwtModel( + secret: 'test_secret', + payload: '{"user": "test"}', + addTokenTo: 'header', + algorithm: 'PS256', + isSecretBase64Encoded: false, + headerPrefix: 'Bearer', + queryParamKey: 'token', + header: '', + privateKey: null, + ); + + expect(() => generateJWT(jwtAuth), throwsA(isA())); + }); + + test('should throw exception for EdDSA algorithm without private key', () { + const jwtAuth = AuthJwtModel( + secret: 'test_secret', + payload: '{"user": "test"}', + addTokenTo: 'header', + algorithm: 'EdDSA', + isSecretBase64Encoded: false, + headerPrefix: 'Bearer', + queryParamKey: 'token', + header: '', + privateKey: null, + ); + + expect(() => generateJWT(jwtAuth), throwsA(isA())); + }); + + test('should handle invalid header JSON gracefully', () { + const jwtAuth = AuthJwtModel( + secret: 'test_secret', + payload: '{"user": "test"}', + addTokenTo: 'header', + algorithm: 'HS256', + isSecretBase64Encoded: false, + headerPrefix: 'Bearer', + queryParamKey: 'token', + header: '{"invalid": json}', // Invalid JSON + ); + + final token = generateJWT(jwtAuth); + expect(token, isNotEmpty); + expect(token.split('.').length, equals(3)); + + // Should still generate a valid token with empty header + final decoded = JWT.decode(token); + expect(decoded.payload['user'], equals('test')); + }); + + test('should throw exception for unknown algorithm', () { + const jwtAuth = AuthJwtModel( + secret: 'test_secret', + payload: '{"user": "test"}', + addTokenTo: 'header', + algorithm: 'UNKNOWN_ALG', + isSecretBase64Encoded: false, + headerPrefix: 'Bearer', + queryParamKey: 'token', + header: '', + ); + + expect(() => generateJWT(jwtAuth), throwsA(isA())); + }); + }); +} diff --git a/packages/json_field_editor/example/pubspec.lock b/packages/json_field_editor/example/pubspec.lock index 49c1a06a0..ccfa627cd 100644 --- a/packages/json_field_editor/example/pubspec.lock +++ b/packages/json_field_editor/example/pubspec.lock @@ -45,10 +45,10 @@ packages: dependency: "direct main" description: name: cupertino_icons - sha256: d57953e10f9f8327ce64a508a355f0b1ec902193f66288e8cb5070e7c47eeb2d + sha256: ba631d1c7f7bef6b729a622b7b752645a2d076dba9976925b8f25725a30e1ee6 url: "https://pub.dev" source: hosted - version: "1.0.6" + version: "1.0.8" extended_text_field: dependency: transitive description: @@ -61,10 +61,10 @@ packages: dependency: transitive description: name: extended_text_library - sha256: "55d09098ec56fab0d9a8a68950ca0bbf2efa1327937f7cec6af6dfa066234829" + sha256: "13d99f8a10ead472d5e2cf4770d3d047203fe5054b152e9eb5dc692a71befbba" url: "https://pub.dev" source: hosted - version: "12.0.0" + version: "12.0.1" fake_async: dependency: transitive description: diff --git a/packages/multi_trigger_autocomplete_plus/example/pubspec.lock b/packages/multi_trigger_autocomplete_plus/example/pubspec.lock index 7f847cdd1..c3af53252 100644 --- a/packages/multi_trigger_autocomplete_plus/example/pubspec.lock +++ b/packages/multi_trigger_autocomplete_plus/example/pubspec.lock @@ -69,10 +69,10 @@ packages: dependency: transitive description: name: ffi - sha256: "16ed7b077ef01ad6170a3d0c57caa4a112a38d7a2ed5602e0aca9ca6f3d98da6" + sha256: "289279317b4b16eb2bb7e271abccd4bf84ec9bdcbe999e278a94b804f5630418" url: "https://pub.dev" source: hosted - version: "2.1.3" + version: "2.1.4" flutter: dependency: "direct main" description: flutter @@ -119,18 +119,18 @@ packages: dependency: transitive description: name: http - sha256: fe7ab022b76f3034adc518fb6ea04a82387620e19977665ea18d30a1cf43442f + sha256: "2c11f3f94c687ee9bad77c171151672986360b2b001d109814ee7140b2cf261b" url: "https://pub.dev" source: hosted - version: "1.3.0" + version: "1.4.0" http_parser: dependency: transitive description: name: http_parser - sha256: "2aa08ce0341cc9b354a498388e30986515406668dbcc4f7c950c3e715496693b" + sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571" url: "https://pub.dev" source: hosted - version: "4.0.2" + version: "4.1.2" leak_tracker: dependency: transitive description: @@ -214,10 +214,10 @@ packages: dependency: transitive description: name: path_provider_android - sha256: "4adf4fd5423ec60a29506c76581bc05854c55e3a0b72d35bb28d661c9686edf2" + sha256: d0d310befe2c8ab9e7f393288ccbb11b60c019c6b5afc21973eeee4dda2b35e9 url: "https://pub.dev" source: hosted - version: "2.2.15" + version: "2.2.17" path_provider_foundation: dependency: transitive description: @@ -347,10 +347,10 @@ packages: dependency: transitive description: name: web - sha256: cd3543bd5798f6ad290ea73d210f423502e71900302dde696f8bff84bf89a1cb + sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a" url: "https://pub.dev" source: hosted - version: "1.1.0" + version: "1.1.1" xdg_directories: dependency: transitive description: @@ -360,5 +360,5 @@ packages: source: hosted version: "1.1.0" sdks: - dart: ">=3.7.0-0 <4.0.0" - flutter: ">=3.24.0" + dart: ">=3.7.0 <4.0.0" + flutter: ">=3.27.0" diff --git a/pubspec.lock b/pubspec.lock index 590e147c6..0536f2ac5 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -5,18 +5,26 @@ packages: dependency: transitive description: name: _fe_analyzer_shared - sha256: dc27559385e905ad30838356c5f5d574014ba39872d732111cd07ac0beff4c57 + sha256: e55636ed79578b9abca5fecf9437947798f5ef7456308b5cb85720b793eac92f url: "https://pub.dev" source: hosted - version: "80.0.0" + version: "82.0.0" + adaptive_number: + dependency: transitive + description: + name: adaptive_number + sha256: "3a567544e9b5c9c803006f51140ad544aedc79604fd4f3f2c1380003f97c1d77" + url: "https://pub.dev" + source: hosted + version: "1.0.0" analyzer: dependency: transitive description: name: analyzer - sha256: "192d1c5b944e7e53b24b5586db760db934b177d4147c42fbca8c8c5f1eb8d11e" + sha256: "904ae5bb474d32c38fb9482e2d925d5454cda04ddd0e55d2e6826bc72f6ba8c0" url: "https://pub.dev" source: hosted - version: "7.3.0" + version: "7.4.5" ansi_styles: dependency: transitive description: @@ -51,18 +59,18 @@ packages: dependency: transitive description: name: archive - sha256: "528579c7e4579719f04b21eeeeddfd73a18b31dabc22766893b7d1be7f49b967" + sha256: "2fde1607386ab523f7a36bb3e7edb43bd58e6edaf2ffb29d8a6d578b297fdbbd" url: "https://pub.dev" source: hosted - version: "4.0.3" + version: "4.0.7" args: dependency: transitive description: name: args - sha256: bf9f5caeea8d8fe6721a9c358dd8a5c1947b27f1cfaa18b39c301273594919e6 + sha256: d0481093c50b1da8910eb0bb301626d4d8eb7284aa739614d2b394ee09e3ea04 url: "https://pub.dev" source: hosted - version: "2.6.0" + version: "2.7.0" async: dependency: transitive description: @@ -106,10 +114,10 @@ packages: dependency: transitive description: name: bidi - sha256: "9a712c7ddf708f7c41b1923aa83648a3ed44cfd75b04f72d598c45e5be287f9d" + sha256: "77f475165e94b261745cf1032c751e2032b8ed92ccb2bf5716036db79320637d" url: "https://pub.dev" source: hosted - version: "2.0.12" + version: "2.0.13" binary_codec: dependency: transitive description: @@ -186,18 +194,18 @@ packages: dependency: transitive description: name: built_value - sha256: "8b158ab94ec6913e480dc3f752418348b5ae099eb75868b5f4775f0572999c61" + sha256: "082001b5c3dc495d4a42f1d5789990505df20d8547d42507c29050af6933ee27" url: "https://pub.dev" source: hosted - version: "8.9.4" + version: "8.10.1" carousel_slider: dependency: "direct main" description: name: carousel_slider - sha256: "7b006ec356205054af5beaef62e2221160ea36b90fb70a35e4deacd49d0349ae" + sha256: bcc61735345c9ab5cb81073896579e735f81e35fd588907a393143ea986be8ff url: "https://pub.dev" source: hosted - version: "5.0.0" + version: "5.1.1" characters: dependency: transitive description: @@ -218,10 +226,10 @@ packages: dependency: transitive description: name: checked_yaml - sha256: feb6bed21949061731a7a75fc5d2aa727cf160b91af9a3e464c5e3a32e28b5ff + sha256: "959525d3162f249993882720d52b7e0c833978df229be20702b33d48d91de70f" url: "https://pub.dev" source: hosted - version: "2.0.3" + version: "2.0.4" checks: dependency: transitive description: @@ -238,6 +246,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.1.0" + cli_config: + dependency: transitive + description: + name: cli_config + sha256: ac20a183a07002b700f0c25e61b7ee46b23c309d76ab7b7640a028f18e4d99ec + url: "https://pub.dev" + source: hosted + version: "0.2.0" cli_launcher: dependency: transitive description: @@ -282,10 +298,10 @@ packages: dependency: transitive description: name: conventional_commit - sha256: dec15ad1118f029c618651a4359eb9135d8b88f761aa24e4016d061cd45948f2 + sha256: c40b1b449ce2a63fa2ce852f35e3890b1e182f5951819934c0e4a66254bc0dc3 url: "https://pub.dev" source: hosted - version: "0.6.0+1" + version: "0.6.1+1" convert: dependency: transitive description: @@ -298,10 +314,10 @@ packages: dependency: transitive description: name: coverage - sha256: e3493833ea012784c740e341952298f1cc77f1f01b1bbc3eb4eecf6984fb7f43 + sha256: aa07dbe5f2294c827b7edb9a87bba44a9c15a3cc81bc8da2ca19b37322d30080 url: "https://pub.dev" source: hosted - version: "1.11.1" + version: "1.14.1" cross_file: dependency: transitive description: @@ -341,14 +357,22 @@ packages: relative: true source: path version: "0.1.3" + dart_jsonwebtoken: + dependency: transitive + description: + name: dart_jsonwebtoken + sha256: "21ce9f8a8712f741e8d6876a9c82c0f8a257fe928c4378a91d8527b92a3fd413" + url: "https://pub.dev" + source: hosted + version: "3.2.0" dart_style: dependency: "direct main" description: name: dart_style - sha256: "27eb0ae77836989a3bc541ce55595e8ceee0992807f14511552a898ddd0d88ac" + sha256: "5b236382b47ee411741447c1f1e111459c941ea1b3f2b540dde54c210a3662af" url: "https://pub.dev" source: hosted - version: "3.0.1" + version: "3.1.0" dartx: dependency: transitive description: @@ -373,6 +397,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.5.0" + ed25519_edwards: + dependency: transitive + description: + name: ed25519_edwards + sha256: "6ce0112d131327ec6d42beede1e5dfd526069b18ad45dcf654f15074ad9276cd" + url: "https://pub.dev" + source: hosted + version: "0.3.1" equatable: dependency: transitive description: @@ -413,22 +445,6 @@ packages: url: "https://pub.dev" source: hosted version: "1.3.3" - fetch_api: - dependency: transitive - description: - name: fetch_api - sha256: "97f46c25b480aad74f7cc2ad7ccba2c5c6f08d008e68f95c1077286ce243d0e6" - url: "https://pub.dev" - source: hosted - version: "2.2.0" - fetch_client: - dependency: transitive - description: - name: fetch_client - sha256: "9666ee14536778474072245ed5cba07db81ae8eb5de3b7bf4a2d1e2c49696092" - url: "https://pub.dev" - source: hosted - version: "1.1.2" ffi: dependency: transitive description: @@ -457,10 +473,10 @@ packages: dependency: transitive description: name: file_selector_android - sha256: "98ac58e878b05ea2fdb204e7f4fc4978d90406c9881874f901428e01d3b18fbc" + sha256: "6bba3d590ee9462758879741abc132a19133600dd31832f55627442f1ebd7b54" url: "https://pub.dev" source: hosted - version: "0.5.1+12" + version: "0.5.1+14" file_selector_ios: dependency: transitive description: @@ -481,10 +497,10 @@ packages: dependency: transitive description: name: file_selector_macos - sha256: "271ab9986df0c135d45c3cdb6bd0faa5db6f4976d3e4b437cf7d0f258d941bfc" + sha256: "8c9250b2bd2d8d4268e39c82543bacbaca0fda7d29e0728c3c4bbb7c820fd711" url: "https://pub.dev" source: hosted - version: "0.9.4+2" + version: "0.9.4+3" file_selector_platform_interface: dependency: transitive description: @@ -587,10 +603,10 @@ packages: dependency: "direct dev" description: name: flutter_launcher_icons - sha256: bfa04787c85d80ecb3f8777bde5fc10c3de809240c48fa061a2c2bf15ea5211c + sha256: "10f13781741a2e3972126fae08393d3c4e01fa4cd7473326b94b72cf594195e7" url: "https://pub.dev" source: hosted - version: "0.14.3" + version: "0.14.4" flutter_lints: dependency: "direct dev" description: @@ -603,18 +619,18 @@ packages: dependency: "direct main" description: name: flutter_markdown - sha256: e7bbc718adc9476aa14cfddc1ef048d2e21e4e8f18311aaac723266db9f9e7b5 + sha256: "08fb8315236099ff8e90cb87bb2b935e0a724a3af1623000a9cec930468e0f27" url: "https://pub.dev" source: hosted - version: "0.7.6+2" + version: "0.7.7+1" flutter_native_splash: dependency: "direct dev" description: name: flutter_native_splash - sha256: edb09c35ee9230c4b03f13dd45bb3a276d0801865f0a4650b7e2a3bba61a803a + sha256: "8321a6d11a8d13977fa780c89de8d257cce3d841eecfb7a4cadffcc4f12d82dc" url: "https://pub.dev" source: hosted - version: "2.4.5" + version: "2.4.6" flutter_portal: dependency: "direct main" description: @@ -635,10 +651,10 @@ packages: dependency: "direct main" description: name: flutter_svg - sha256: c200fd79c918a40c5cd50ea0877fa13f81bdaf6f0a5d3dbcc2a13e3285d6aa1b + sha256: d44bf546b13025ec7353091516f6881f1d4c633993cb109c3916c3a0159dadf1 url: "https://pub.dev" source: hosted - version: "2.0.17" + version: "2.1.0" flutter_test: dependency: "direct dev" description: flutter @@ -682,10 +698,10 @@ packages: dependency: "direct main" description: name: fvp - sha256: f5012756985f7c8c19caaea2d65baf8a6cf5fee9fe520e1fabb8bc61e1d5f468 + sha256: a2b6f305a5e559abc21b1be06ca0ffb5bb6b5b523d6d45eb8e78d53f3b89e9a2 url: "https://pub.dev" source: hosted - version: "0.30.0" + version: "0.32.1" glob: dependency: transitive description: @@ -769,10 +785,10 @@ packages: dependency: transitive description: name: html - sha256: "1fc58edeaec4307368c60d59b7e15b9d658b57d7f3125098b6294153c75337ec" + sha256: "6d1264f2dffa1b1101c25a91dff0dc2daee4c18e87cd8538729773c073dbf602" url: "https://pub.dev" source: hosted - version: "0.15.5" + version: "0.15.6" html_unescape: dependency: transitive description: @@ -785,10 +801,10 @@ packages: dependency: transitive description: name: http - sha256: fe7ab022b76f3034adc518fb6ea04a82387620e19977665ea18d30a1cf43442f + sha256: "2c11f3f94c687ee9bad77c171151672986360b2b001d109814ee7140b2cf261b" url: "https://pub.dev" source: hosted - version: "1.3.0" + version: "1.4.0" http_multi_server: dependency: transitive description: @@ -809,10 +825,10 @@ packages: dependency: transitive description: name: image - sha256: "13d3349ace88f12f4a0d175eb5c12dcdd39d35c4c109a8a13dfeb6d0bd9e31c3" + sha256: "4e973fcf4caae1a4be2fa0a13157aa38a8f9cb049db6529aa00b4d71abc4d928" url: "https://pub.dev" source: hosted - version: "4.5.3" + version: "4.5.4" insomnia_collection: dependency: transitive description: @@ -853,10 +869,10 @@ packages: dependency: "direct main" description: name: jinja - sha256: "157a05d22c1b60aaf21e3e9f0b26ec27315cd2892f7ac9571e9478eb0d5d62f9" + sha256: e0e14bb04cde45f944d61140612ee9368cfcd890e3ca65573316b7c6ce51c635 url: "https://pub.dev" source: hosted - version: "0.6.1" + version: "0.6.2" js: dependency: transitive description: @@ -899,10 +915,10 @@ packages: dependency: "direct dev" description: name: json_serializable - sha256: "81f04dee10969f89f604e1249382d46b97a1ccad53872875369622b5bfc9e58a" + sha256: c50ef5fc083d5b5e12eef489503ba3bf5ccc899e487d691584699b4bdefeea8c url: "https://pub.dev" source: hosted - version: "6.9.4" + version: "6.9.5" just_audio: dependency: "direct main" description: @@ -923,18 +939,18 @@ packages: dependency: transitive description: name: just_audio_platform_interface - sha256: "271b93b484c6f494ecd72a107fffbdb26b425f170c665b9777a0a24a726f2f24" + sha256: "4cd94536af0219fa306205a58e78d67e02b0555283c1c094ee41e402a14a5c4a" url: "https://pub.dev" source: hosted - version: "4.4.0" + version: "4.5.0" just_audio_web: dependency: transitive description: name: just_audio_web - sha256: "58915be64509a7683c44bf11cd1a23c15a48de104927bee116e3c63c8eeea0d4" + sha256: "6ba8a2a7e87d57d32f0f7b42856ade3d6a9fbe0f1a11fabae0a4f00bb73f0663" url: "https://pub.dev" source: hosted - version: "0.4.14" + version: "0.4.16" just_audio_windows: dependency: "direct main" description: @@ -1027,10 +1043,10 @@ packages: dependency: "direct dev" description: name: melos - sha256: "3f3ab3f902843d1e5a1b1a4dd39a4aca8ba1056f2d32fd8995210fa2843f646f" + sha256: "4280dc46bd5b741887cce1e67e5c1a6aaf3c22310035cf5bd33dceeeda62ed22" url: "https://pub.dev" source: hosted - version: "6.3.2" + version: "6.3.3" meta: dependency: transitive description: @@ -1122,18 +1138,18 @@ packages: dependency: "direct main" description: name: ollama_dart - sha256: "4e40bc499b6fe46ba54a004d2da601c40bd73d66e3f18cf7b03225ccf3d481a6" + sha256: c5c8656aa42e109b5a3e58a9b92661adc8204a5a1c312644c4b448ac2d413b95 url: "https://pub.dev" source: hosted - version: "0.2.2+1" + version: "0.2.3" package_config: dependency: transitive description: name: package_config - sha256: "92d4488434b520a62570293fbd33bb556c7d49230791c1b4bbd973baf6d2dc67" + sha256: f096c55ebb7deb7e384101542bfba8c52696c1b56fca2eb62827989ef2353bbc url: "https://pub.dev" source: hosted - version: "2.1.1" + version: "2.2.0" package_info_plus: dependency: "direct main" description: @@ -1178,10 +1194,10 @@ packages: dependency: transitive description: name: path_provider_android - sha256: "4adf4fd5423ec60a29506c76581bc05854c55e3a0b72d35bb28d661c9686edf2" + sha256: d0d310befe2c8ab9e7f393288ccbb11b60c019c6b5afc21973eeee4dda2b35e9 url: "https://pub.dev" source: hosted - version: "2.2.15" + version: "2.2.17" path_provider_foundation: dependency: transitive description: @@ -1254,6 +1270,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.8" + pointycastle: + dependency: transitive + description: + name: pointycastle + sha256: "92aa3841d083cc4b0f4709b5c74fd6409a3e6ba833ffc7dc6a8fee096366acf5" + url: "https://pub.dev" + source: hosted + version: "4.0.0" pool: dependency: transitive description: @@ -1266,10 +1290,10 @@ packages: dependency: transitive description: name: posix - sha256: a0117dc2167805aa9125b82eee515cc891819bac2f538c83646d355b16f58b9a + sha256: f0d7856b6ca1887cfa6d1d394056a296ae33489db914e365e2044fdada449e62 url: "https://pub.dev" source: hosted - version: "6.0.1" + version: "6.0.2" postman: dependency: transitive description: @@ -1305,26 +1329,26 @@ packages: dependency: "direct main" description: name: provider - sha256: c8a055ee5ce3fd98d6fc872478b03823ffdb448699c6ebdbbc71d59b596fd48c + sha256: "4abbd070a04e9ddc287673bf5a030c7ca8b685ff70218720abab8b092f53dd84" url: "https://pub.dev" source: hosted - version: "6.1.2" + version: "6.1.5" pub_semver: dependency: "direct main" description: name: pub_semver - sha256: "7b3cfbf654f3edd0c6298ecd5be782ce997ddf0e00531b9464b55245185bbbbd" + sha256: "5bfcf68ca79ef689f8990d1160781b4bad40a3bd5e5218ad4076ddb7f4081585" url: "https://pub.dev" source: hosted - version: "2.1.5" + version: "2.2.0" pub_updater: dependency: transitive description: name: pub_updater - sha256: "54e8dc865349059ebe7f163d6acce7c89eb958b8047e6d6e80ce93b13d7c9e60" + sha256: "739a0161d73a6974c0675b864fb0cf5147305f7b077b7f03a58fa7a9ab3e7e7d" url: "https://pub.dev" source: hosted - version: "0.4.0" + version: "0.5.0" pubspec_parse: dependency: transitive description: @@ -1433,18 +1457,18 @@ packages: dependency: "direct main" description: name: shared_preferences - sha256: "846849e3e9b68f3ef4b60c60cf4b3e02e9321bc7f4d8c4692cf87ffa82fc8a3a" + sha256: "6e8bf70b7fef813df4e9a36f658ac46d107db4b4cfe1048b477d4e453a8159f5" url: "https://pub.dev" source: hosted - version: "2.5.2" + version: "2.5.3" shared_preferences_android: dependency: transitive description: name: shared_preferences_android - sha256: a768fc8ede5f0c8e6150476e14f38e2417c0864ca36bb4582be8e21925a03c22 + sha256: "20cbd561f743a342c76c151d6ddb93a9ce6005751e7aa458baad3858bfbfb6ac" url: "https://pub.dev" source: hosted - version: "2.4.6" + version: "2.4.10" shared_preferences_foundation: dependency: transitive description: @@ -1650,6 +1674,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.3.1" + synchronized: + dependency: transitive + description: + name: synchronized + sha256: "0669c70faae6270521ee4f05bffd2919892d42d1276e6c495be80174b6bc0ef6" + url: "https://pub.dev" + source: hosted + version: "3.3.1" term_glyph: dependency: transitive description: @@ -1742,18 +1774,18 @@ packages: dependency: transitive description: name: url_launcher_android - sha256: "6fc2f56536ee873eeb867ad176ae15f304ccccc357848b351f6f0d8d4a40d193" + sha256: "8582d7f6fe14d2652b4c45c9b6c14c0b678c2af2d083a11b604caeba51930d79" url: "https://pub.dev" source: hosted - version: "6.3.14" + version: "6.3.16" url_launcher_ios: dependency: transitive description: name: url_launcher_ios - sha256: "16a513b6c12bb419304e72ea0ae2ab4fed569920d1c7cb850263fe3acc824626" + sha256: "7f2022359d4c099eea7df3fdf739f7d3d3b9faf3166fb1dd390775176e0b76cb" url: "https://pub.dev" source: hosted - version: "6.3.2" + version: "6.3.3" url_launcher_linux: dependency: transitive description: @@ -1782,10 +1814,10 @@ packages: dependency: transitive description: name: url_launcher_web - sha256: "3ba963161bd0fe395917ba881d320b9c4f6dd3c4a233da62ab18a5025c85f1e9" + sha256: "4bd2b7b4dc4d4d0b94e5babfffbca8eac1a126c7f3d6ecbc1a11013faa3abba2" url: "https://pub.dev" source: hosted - version: "2.4.0" + version: "2.4.1" url_launcher_windows: dependency: transitive description: @@ -1806,10 +1838,10 @@ packages: dependency: transitive description: name: vector_graphics - sha256: "44cc7104ff32563122a929e4620cf3efd584194eec6d1d913eb5ba593dbcf6de" + sha256: a4f059dc26fc8295b5921376600a194c4ec7d55e72f2fe4c7d2831e103d461e6 url: "https://pub.dev" source: hosted - version: "1.1.18" + version: "1.1.19" vector_graphics_codec: dependency: transitive description: @@ -1822,10 +1854,10 @@ packages: dependency: "direct main" description: name: vector_graphics_compiler - sha256: "1b4b9e706a10294258727674a340ae0d6e64a7231980f9f9a3d12e4b42407aad" + sha256: "557a315b7d2a6dbb0aaaff84d857967ce6bdc96a63dc6ee2a57ce5a6ee5d3331" url: "https://pub.dev" source: hosted - version: "1.1.16" + version: "1.1.17" vector_math: dependency: transitive description: @@ -1838,26 +1870,26 @@ packages: dependency: "direct main" description: name: video_player - sha256: "48941c8b05732f9582116b1c01850b74dbee1d8520cd7e34ad4609d6df666845" + sha256: "0d55b1f1a31e5ad4c4967bfaa8ade0240b07d20ee4af1dfef5f531056512961a" url: "https://pub.dev" source: hosted - version: "2.9.3" + version: "2.10.0" video_player_android: dependency: transitive description: name: video_player_android - sha256: "7018dbcb395e2bca0b9a898e73989e67c0c4a5db269528e1b036ca38bcca0d0b" + sha256: "4a5135754a62dbc827a64a42ef1f8ed72c962e191c97e2d48744225c2b9ebb73" url: "https://pub.dev" source: hosted - version: "2.7.17" + version: "2.8.7" video_player_avfoundation: dependency: transitive description: name: video_player_avfoundation - sha256: "84b4752745eeccb6e75865c9aab39b3d28eb27ba5726d352d45db8297fbd75bc" + sha256: "9ee764e5cd2fc1e10911ae8ad588e1a19db3b6aa9a6eb53c127c42d3a3c3f22f" url: "https://pub.dev" source: hosted - version: "2.7.0" + version: "2.7.1" video_player_platform_interface: dependency: "direct main" description: @@ -1870,10 +1902,10 @@ packages: dependency: transitive description: name: video_player_web - sha256: "3ef40ea6d72434edbfdba4624b90fd3a80a0740d260667d91e7ecd2d79e13476" + sha256: e8bba2e5d1e159d5048c9a491bb2a7b29c535c612bb7d10c1e21107f5bd365ba url: "https://pub.dev" source: hosted - version: "2.3.4" + version: "2.3.5" vm_service: dependency: transitive description: @@ -1886,10 +1918,10 @@ packages: dependency: transitive description: name: watcher - sha256: "69da27e49efa56a15f8afe8f4438c4ec02eff0a117df1b22ea4aad194fe1c104" + sha256: "0b7fd4a0bbc4b92641dbf20adfd7e3fd1398fe17102d94b674234563e110088a" url: "https://pub.dev" source: hosted - version: "1.1.1" + version: "1.1.2" web: dependency: "direct overridden" description: @@ -1902,18 +1934,18 @@ packages: dependency: transitive description: name: web_socket - sha256: "3c12d96c0c9a4eec095246debcea7b86c0324f22df69893d538fcc6f1b8cce83" + sha256: "34d64019aa8e36bf9842ac014bb5d2f5586ca73df5e4d9bf5c936975cae6982c" url: "https://pub.dev" source: hosted - version: "0.1.6" + version: "1.0.1" web_socket_channel: dependency: transitive description: name: web_socket_channel - sha256: "0b8e2457400d8a859b7b2030786835a28a8e80836ef64402abef392ff4f1d0e5" + sha256: d645757fb0f4773d602444000a8131ff5d48c9e47adfe9772652dd1a4f2d45c8 url: "https://pub.dev" source: hosted - version: "3.0.2" + version: "3.0.3" webdriver: dependency: transitive description: @@ -1934,10 +1966,10 @@ packages: dependency: transitive description: name: win32 - sha256: b89e6e24d1454e149ab20fbb225af58660f0c0bf4475544650700d8e2da54aef + sha256: "66814138c3562338d05613a6e368ed8cfb237ad6d64a9e9334be3f309acfca03" url: "https://pub.dev" source: hosted - version: "5.11.0" + version: "5.14.0" window_manager: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index f34e23bac..0ef4fae78 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -32,7 +32,7 @@ dependencies: flutter_portal: ^1.1.4 flutter_riverpod: ^2.5.1 flutter_svg: ^2.0.17 - fvp: ^0.30.0 + fvp: ^0.32.1 highlight: ^0.7.0 highlighter: ^0.1.1 hive_flutter: ^1.1.0 diff --git a/test/models/history_models.dart b/test/models/history_models.dart index 557b3a2ef..6de41dc1b 100644 --- a/test/models/history_models.dart +++ b/test/models/history_models.dart @@ -18,11 +18,11 @@ final historyMetaModel1 = HistoryMetaModel( /// Basic History Request model 1 final historyRequestModel1 = HistoryRequestModel( - historyId: 'historyId1', - metaData: historyMetaModel1, - httpRequestModel: httpRequestModelGet4, - httpResponseModel: responseModel, -); + historyId: 'historyId1', + metaData: historyMetaModel1, + httpRequestModel: httpRequestModelGet4, + httpResponseModel: responseModel, + authModel: AuthModel(type: APIAuthType.none)); final historyMetaModel2 = HistoryMetaModel( historyId: 'historyId2', @@ -35,11 +35,11 @@ final historyMetaModel2 = HistoryMetaModel( ); final historyRequestModel2 = HistoryRequestModel( - historyId: 'historyId2', - metaData: historyMetaModel2, - httpRequestModel: httpRequestModelPost10, - httpResponseModel: responseModel, -); + historyId: 'historyId2', + metaData: historyMetaModel2, + httpRequestModel: httpRequestModelPost10, + httpResponseModel: responseModel, + authModel: AuthModel(type: APIAuthType.none)); /// JSONs final Map historyMetaModelJson1 = { @@ -59,7 +59,15 @@ final Map historyRequestModelJson1 = { "httpRequestModel": httpRequestModelGet4Json, "httpResponseModel": responseModelJson, 'preRequestScript': null, - 'postRequestScript': null + 'postRequestScript': null, + 'authModel': { + 'type': 'none', + 'apikey': null, + 'bearer': null, + 'basic': null, + 'jwt': null, + 'digest': null + } }; final Map historyMetaModelJson2 = { diff --git a/test/models/http_request_model_test.dart b/test/models/http_request_model_test.dart index 78a520978..2a5cd3970 100644 --- a/test/models/http_request_model_test.dart +++ b/test/models/http_request_model_test.dart @@ -83,4 +83,325 @@ void main() { var httpRequestModel3 = httpRequestModel.copyWith(headers: null); expect(httpRequestModel3.headers, null); }); + + group('HttpRequestModel Auth Tests', () { + test('should create HttpRequestModel with no authentication', () { + const httpRequestModel = HttpRequestModel( + method: HTTPVerb.get, + url: 'https://api.apidash.dev/users', + ); + + expect(httpRequestModel.authModel?.type, APIAuthType.none); + }); + + test('should create HttpRequestModel with basic authentication', () { + const basicAuth = AuthBasicAuthModel( + username: 'testuser', + password: 'testpass', + ); + const authModel = AuthModel( + type: APIAuthType.basic, + basic: basicAuth, + ); + + const httpRequestModel = HttpRequestModel( + method: HTTPVerb.get, + url: 'https://api.apidash.dev/users', + authModel: authModel, + ); + + expect(httpRequestModel.authModel, isNotNull); + expect(httpRequestModel.authModel?.type, APIAuthType.basic); + expect(httpRequestModel.authModel?.basic?.username, 'testuser'); + expect(httpRequestModel.authModel?.basic?.password, 'testpass'); + }); + + test('should create HttpRequestModel with bearer authentication', () { + const bearerAuth = AuthBearerModel(token: 'bearer-token-123'); + const authModel = AuthModel( + type: APIAuthType.bearer, + bearer: bearerAuth, + ); + + const httpRequestModel = HttpRequestModel( + method: HTTPVerb.post, + url: 'https://api.apidash.dev/users', + authModel: authModel, + ); + + expect(httpRequestModel.authModel?.type, APIAuthType.bearer); + expect(httpRequestModel.authModel?.bearer?.token, 'bearer-token-123'); + }); + + test('should create HttpRequestModel with API key authentication', () { + const apiKeyAuth = AuthApiKeyModel( + key: 'api-key-123', + location: 'header', + name: 'X-API-Key', + ); + const authModel = AuthModel( + type: APIAuthType.apiKey, + apikey: apiKeyAuth, + ); + + const httpRequestModel = HttpRequestModel( + method: HTTPVerb.get, + url: 'https://api.apidash.dev/users', + authModel: authModel, + ); + + expect(httpRequestModel.authModel?.type, APIAuthType.apiKey); + expect(httpRequestModel.authModel?.apikey?.key, 'api-key-123'); + expect(httpRequestModel.authModel?.apikey?.location, 'header'); + expect(httpRequestModel.authModel?.apikey?.name, 'X-API-Key'); + }); + + test('should create HttpRequestModel with JWT authentication', () { + const jwtAuth = AuthJwtModel( + secret: 'jwt-secret', + payload: '{"sub": "1234567890"}', + addTokenTo: 'header', + algorithm: 'HS256', + isSecretBase64Encoded: false, + headerPrefix: 'Bearer', + queryParamKey: 'token', + header: 'Authorization', + ); + const authModel = AuthModel( + type: APIAuthType.jwt, + jwt: jwtAuth, + ); + + const httpRequestModel = HttpRequestModel( + method: HTTPVerb.patch, + url: 'https://api.apidash.dev/users/1', + authModel: authModel, + ); + + expect(httpRequestModel.authModel?.type, APIAuthType.jwt); + expect(httpRequestModel.authModel?.jwt?.secret, 'jwt-secret'); + expect(httpRequestModel.authModel?.jwt?.algorithm, 'HS256'); + expect(httpRequestModel.authModel?.jwt?.isSecretBase64Encoded, false); + }); + + test('should create HttpRequestModel with digest authentication', () { + const digestAuth = AuthDigestModel( + username: 'digestuser', + password: 'digestpass', + realm: 'test-realm', + nonce: 'test-nonce', + algorithm: 'MD5', + qop: 'auth', + opaque: 'test-opaque', + ); + const authModel = AuthModel( + type: APIAuthType.digest, + digest: digestAuth, + ); + + const httpRequestModel = HttpRequestModel( + method: HTTPVerb.delete, + url: 'https://api.apidash.dev/users/1', + authModel: authModel, + ); + + expect(httpRequestModel.authModel?.type, APIAuthType.digest); + expect(httpRequestModel.authModel?.digest?.username, 'digestuser'); + expect(httpRequestModel.authModel?.digest?.realm, 'test-realm'); + expect(httpRequestModel.authModel?.digest?.algorithm, 'MD5'); + }); + + test( + 'should serialize and deserialize HttpRequestModel with auth correctly', + () { + const basicAuth = AuthBasicAuthModel( + username: 'testuser', + password: 'testpass', + ); + const authModel = AuthModel( + type: APIAuthType.basic, + basic: basicAuth, + ); + + const originalModel = HttpRequestModel( + method: HTTPVerb.get, + url: 'https://api.apidash.dev/users', + authModel: authModel, + ); + + final json = originalModel.toJson(); + final deserializedModel = HttpRequestModel.fromJson(json); + + expect(deserializedModel.method, originalModel.method); + expect(deserializedModel.url, originalModel.url); + expect(deserializedModel.authModel?.type, originalModel.authModel?.type); + expect(deserializedModel.authModel?.basic?.username, + originalModel.authModel?.basic?.username); + expect(deserializedModel.authModel?.basic?.password, + originalModel.authModel?.basic?.password); + }); + + test('should handle copyWith for HttpRequestModel with auth', () { + const originalAuth = AuthBasicAuthModel( + username: 'testuser', + password: 'testpass', + ); + const originalAuthModel = AuthModel( + type: APIAuthType.basic, + basic: originalAuth, + ); + + const originalModel = HttpRequestModel( + method: HTTPVerb.get, + url: 'https://api.apidash.dev/users', + authModel: originalAuthModel, + ); + + const newAuth = AuthBearerModel(token: 'new-bearer-token'); + const newAuthModel = AuthModel( + type: APIAuthType.bearer, + bearer: newAuth, + ); + + final copiedModel = originalModel.copyWith( + authModel: newAuthModel, + ); + + expect(copiedModel.method, originalModel.method); + expect(copiedModel.url, originalModel.url); + expect(copiedModel.authModel?.type, APIAuthType.bearer); + expect(copiedModel.authModel?.bearer?.token, 'new-bearer-token'); + }); + + test('should handle HttpRequestModel with complex auth scenarios', () { + const complexPayload = ''' + { + "sub": "1234567890", + "name": "John Doe", + "iat": 1516239022, + "exp": 1516242622, + "roles": ["admin", "user"], + "permissions": { + "read": true, + "write": false + } + } + '''; + + const jwtAuth = AuthJwtModel( + secret: 'complex-secret', + privateKey: 'private-key-content', + payload: complexPayload, + addTokenTo: 'query', + algorithm: 'RS256', + isSecretBase64Encoded: true, + headerPrefix: 'JWT', + queryParamKey: 'jwt_token', + header: 'X-JWT-Token', + ); + const authModel = AuthModel( + type: APIAuthType.jwt, + jwt: jwtAuth, + ); + + const httpRequestModel = HttpRequestModel( + method: HTTPVerb.post, + url: 'https://api.apidash.dev/secure-endpoint', + authModel: authModel, + headers: [ + NameValueModel(name: 'Content-Type', value: 'application/json'), + NameValueModel(name: 'Accept', value: 'application/json'), + ], + ); + + expect(httpRequestModel.authModel?.jwt?.payload, complexPayload); + expect( + httpRequestModel.authModel?.jwt?.privateKey, 'private-key-content'); + expect(httpRequestModel.authModel?.jwt?.algorithm, 'RS256'); + expect(httpRequestModel.authModel?.jwt?.isSecretBase64Encoded, true); + expect(httpRequestModel.authModel?.jwt?.addTokenTo, 'query'); + }); + + test('should handle HttpRequestModel with auth and other fields', () { + const apiKeyAuth = AuthApiKeyModel( + key: 'api-key-123', + location: 'header', + name: 'X-API-Key', + ); + const authModel = AuthModel( + type: APIAuthType.apiKey, + apikey: apiKeyAuth, + ); + + const httpRequestModel = HttpRequestModel( + method: HTTPVerb.post, + url: 'https://api.apidash.dev/users', + authModel: authModel, + headers: [ + NameValueModel(name: 'Content-Type', value: 'application/json'), + NameValueModel(name: 'Accept', value: 'application/json'), + ], + params: [ + NameValueModel(name: 'limit', value: '10'), + NameValueModel(name: 'offset', value: '0'), + ], + body: '{"name": "John Doe", "email": "john@example.com"}', + bodyContentType: ContentType.json, + ); + + expect(httpRequestModel.authModel?.type, APIAuthType.apiKey); + expect(httpRequestModel.authModel?.apikey?.key, 'api-key-123'); + expect(httpRequestModel.headers?.length, 2); + expect(httpRequestModel.params?.length, 2); + expect(httpRequestModel.body, + '{"name": "John Doe", "email": "john@example.com"}'); + expect(httpRequestModel.bodyContentType, ContentType.json); + }); + + test('should handle HttpRequestModel with multiple auth types in sequence', + () { + const originalAuth = AuthBasicAuthModel( + username: 'testuser', + password: 'testpass', + ); + const originalAuthModel = AuthModel( + type: APIAuthType.basic, + basic: originalAuth, + ); + + var httpRequestModel = HttpRequestModel( + method: HTTPVerb.get, + url: 'https://api.apidash.dev/users', + authModel: originalAuthModel, + ); + + expect(httpRequestModel.authModel?.type, APIAuthType.basic); + + // Change to bearer + const bearerAuth = AuthBearerModel(token: 'bearer-token'); + const bearerAuthModel = AuthModel( + type: APIAuthType.bearer, + bearer: bearerAuth, + ); + + httpRequestModel = httpRequestModel.copyWith(authModel: bearerAuthModel); + expect(httpRequestModel.authModel?.type, APIAuthType.bearer); + + // Change to API key + const apiKeyAuth = AuthApiKeyModel( + key: 'api-key', + location: 'query', + name: 'key', + ); + const apiKeyAuthModel = AuthModel( + type: APIAuthType.apiKey, + apikey: apiKeyAuth, + ); + + httpRequestModel = httpRequestModel.copyWith(authModel: apiKeyAuthModel); + expect(httpRequestModel.authModel?.type, APIAuthType.apiKey); + expect(httpRequestModel.authModel?.apikey?.location, 'query'); + expect(httpRequestModel.authModel?.apikey?.name, 'key'); + }); + }); } diff --git a/test/models/http_request_models.dart b/test/models/http_request_models.dart index 10b84edaf..f5980fbe6 100644 --- a/test/models/http_request_models.dart +++ b/test/models/http_request_models.dart @@ -384,6 +384,14 @@ const httpRequestModelGet4Json = { {'name': 'add_space', 'value': 'true'}, {'name': 'trailing_zeros', 'value': 'true'} ], + 'authModel': { + 'type': 'none', + 'apikey': null, + 'bearer': null, + 'basic': null, + 'jwt': null, + 'digest': null + }, "isHeaderEnabledList": null, "isParamEnabledList": null, "bodyContentType": "json", @@ -403,6 +411,14 @@ const httpRequestModelPost10Json = { {'name': 'size', 'value': '2'}, {'name': 'len', 'value': '3'} ], + 'authModel': { + 'type': 'none', + 'apikey': null, + 'bearer': null, + 'basic': null, + 'jwt': null, + 'digest': null + }, 'isHeaderEnabledList': [false, true], 'isParamEnabledList': null, "bodyContentType": 'json', diff --git a/test/models/response_model_test.dart b/test/models/response_model_test.dart index 74872cdb4..56ba1e2ab 100644 --- a/test/models/response_model_test.dart +++ b/test/models/response_model_test.dart @@ -17,6 +17,7 @@ void main() { var responseRec = await sendHttpRequest( requestModelGet1.id, requestModelGet1.apiType, + AuthModel(type: APIAuthType.none), requestModelGet1.httpRequestModel!, defaultUriScheme: kDefaultUriScheme, noSSL: false, @@ -35,6 +36,7 @@ void main() { var responseRec = await sendHttpRequest( requestModelGet13.id, requestModelGet13.apiType, + AuthModel(type: APIAuthType.none), requestModelGet13.httpRequestModel!, defaultUriScheme: kDefaultUriScheme, noSSL: false, @@ -52,6 +54,7 @@ void main() { var responseRec = await sendHttpRequest( requestModelPost11.id, requestModelPost11.apiType, + AuthModel(type: APIAuthType.none), requestModelPost11.httpRequestModel!, ); @@ -66,6 +69,7 @@ void main() { var responseRec = await sendHttpRequest( requestModelPost12.id, requestModelPost12.apiType, + AuthModel(type: APIAuthType.none), requestModelPost12.httpRequestModel!, ); @@ -79,6 +83,7 @@ void main() { var responseRec = await sendHttpRequest( requestModelPost13.id, requestModelPost13.apiType, + AuthModel(type: APIAuthType.none), requestModelPost13.httpRequestModel!, ); @@ -92,6 +97,7 @@ void main() { var responseRec = await sendHttpRequest( requestModelGetBadSSL.id, requestModelGetBadSSL.apiType, + AuthModel(type: APIAuthType.none), requestModelGetBadSSL.httpRequestModel!, defaultUriScheme: kDefaultUriScheme, noSSL: false, @@ -104,6 +110,7 @@ void main() { var responseRec = await sendHttpRequest( requestModelGetBadSSL.id, requestModelGetBadSSL.apiType, + AuthModel(type: APIAuthType.none), requestModelGetBadSSL.httpRequestModel!, defaultUriScheme: kDefaultUriScheme, noSSL: true, @@ -124,6 +131,7 @@ void main() { var responseRec = await sendHttpRequest( requestModelOptions1.id, requestModelOptions1.apiType, + AuthModel(type: APIAuthType.none), requestModelOptions1.httpRequestModel!, defaultUriScheme: kDefaultUriScheme, noSSL: false, @@ -131,9 +139,14 @@ void main() { final responseData = responseModel.fromResponse(response: responseRec.$1!); expect(responseData.statusCode, 200); - expect(responseData.headers?['access-control-allow-methods'], 'GET,POST,PUT,PATCH,DELETE,HEAD,OPTIONS'); - expect(responseData.headers?['access-control-allow-methods']?.contains("OPTIONS"), true); - expect(responseData.headers?['allow'], 'GET,POST,PUT,PATCH,DELETE,HEAD,OPTIONS'); + expect(responseData.headers?['access-control-allow-methods'], + 'GET,POST,PUT,PATCH,DELETE,HEAD,OPTIONS'); + expect( + responseData.headers?['access-control-allow-methods'] + ?.contains("OPTIONS"), + true); + expect(responseData.headers?['allow'], + 'GET,POST,PUT,PATCH,DELETE,HEAD,OPTIONS'); expect(responseData.headers?['allow']?.contains("OPTIONS"), true); }); } diff --git a/test/providers/collection_providers_test.dart b/test/providers/collection_providers_test.dart index 097b71f0b..3bf59f83d 100644 --- a/test/providers/collection_providers_test.dart +++ b/test/providers/collection_providers_test.dart @@ -51,4 +51,580 @@ void main() async { // Verify that the Snackbar is shown expect(find.text('Switched to POST method'), findsOneWidget); }, skip: true); + + group('CollectionStateNotifier Auth Tests', () { + late ProviderContainer container; + late CollectionStateNotifier notifier; + + setUp(() { + container = createContainer(); + notifier = container.read(collectionStateNotifierProvider.notifier); + }); + + test('should update request with basic authentication', () { + final id = notifier.state!.entries.first.key; + const basicAuth = AuthBasicAuthModel( + username: 'testuser', + password: 'testpass', + ); + const authModel = AuthModel( + type: APIAuthType.basic, + basic: basicAuth, + ); + + notifier.update(id: id, authModel: authModel); + + final updatedRequest = notifier.getRequestModel(id); + expect( + updatedRequest?.httpRequestModel?.authModel?.type, APIAuthType.basic); + expect(updatedRequest?.httpRequestModel?.authModel?.basic?.username, + 'testuser'); + expect(updatedRequest?.httpRequestModel?.authModel?.basic?.password, + 'testpass'); + }); + + test('should update request with bearer authentication', () { + final id = notifier.state!.entries.first.key; + const bearerAuth = AuthBearerModel(token: 'bearer-token-123'); + const authModel = AuthModel( + type: APIAuthType.bearer, + bearer: bearerAuth, + ); + + notifier.update(id: id, authModel: authModel); + + final updatedRequest = notifier.getRequestModel(id); + expect(updatedRequest?.httpRequestModel?.authModel?.type, + APIAuthType.bearer); + expect(updatedRequest?.httpRequestModel?.authModel?.bearer?.token, + 'bearer-token-123'); + }); + + test('should update request with API key authentication', () { + final id = notifier.state!.entries.first.key; + const apiKeyAuth = AuthApiKeyModel( + key: 'api-key-123', + location: 'header', + name: 'X-API-Key', + ); + const authModel = AuthModel( + type: APIAuthType.apiKey, + apikey: apiKeyAuth, + ); + + notifier.update(id: id, authModel: authModel); + + final updatedRequest = notifier.getRequestModel(id); + expect(updatedRequest?.httpRequestModel?.authModel?.type, + APIAuthType.apiKey); + expect(updatedRequest?.httpRequestModel?.authModel?.apikey?.key, + 'api-key-123'); + expect(updatedRequest?.httpRequestModel?.authModel?.apikey?.location, + 'header'); + expect(updatedRequest?.httpRequestModel?.authModel?.apikey?.name, + 'X-API-Key'); + }); + + test('should update request with JWT authentication', () { + final id = notifier.state!.entries.first.key; + const jwtAuth = AuthJwtModel( + secret: 'jwt-secret', + payload: '{"sub": "1234567890"}', + addTokenTo: 'header', + algorithm: 'HS256', + isSecretBase64Encoded: false, + headerPrefix: 'Bearer', + queryParamKey: 'token', + header: 'Authorization', + ); + const authModel = AuthModel( + type: APIAuthType.jwt, + jwt: jwtAuth, + ); + + notifier.update(id: id, authModel: authModel); + + final updatedRequest = notifier.getRequestModel(id); + expect( + updatedRequest?.httpRequestModel?.authModel?.type, APIAuthType.jwt); + expect(updatedRequest?.httpRequestModel?.authModel?.jwt?.secret, + 'jwt-secret'); + expect( + updatedRequest?.httpRequestModel?.authModel?.jwt?.algorithm, 'HS256'); + expect( + updatedRequest + ?.httpRequestModel?.authModel?.jwt?.isSecretBase64Encoded, + false); + }); + + test('should update request with digest authentication', () { + final id = notifier.state!.entries.first.key; + const digestAuth = AuthDigestModel( + username: 'digestuser', + password: 'digestpass', + realm: 'test-realm', + nonce: 'test-nonce', + algorithm: 'MD5', + qop: 'auth', + opaque: 'test-opaque', + ); + const authModel = AuthModel( + type: APIAuthType.digest, + digest: digestAuth, + ); + + notifier.update(id: id, authModel: authModel); + + final updatedRequest = notifier.getRequestModel(id); + expect(updatedRequest?.httpRequestModel?.authModel?.type, + APIAuthType.digest); + expect(updatedRequest?.httpRequestModel?.authModel?.digest?.username, + 'digestuser'); + expect(updatedRequest?.httpRequestModel?.authModel?.digest?.realm, + 'test-realm'); + expect(updatedRequest?.httpRequestModel?.authModel?.digest?.algorithm, + 'MD5'); + }); + + test('should remove authentication when set to none', () { + final id = notifier.state!.entries.first.key; + + // First add auth + const basicAuth = AuthBasicAuthModel( + username: 'testuser', + password: 'testpass', + ); + const authModel = AuthModel( + type: APIAuthType.basic, + basic: basicAuth, + ); + notifier.update(id: id, authModel: authModel); + + // Then remove auth + const noAuthModel = AuthModel(type: APIAuthType.none); + notifier.update(id: id, authModel: noAuthModel); + + final updatedRequest = notifier.getRequestModel(id); + expect( + updatedRequest?.httpRequestModel?.authModel?.type, APIAuthType.none); + expect(updatedRequest?.httpRequestModel?.authModel?.basic, isNull); + }); + + test('should preserve auth when duplicating request', () { + final id = notifier.state!.entries.first.key; + const basicAuth = AuthBasicAuthModel( + username: 'testuser', + password: 'testpass', + ); + const authModel = AuthModel( + type: APIAuthType.basic, + basic: basicAuth, + ); + + notifier.update(id: id, authModel: authModel); + notifier.duplicate(id: id); + + final sequence = container.read(requestSequenceProvider); + final duplicatedId = sequence.firstWhere((element) => element != id); + final duplicatedRequest = notifier.getRequestModel(duplicatedId); + + expect(duplicatedRequest?.httpRequestModel?.authModel?.type, + APIAuthType.basic); + expect(duplicatedRequest?.httpRequestModel?.authModel?.basic?.username, + 'testuser'); + expect(duplicatedRequest?.httpRequestModel?.authModel?.basic?.password, + 'testpass'); + }); + + test('should not clear auth when clearing response', () { + final id = notifier.state!.entries.first.key; + const bearerAuth = AuthBearerModel(token: 'bearer-token-123'); + const authModel = AuthModel( + type: APIAuthType.bearer, + bearer: bearerAuth, + ); + + notifier.update(id: id, authModel: authModel); + notifier.clearResponse(id: id); + + final updatedRequest = notifier.getRequestModel(id); + // Auth should be preserved when clearing response + expect(updatedRequest?.httpRequestModel?.authModel?.type, + APIAuthType.bearer); + expect(updatedRequest?.httpRequestModel?.authModel?.bearer?.token, + 'bearer-token-123'); + }); + + test('should handle auth with special characters', () { + final id = notifier.state!.entries.first.key; + const basicAuth = AuthBasicAuthModel( + username: 'user@domain.com', + password: r'P@ssw0rd!@#$%^&*()', + ); + const authModel = AuthModel( + type: APIAuthType.basic, + basic: basicAuth, + ); + + notifier.update(id: id, authModel: authModel); + + final updatedRequest = notifier.getRequestModel(id); + expect(updatedRequest?.httpRequestModel?.authModel?.basic?.username, + 'user@domain.com'); + expect(updatedRequest?.httpRequestModel?.authModel?.basic?.password, + r'P@ssw0rd!@#$%^&*()'); + }); + + test('should handle multiple auth type changes', () { + final id = notifier.state!.entries.first.key; + + // Start with basic auth + const basicAuth = AuthBasicAuthModel( + username: 'testuser', + password: 'testpass', + ); + const basicAuthModel = AuthModel( + type: APIAuthType.basic, + basic: basicAuth, + ); + notifier.update(id: id, authModel: basicAuthModel); + + // Switch to bearer + const bearerAuth = AuthBearerModel(token: 'bearer-token-123'); + const bearerAuthModel = AuthModel( + type: APIAuthType.bearer, + bearer: bearerAuth, + ); + notifier.update(id: id, authModel: bearerAuthModel); + + // Switch to API key + const apiKeyAuth = AuthApiKeyModel( + key: 'api-key-123', + location: 'query', + name: 'apikey', + ); + const apiKeyAuthModel = AuthModel( + type: APIAuthType.apiKey, + apikey: apiKeyAuth, + ); + notifier.update(id: id, authModel: apiKeyAuthModel); + + final updatedRequest = notifier.getRequestModel(id); + expect(updatedRequest?.httpRequestModel?.authModel?.type, + APIAuthType.apiKey); + expect(updatedRequest?.httpRequestModel?.authModel?.apikey?.key, + 'api-key-123'); + expect(updatedRequest?.httpRequestModel?.authModel?.apikey?.location, + 'query'); + expect( + updatedRequest?.httpRequestModel?.authModel?.apikey?.name, 'apikey'); + }); + + test('should handle empty auth values', () { + final id = notifier.state!.entries.first.key; + const basicAuth = AuthBasicAuthModel( + username: '', + password: '', + ); + const authModel = AuthModel( + type: APIAuthType.basic, + basic: basicAuth, + ); + + notifier.update(id: id, authModel: authModel); + + final updatedRequest = notifier.getRequestModel(id); + expect( + updatedRequest?.httpRequestModel?.authModel?.type, APIAuthType.basic); + expect(updatedRequest?.httpRequestModel?.authModel?.basic?.username, ''); + expect(updatedRequest?.httpRequestModel?.authModel?.basic?.password, ''); + }); + + test('should save and load auth data correctly', () async { + final notifier = container.read(collectionStateNotifierProvider.notifier); + + final id = notifier.state!.entries.first.key; + const jwtAuth = AuthJwtModel( + secret: 'jwt-secret', + payload: '{"sub": "1234567890"}', + addTokenTo: 'header', + algorithm: 'HS256', + isSecretBase64Encoded: false, + headerPrefix: 'Bearer', + queryParamKey: 'token', + header: 'Authorization', + ); + const authModel = AuthModel( + type: APIAuthType.jwt, + jwt: jwtAuth, + ); + + notifier.update(id: id, authModel: authModel); + await notifier.saveData(); + + // Create new container and load data + late ProviderContainer newContainer; + try { + newContainer = ProviderContainer(); + + // Wait for the container to initialize by accessing the provider + final newNotifier = + newContainer.read(collectionStateNotifierProvider.notifier); + + // Give some time for the microtask in the constructor to complete + await Future.delayed(const Duration(milliseconds: 10)); + + final loadedRequest = newNotifier.getRequestModel(id); + + expect( + loadedRequest?.httpRequestModel?.authModel?.type, APIAuthType.jwt); + expect(loadedRequest?.httpRequestModel?.authModel?.jwt?.secret, + 'jwt-secret'); + expect(loadedRequest?.httpRequestModel?.authModel?.jwt?.algorithm, + 'HS256'); + } finally { + newContainer.dispose(); + } + }); + + test('should handle auth in addRequestModel', () { + const basicAuth = AuthBasicAuthModel( + username: 'testuser', + password: 'testpass', + ); + const authModel = AuthModel( + type: APIAuthType.basic, + basic: basicAuth, + ); + + final httpRequestModel = HttpRequestModel( + method: HTTPVerb.get, + url: 'https://api.example.com/users', + authModel: authModel, + ); + + notifier.addRequestModel(httpRequestModel, name: 'Test Request'); + + final sequence = container.read(requestSequenceProvider); + final addedRequest = notifier.getRequestModel(sequence.first); + + expect( + addedRequest?.httpRequestModel?.authModel?.type, APIAuthType.basic); + expect(addedRequest?.httpRequestModel?.authModel?.basic?.username, + 'testuser'); + expect(addedRequest?.httpRequestModel?.authModel?.basic?.password, + 'testpass'); + }); + + test('should handle complex JWT configuration', () { + final id = notifier.state!.entries.first.key; + const complexPayload = ''' + { + "sub": "1234567890", + "name": "John Doe", + "iat": 1516239022, + "exp": 1516242622, + "roles": ["admin", "user"], + "permissions": { + "read": true, + "write": false + } + } + '''; + + const jwtAuth = AuthJwtModel( + secret: 'complex-secret', + privateKey: 'private-key-content', + payload: complexPayload, + addTokenTo: 'query', + algorithm: 'RS256', + isSecretBase64Encoded: true, + headerPrefix: 'JWT', + queryParamKey: 'jwt_token', + header: 'X-JWT-Token', + ); + const authModel = AuthModel( + type: APIAuthType.jwt, + jwt: jwtAuth, + ); + + notifier.update(id: id, authModel: authModel); + + final updatedRequest = notifier.getRequestModel(id); + expect( + updatedRequest?.httpRequestModel?.authModel?.type, APIAuthType.jwt); + expect(updatedRequest?.httpRequestModel?.authModel?.jwt?.payload, + complexPayload); + expect(updatedRequest?.httpRequestModel?.authModel?.jwt?.privateKey, + 'private-key-content'); + expect( + updatedRequest?.httpRequestModel?.authModel?.jwt?.algorithm, 'RS256'); + expect( + updatedRequest + ?.httpRequestModel?.authModel?.jwt?.isSecretBase64Encoded, + true); + expect(updatedRequest?.httpRequestModel?.authModel?.jwt?.addTokenTo, + 'query'); + }); + + test('should handle API key in different locations', () { + final id = notifier.state!.entries.first.key; + + // Test header location + const headerApiKey = AuthApiKeyModel( + key: 'header-key', + location: 'header', + name: 'X-API-Key', + ); + const headerAuthModel = AuthModel( + type: APIAuthType.apiKey, + apikey: headerApiKey, + ); + notifier.update(id: id, authModel: headerAuthModel); + + var updatedRequest = notifier.getRequestModel(id); + expect(updatedRequest?.httpRequestModel?.authModel?.apikey?.location, + 'header'); + expect(updatedRequest?.httpRequestModel?.authModel?.apikey?.name, + 'X-API-Key'); + + // Test query location + const queryApiKey = AuthApiKeyModel( + key: 'query-key', + location: 'query', + name: 'apikey', + ); + const queryAuthModel = AuthModel( + type: APIAuthType.apiKey, + apikey: queryApiKey, + ); + notifier.update(id: id, authModel: queryAuthModel); + + updatedRequest = notifier.getRequestModel(id); + expect(updatedRequest?.httpRequestModel?.authModel?.apikey?.location, + 'query'); + expect( + updatedRequest?.httpRequestModel?.authModel?.apikey?.name, 'apikey'); + }); + + test('should handle digest auth with different algorithms', () { + final id = notifier.state!.entries.first.key; + + // Test MD5 algorithm + const md5DigestAuth = AuthDigestModel( + username: 'digestuser', + password: 'digestpass', + realm: 'test-realm', + nonce: 'test-nonce', + algorithm: 'MD5', + qop: 'auth', + opaque: 'test-opaque', + ); + const md5AuthModel = AuthModel( + type: APIAuthType.digest, + digest: md5DigestAuth, + ); + notifier.update(id: id, authModel: md5AuthModel); + + var updatedRequest = notifier.getRequestModel(id); + expect(updatedRequest?.httpRequestModel?.authModel?.digest?.algorithm, + 'MD5'); + + // Test SHA-256 algorithm + const sha256DigestAuth = AuthDigestModel( + username: 'digestuser', + password: 'digestpass', + realm: 'test-realm', + nonce: 'test-nonce', + algorithm: 'SHA-256', + qop: 'auth-int', + opaque: 'test-opaque', + ); + const sha256AuthModel = AuthModel( + type: APIAuthType.digest, + digest: sha256DigestAuth, + ); + notifier.update(id: id, authModel: sha256AuthModel); + + updatedRequest = notifier.getRequestModel(id); + expect(updatedRequest?.httpRequestModel?.authModel?.digest?.algorithm, + 'SHA-256'); + expect( + updatedRequest?.httpRequestModel?.authModel?.digest?.qop, 'auth-int'); + }); + + test('should handle auth model copyWith functionality', () { + final id = notifier.state!.entries.first.key; + const originalAuth = AuthBasicAuthModel( + username: 'original', + password: 'original', + ); + const originalAuthModel = AuthModel( + type: APIAuthType.basic, + basic: originalAuth, + ); + + notifier.update(id: id, authModel: originalAuthModel); + + // Update with copyWith + const updatedAuth = AuthBasicAuthModel( + username: 'updated', + password: 'updated', + ); + final updatedAuthModel = originalAuthModel.copyWith( + basic: updatedAuth, + ); + + notifier.update(id: id, authModel: updatedAuthModel); + + final updatedRequest = notifier.getRequestModel(id); + expect(updatedRequest?.httpRequestModel?.authModel?.basic?.username, + 'updated'); + expect(updatedRequest?.httpRequestModel?.authModel?.basic?.password, + 'updated'); + }); + + test('should handle auth with very long tokens', () { + final id = notifier.state!.entries.first.key; + final longToken = 'a' * 5000; // Very long token + + final bearerAuth = AuthBearerModel(token: longToken); + final authModel = AuthModel( + type: APIAuthType.bearer, + bearer: bearerAuth, + ); + + notifier.update(id: id, authModel: authModel); + + final updatedRequest = notifier.getRequestModel(id); + expect(updatedRequest?.httpRequestModel?.authModel?.bearer?.token, + longToken); + expect(updatedRequest?.httpRequestModel?.authModel?.bearer?.token.length, + 5000); + }); + + test('should handle auth with Unicode characters', () { + final id = notifier.state!.entries.first.key; + const basicAuth = AuthBasicAuthModel( + username: 'user_测试_тест_テスト', + password: 'password_🔑_🚀_💻', + ); + const authModel = AuthModel( + type: APIAuthType.basic, + basic: basicAuth, + ); + + notifier.update(id: id, authModel: authModel); + + final updatedRequest = notifier.getRequestModel(id); + expect(updatedRequest?.httpRequestModel?.authModel?.basic?.username, + 'user_测试_тест_テスト'); + expect(updatedRequest?.httpRequestModel?.authModel?.basic?.password, + 'password_🔑_🚀_💻'); + }); + + tearDown(() { + container.dispose(); + }); + }); } diff --git a/test/screens/common_widgets/auth/api_key_auth_fields_test.dart b/test/screens/common_widgets/auth/api_key_auth_fields_test.dart new file mode 100644 index 000000000..1e60cbd60 --- /dev/null +++ b/test/screens/common_widgets/auth/api_key_auth_fields_test.dart @@ -0,0 +1,294 @@ +import 'package:apidash/screens/common_widgets/auth/api_key_auth_fields.dart'; +import 'package:apidash/widgets/widgets.dart'; +import 'package:apidash_core/apidash_core.dart'; +import 'package:apidash_design_system/apidash_design_system.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + group('ApiKeyAuthFields Widget Tests', () { + late AuthModel? mockAuthData; + late Function(AuthModel?) mockUpdateAuth; + late List capturedAuthUpdates; + + setUp(() { + capturedAuthUpdates = []; + mockUpdateAuth = (AuthModel? authModel) { + capturedAuthUpdates.add(authModel); + }; + }); + + testWidgets('renders with default values when authData is null', + (WidgetTester tester) async { + mockAuthData = null; + + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: ApiKeyAuthFields( + authData: mockAuthData, + updateAuth: mockUpdateAuth, + ), + ), + ), + ); + + expect(find.text('Add to'), findsOneWidget); + expect(find.byType(ADPopupMenu), findsOneWidget); + expect(find.byType(AuthTextField), findsNWidgets(2)); + expect(find.text('Header'), findsOneWidget); + }); + + testWidgets( + 'updates auth data when authData is null and API key value is changed', + (WidgetTester tester) async { + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: ApiKeyAuthFields( + authData: null, + updateAuth: mockUpdateAuth, + ), + ), + ), + ); + + // Find the key field (second AuthTextField) + final keyField = find.byType(AuthTextField).last; + await tester.tap(keyField); + await tester.enterText(keyField, 'new-api-key'); + await tester.pumpAndSettle(); + + // Verify that updateAuth was called + expect(capturedAuthUpdates.length, greaterThan(0)); + final lastUpdate = capturedAuthUpdates.last; + expect(lastUpdate?.apikey?.key, 'new-api-key'); + }); + + testWidgets('renders with existing API key auth data', + (WidgetTester tester) async { + mockAuthData = const AuthModel( + type: APIAuthType.apiKey, + apikey: AuthApiKeyModel( + key: 'test-api-key', + name: 'X-API-Key', + location: 'header', + ), + ); + + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: ApiKeyAuthFields( + authData: mockAuthData, + updateAuth: mockUpdateAuth, + ), + ), + ), + ); + + expect(find.text('Add to'), findsOneWidget); + expect(find.text('Header'), findsOneWidget); + expect(find.byType(AuthTextField), findsNWidgets(2)); + }); + + testWidgets('renders with query params location', + (WidgetTester tester) async { + mockAuthData = const AuthModel( + type: APIAuthType.apiKey, + apikey: AuthApiKeyModel( + key: 'test-api-key', + name: 'api_key', + location: 'query', + ), + ); + + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: ApiKeyAuthFields( + authData: mockAuthData, + updateAuth: mockUpdateAuth, + ), + ), + ), + ); + + expect(find.text('Add to'), findsOneWidget); + expect(find.text('Query Params'), findsOneWidget); + }); + + testWidgets('updates auth data when location dropdown changes', + (WidgetTester tester) async { + mockAuthData = const AuthModel( + type: APIAuthType.apiKey, + apikey: AuthApiKeyModel( + key: 'test-key', + name: 'X-API-Key', + location: 'header', + ), + ); + + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: ApiKeyAuthFields( + authData: mockAuthData, + updateAuth: mockUpdateAuth, + ), + ), + ), + ); + + // Find and tap the dropdown + await tester.tap(find.byType(ADPopupMenu)); + await tester.pumpAndSettle(); + + // Select Query Params option + await tester.tap(find.text('Query Params').last); + await tester.pumpAndSettle(); + + // Verify that updateAuth was called + expect(capturedAuthUpdates.length, greaterThan(0)); + final lastUpdate = capturedAuthUpdates.last; + expect(lastUpdate?.apikey?.location, 'query'); + }); + + testWidgets('updates auth data when API key name changes', + (WidgetTester tester) async { + mockAuthData = const AuthModel( + type: APIAuthType.apiKey, + apikey: AuthApiKeyModel( + key: 'test-key', + name: 'X-API-Key', + location: 'header', + ), + ); + + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: ApiKeyAuthFields( + authData: mockAuthData, + updateAuth: mockUpdateAuth, + ), + ), + ), + ); + + // Find the name field (first AuthTextField) + final nameField = find.byType(AuthTextField).first; + await tester.tap(nameField); + await tester.enterText(nameField, 'Authorization'); + await tester.pumpAndSettle(); + + // Verify that updateAuth was called + expect(capturedAuthUpdates.length, greaterThan(0)); + final lastUpdate = capturedAuthUpdates.last; + expect(lastUpdate?.apikey?.name, 'Authorization'); + }); + + testWidgets('updates auth data when API key value changes', + (WidgetTester tester) async { + mockAuthData = const AuthModel( + type: APIAuthType.apiKey, + apikey: AuthApiKeyModel( + key: 'old-key', + name: 'X-API-Key', + location: 'header', + ), + ); + + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: ApiKeyAuthFields( + authData: mockAuthData, + updateAuth: mockUpdateAuth, + ), + ), + ), + ); + + // Find the key field (second AuthTextField) + final keyField = find.byType(AuthTextField).last; + await tester.tap(keyField); + await tester.enterText(keyField, 'new-api-key'); + await tester.pumpAndSettle(); + + // Verify that updateAuth was called + expect(capturedAuthUpdates.length, greaterThan(0)); + final lastUpdate = capturedAuthUpdates.last; + expect(lastUpdate?.apikey?.key, 'new-api-key'); + }); + + testWidgets('respects readOnly property', (WidgetTester tester) async { + mockAuthData = const AuthModel( + type: APIAuthType.apiKey, + apikey: AuthApiKeyModel( + key: 'test-key', + name: 'X-API-Key', + location: 'header', + ), + ); + + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: ApiKeyAuthFields( + authData: mockAuthData, + updateAuth: mockUpdateAuth, + readOnly: true, + ), + ), + ), + ); + + // Verify that AuthTextField widgets are rendered + expect(find.byType(AuthTextField), findsNWidgets(2)); + + // The readOnly property should be passed to AuthTextField widgets + // This is verified by the widget structure itself + }); + + testWidgets('displays correct hint texts', (WidgetTester tester) async { + mockAuthData = null; + + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: ApiKeyAuthFields( + authData: mockAuthData, + updateAuth: mockUpdateAuth, + ), + ), + ), + ); + + expect(find.text('Add to'), findsOneWidget); + // Check for the existence of the auth text fields + expect(find.byType(AuthTextField), findsNWidgets(2)); + }); + testWidgets('initializes with correct default values', + (WidgetTester tester) async { + mockAuthData = null; + + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: ApiKeyAuthFields( + authData: mockAuthData, + updateAuth: mockUpdateAuth, + ), + ), + ), + ); + + // Default location should be header + expect(find.text('Header'), findsOneWidget); + + // Default name should be 'x-api-key' in the text field + expect(find.text('x-api-key'), findsOneWidget); + }); + }); +} diff --git a/test/screens/common_widgets/auth/basic_auth_fields_test.dart b/test/screens/common_widgets/auth/basic_auth_fields_test.dart new file mode 100644 index 000000000..8a5d88142 --- /dev/null +++ b/test/screens/common_widgets/auth/basic_auth_fields_test.dart @@ -0,0 +1,231 @@ +import 'package:apidash/screens/common_widgets/auth/basic_auth_fields.dart'; +import 'package:apidash/widgets/widgets.dart'; +import 'package:apidash_core/apidash_core.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + group('BasicAuthFields Widget Tests', () { + late AuthModel? mockAuthData; + late Function(AuthModel?) mockUpdateAuth; + late List capturedAuthUpdates; + + setUp(() { + capturedAuthUpdates = []; + mockUpdateAuth = (AuthModel? authModel) { + capturedAuthUpdates.add(authModel); + }; + }); + + testWidgets('renders with default values when authData is null', + (WidgetTester tester) async { + mockAuthData = null; + + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: BasicAuthFields( + authData: mockAuthData, + updateAuth: mockUpdateAuth, + ), + ), + ), + ); + + expect(find.byType(AuthTextField), findsNWidgets(2)); + expect(find.text('Username'), findsNWidgets(2)); + expect(find.text('Password'), findsNWidgets(2)); + }); + + testWidgets('renders with existing basic auth data', + (WidgetTester tester) async { + mockAuthData = const AuthModel( + type: APIAuthType.basic, + basic: AuthBasicAuthModel( + username: 'testuser', + password: 'testpass', + ), + ); + + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: BasicAuthFields( + authData: mockAuthData, + updateAuth: mockUpdateAuth, + ), + ), + ), + ); + + expect(find.byType(AuthTextField), findsNWidgets(2)); + expect(find.text('Username'), findsExactly(2)); + expect(find.text('Password'), findsExactly(2)); + }); + + testWidgets('updates auth data when username changes', + (WidgetTester tester) async { + mockAuthData = const AuthModel( + type: APIAuthType.basic, + basic: AuthBasicAuthModel( + username: 'olduser', + password: 'password', + ), + ); + + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: BasicAuthFields( + authData: mockAuthData, + updateAuth: mockUpdateAuth, + ), + ), + ), + ); + + // Find the username field (first AuthTextField) + final usernameField = find.byType(AuthTextField).first; + await tester.tap(usernameField); + await tester.enterText(usernameField, 'newuser'); + await tester.pumpAndSettle(); + + // Verify that updateAuth was called + expect(capturedAuthUpdates.length, greaterThan(0)); + final lastUpdate = capturedAuthUpdates.last; + expect(lastUpdate?.basic?.username, 'newuser'); + expect(lastUpdate?.type, APIAuthType.basic); + }); + + testWidgets('updates auth data when password changes', + (WidgetTester tester) async { + mockAuthData = const AuthModel( + type: APIAuthType.basic, + basic: AuthBasicAuthModel( + username: 'user', + password: 'oldpass', + ), + ); + + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: BasicAuthFields( + authData: mockAuthData, + updateAuth: mockUpdateAuth, + ), + ), + ), + ); + + // Find the password field (second AuthTextField) + final passwordField = find.byType(AuthTextField).last; + await tester.tap(passwordField); + await tester.enterText(passwordField, 'newpass'); + await tester.pumpAndSettle(); + + // Verify that updateAuth was called + expect(capturedAuthUpdates.length, greaterThan(0)); + final lastUpdate = capturedAuthUpdates.last; + expect(lastUpdate?.basic?.password, 'newpass'); + expect(lastUpdate?.type, APIAuthType.basic); + }); + + testWidgets('respects readOnly property', (WidgetTester tester) async { + mockAuthData = const AuthModel( + type: APIAuthType.basic, + basic: AuthBasicAuthModel( + username: 'user', + password: 'pass', + ), + ); + + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: BasicAuthFields( + authData: mockAuthData, + updateAuth: mockUpdateAuth, + readOnly: true, + ), + ), + ), + ); + + // Verify that AuthTextField widgets are rendered + expect(find.byType(AuthTextField), findsNWidgets(2)); + + // The readOnly property should be passed to AuthTextField widgets + // This is verified by the widget structure itself + }); + + testWidgets('displays correct hint texts', (WidgetTester tester) async { + mockAuthData = null; + + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: BasicAuthFields( + authData: mockAuthData, + updateAuth: mockUpdateAuth, + ), + ), + ), + ); + + expect(find.byType(AuthTextField), findsNWidgets(2)); + }); + + testWidgets('handles empty auth data gracefully', + (WidgetTester tester) async { + mockAuthData = const AuthModel( + type: APIAuthType.basic, + basic: AuthBasicAuthModel( + username: '', + password: '', + ), + ); + + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: BasicAuthFields( + authData: mockAuthData, + updateAuth: mockUpdateAuth, + ), + ), + ), + ); + + expect(find.byType(AuthTextField), findsNWidgets(2)); + }); + + testWidgets('creates proper AuthModel on field changes', + (WidgetTester tester) async { + mockAuthData = null; + + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: BasicAuthFields( + authData: mockAuthData, + updateAuth: mockUpdateAuth, + ), + ), + ), + ); + + // Enter username + final usernameField = find.byType(AuthTextField).first; + await tester.tap(usernameField); + await tester.enterText(usernameField, 'testuser'); + await tester.pumpAndSettle(); + + // Verify that updateAuth was called with correct structure + expect(capturedAuthUpdates.length, greaterThan(0)); + final lastUpdate = capturedAuthUpdates.last; + expect(lastUpdate?.type, APIAuthType.basic); + expect(lastUpdate?.basic?.username, 'testuser'); + }); + }); +} diff --git a/test/screens/common_widgets/auth/bearer_auth_fields_test.dart b/test/screens/common_widgets/auth/bearer_auth_fields_test.dart new file mode 100644 index 000000000..c8dc13368 --- /dev/null +++ b/test/screens/common_widgets/auth/bearer_auth_fields_test.dart @@ -0,0 +1,237 @@ +import 'package:apidash/screens/common_widgets/auth/bearer_auth_fields.dart'; +import 'package:apidash/widgets/widgets.dart'; +import 'package:apidash_core/apidash_core.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + group('BearerAuthFields Widget Tests', () { + late AuthModel? mockAuthData; + late Function(AuthModel?) mockUpdateAuth; + late List capturedAuthUpdates; + + setUp(() { + capturedAuthUpdates = []; + mockUpdateAuth = (AuthModel? authModel) { + capturedAuthUpdates.add(authModel); + }; + }); + + testWidgets('renders with default values when authData is null', + (WidgetTester tester) async { + mockAuthData = null; + + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: BearerAuthFields( + authData: mockAuthData, + updateAuth: mockUpdateAuth, + ), + ), + ), + ); + + expect(find.byType(AuthTextField), findsOneWidget); + expect(find.text('Token'), findsNWidgets(2)); + }); + + testWidgets('renders with existing bearer auth data', + (WidgetTester tester) async { + mockAuthData = const AuthModel( + type: APIAuthType.bearer, + bearer: AuthBearerModel( + token: 'test-bearer-token', + ), + ); + + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: BearerAuthFields( + authData: mockAuthData, + updateAuth: mockUpdateAuth, + ), + ), + ), + ); + + expect(find.byType(AuthTextField), findsOneWidget); + expect(find.text('Token'), findsNWidgets(2)); + }); + + testWidgets('updates auth data when token changes', + (WidgetTester tester) async { + mockAuthData = const AuthModel( + type: APIAuthType.bearer, + bearer: AuthBearerModel( + token: 'old-token', + ), + ); + + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: BearerAuthFields( + authData: mockAuthData, + updateAuth: mockUpdateAuth, + ), + ), + ), + ); + + // Find the token field + final tokenField = find.byType(AuthTextField); + await tester.tap(tokenField); + await tester.enterText(tokenField, 'new-bearer-token'); + await tester.pumpAndSettle(); + + // Verify that updateAuth was called + expect(capturedAuthUpdates.length, greaterThan(0)); + final lastUpdate = capturedAuthUpdates.last; + expect(lastUpdate?.bearer?.token, 'new-bearer-token'); + expect(lastUpdate?.type, APIAuthType.bearer); + }); + + testWidgets('respects readOnly property', (WidgetTester tester) async { + mockAuthData = const AuthModel( + type: APIAuthType.bearer, + bearer: AuthBearerModel( + token: 'test-token', + ), + ); + + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: BearerAuthFields( + authData: mockAuthData, + updateAuth: mockUpdateAuth, + readOnly: true, + ), + ), + ), + ); + + // Verify that AuthTextField widget is rendered + expect(find.byType(AuthTextField), findsOneWidget); + + // The readOnly property should be passed to AuthTextField widget + // This is verified by the widget structure itself + }); + + testWidgets('displays correct hint text', (WidgetTester tester) async { + mockAuthData = null; + + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: BearerAuthFields( + authData: mockAuthData, + updateAuth: mockUpdateAuth, + ), + ), + ), + ); + + expect(find.byType(AuthTextField), findsOneWidget); + }); + + testWidgets('handles empty auth data gracefully', + (WidgetTester tester) async { + mockAuthData = const AuthModel( + type: APIAuthType.bearer, + bearer: AuthBearerModel( + token: '', + ), + ); + + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: BearerAuthFields( + authData: mockAuthData, + updateAuth: mockUpdateAuth, + ), + ), + ), + ); + + expect(find.byType(AuthTextField), findsOneWidget); + }); + + testWidgets('creates proper AuthModel on token change', + (WidgetTester tester) async { + mockAuthData = null; + + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: BearerAuthFields( + authData: mockAuthData, + updateAuth: mockUpdateAuth, + ), + ), + ), + ); + + // Enter token + final tokenField = find.byType(AuthTextField); + await tester.tap(tokenField); + await tester.enterText(tokenField, 'test-bearer-token'); + await tester.pumpAndSettle(); + + // Verify that updateAuth was called with correct structure + expect(capturedAuthUpdates.length, greaterThan(0)); + final lastUpdate = capturedAuthUpdates.last; + expect(lastUpdate?.type, APIAuthType.bearer); + expect(lastUpdate?.bearer?.token, 'test-bearer-token'); + }); + + testWidgets('initializes with correct default token value', + (WidgetTester tester) async { + mockAuthData = null; + + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: BearerAuthFields( + authData: mockAuthData, + updateAuth: mockUpdateAuth, + ), + ), + ), + ); + + // The token field should be empty initially + expect(find.byType(AuthTextField), findsOneWidget); + }); + + testWidgets('trims whitespace from token input', + (WidgetTester tester) async { + mockAuthData = null; + + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: BearerAuthFields( + authData: mockAuthData, + updateAuth: mockUpdateAuth, + ), + ), + ), + ); + + // Enter token with whitespace + final tokenField = find.byType(AuthTextField); + await tester.tap(tokenField); + await tester.enterText(tokenField, ' test-token '); + await tester.pumpAndSettle(); + + // Verify that updateAuth was called with trimmed token + expect(capturedAuthUpdates.length, greaterThan(0)); + final lastUpdate = capturedAuthUpdates.last; + expect(lastUpdate?.bearer?.token, 'test-token'); + }); + }); +} diff --git a/test/screens/common_widgets/auth/digest_auth_fields_test.dart b/test/screens/common_widgets/auth/digest_auth_fields_test.dart new file mode 100644 index 000000000..2eb26a04e --- /dev/null +++ b/test/screens/common_widgets/auth/digest_auth_fields_test.dart @@ -0,0 +1,405 @@ +import 'package:apidash/screens/common_widgets/auth/digest_auth_fields.dart'; +import 'package:apidash/widgets/widgets.dart'; +import 'package:apidash_core/apidash_core.dart'; +import 'package:apidash_design_system/widgets/widgets.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + group('DigestAuthFields Widget Tests', () { + late AuthModel? mockAuthData; + late Function(AuthModel?) mockUpdateAuth; + late List capturedAuthUpdates; + + setUp(() { + capturedAuthUpdates = []; + mockUpdateAuth = (AuthModel? authModel) { + capturedAuthUpdates.add(authModel); + }; + }); + + testWidgets('renders with default values when authData is null', + (WidgetTester tester) async { + mockAuthData = null; + + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: DigestAuthFields( + authData: mockAuthData, + updateAuth: mockUpdateAuth, + ), + ), + ), + ); + + expect(find.byType(AuthTextField), findsNWidgets(6)); + expect(find.byType(ADPopupMenu), findsOneWidget); + // Check for field labels (each AuthTextField creates a Text widget for label) + expect(find.text('Username'), findsNWidgets(2)); + expect(find.text('Password'), findsNWidgets(2)); + expect(find.text('Realm'), findsNWidgets(2)); + expect(find.text('Nonce'), findsNWidgets(2)); + expect(find.text('Algorithm'), findsOneWidget); + expect(find.text('QOP'), findsNWidgets(2)); + expect(find.text('Opaque'), findsNWidgets(2)); + }); + + testWidgets('renders with existing digest auth data', + (WidgetTester tester) async { + mockAuthData = const AuthModel( + type: APIAuthType.digest, + digest: AuthDigestModel( + username: 'testuser', + password: 'testpass', + realm: 'testrealm', + nonce: 'testnonce', + algorithm: 'MD5', + qop: 'auth', + opaque: 'testopaque', + ), + ); + + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: DigestAuthFields( + authData: mockAuthData, + updateAuth: mockUpdateAuth, + ), + ), + ), + ); + + expect(find.byType(AuthTextField), findsNWidgets(6)); + expect(find.byType(ADPopupMenu), findsOneWidget); + expect(find.text('MD5'), findsOneWidget); + }); + + testWidgets('updates auth data when username changes', + (WidgetTester tester) async { + mockAuthData = const AuthModel( + type: APIAuthType.digest, + digest: AuthDigestModel( + username: 'olduser', + password: 'pass', + realm: 'realm', + nonce: 'nonce', + algorithm: 'MD5', + qop: 'auth', + opaque: 'opaque', + ), + ); + + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: DigestAuthFields( + authData: mockAuthData, + updateAuth: mockUpdateAuth, + ), + ), + ), + ); + + // Find the username field (first AuthTextField) + final usernameField = find.byType(AuthTextField).first; + await tester.tap(usernameField); + await tester.enterText(usernameField, 'newuser'); + await tester.pumpAndSettle(); + + // Verify that updateAuth was called + expect(capturedAuthUpdates.length, greaterThan(0)); + final lastUpdate = capturedAuthUpdates.last; + expect(lastUpdate?.digest?.username, 'newuser'); + expect(lastUpdate?.type, APIAuthType.digest); + }); + + testWidgets('updates auth data when password changes', + (WidgetTester tester) async { + mockAuthData = const AuthModel( + type: APIAuthType.digest, + digest: AuthDigestModel( + username: 'user', + password: 'oldpass', + realm: 'realm', + nonce: 'nonce', + algorithm: 'MD5', + qop: 'auth', + opaque: 'opaque', + ), + ); + + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: DigestAuthFields( + authData: mockAuthData, + updateAuth: mockUpdateAuth, + ), + ), + ), + ); + + // Find the password field (second AuthTextField) + final passwordField = find.byType(AuthTextField).at(1); + await tester.tap(passwordField); + await tester.enterText(passwordField, 'newpass'); + await tester.pumpAndSettle(); + + // Verify that updateAuth was called + expect(capturedAuthUpdates.length, greaterThan(0)); + final lastUpdate = capturedAuthUpdates.last; + expect(lastUpdate?.digest?.password, 'newpass'); + expect(lastUpdate?.type, APIAuthType.digest); + }); + + testWidgets('updates auth data when algorithm dropdown changes', + (WidgetTester tester) async { + mockAuthData = const AuthModel( + type: APIAuthType.digest, + digest: AuthDigestModel( + username: 'user', + password: 'pass', + realm: 'realm', + nonce: 'nonce', + algorithm: 'MD5', + qop: 'auth', + opaque: 'opaque', + ), + ); + + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: DigestAuthFields( + authData: mockAuthData, + updateAuth: mockUpdateAuth, + ), + ), + ), + ); + + // Find and tap the algorithm dropdown + await tester.tap(find.byType(ADPopupMenu)); + await tester.pumpAndSettle(); + + // Select SHA-256 option + await tester.tap(find.text('SHA-256').last); + await tester.pumpAndSettle(); + + // Verify that updateAuth was called + expect(capturedAuthUpdates.length, greaterThan(0)); + final lastUpdate = capturedAuthUpdates.last; + expect(lastUpdate?.digest?.algorithm, 'SHA-256'); + }); + + testWidgets('updates auth data when realm changes', + (WidgetTester tester) async { + mockAuthData = const AuthModel( + type: APIAuthType.digest, + digest: AuthDigestModel( + username: 'user', + password: 'pass', + realm: 'oldrealm', + nonce: 'nonce', + algorithm: 'MD5', + qop: 'auth', + opaque: 'opaque', + ), + ); + + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: DigestAuthFields( + authData: mockAuthData, + updateAuth: mockUpdateAuth, + ), + ), + ), + ); + + // Find the realm field (third AuthTextField) + final realmField = find.byType(AuthTextField).at(2); + await tester.tap(realmField); + await tester.enterText(realmField, 'newrealm'); + await tester.pumpAndSettle(); + + // Verify that updateAuth was called + expect(capturedAuthUpdates.length, greaterThan(0)); + final lastUpdate = capturedAuthUpdates.last; + expect(lastUpdate?.digest?.realm, 'newrealm'); + }); + + testWidgets('respects readOnly property', (WidgetTester tester) async { + mockAuthData = const AuthModel( + type: APIAuthType.digest, + digest: AuthDigestModel( + username: 'user', + password: 'pass', + realm: 'realm', + nonce: 'nonce', + algorithm: 'MD5', + qop: 'auth', + opaque: 'opaque', + ), + ); + + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: DigestAuthFields( + authData: mockAuthData, + updateAuth: mockUpdateAuth, + readOnly: true, + ), + ), + ), + ); + + final usernameFieldFinder = find.byType(AuthTextField).first; + + // Try to enter text + await tester.enterText(usernameFieldFinder, 'testuser'); + await tester.pumpAndSettle(); + + // Ensure updateAuth was not called + expect(capturedAuthUpdates, isEmpty); + + // Check the field still shows original value + final textField = tester.widget(usernameFieldFinder); + expect(textField.controller.text, equals('user')); + }); + + testWidgets('displays correct hint texts', (WidgetTester tester) async { + mockAuthData = null; + + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: DigestAuthFields( + authData: mockAuthData, + updateAuth: mockUpdateAuth, + ), + ), + ), + ); + + expect(find.byType(AuthTextField), findsNWidgets(6)); + expect(find.byType(ADPopupMenu), findsOneWidget); + expect(find.text('Algorithm'), findsOneWidget); + }); + + testWidgets('initializes with correct default values', + (WidgetTester tester) async { + mockAuthData = null; + + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: DigestAuthFields( + authData: mockAuthData, + updateAuth: mockUpdateAuth, + ), + ), + ), + ); + + // Default algorithm should be MD5 + expect(find.text('MD5'), findsOneWidget); + + // Default QOP should be 'auth' - but this is in the TextFormField value, not visible text + // We need to check the controller value instead + expect(find.byType(AuthTextField), findsNWidgets(6)); + }); + + testWidgets('creates proper AuthModel on field changes', + (WidgetTester tester) async { + mockAuthData = null; + + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: DigestAuthFields( + authData: mockAuthData, + updateAuth: mockUpdateAuth, + ), + ), + ), + ); + + // Enter username + final usernameField = find.byType(AuthTextField).first; + await tester.tap(usernameField); + await tester.enterText(usernameField, 'testuser'); + await tester.pumpAndSettle(); + + // Verify that updateAuth was called with correct structure + expect(capturedAuthUpdates.length, greaterThan(0)); + final lastUpdate = capturedAuthUpdates.last; + expect(lastUpdate?.type, APIAuthType.digest); + expect(lastUpdate?.digest?.username, 'testuser'); + expect(lastUpdate?.digest?.algorithm, 'MD5'); + }); + + testWidgets('handles all algorithm options correctly', + (WidgetTester tester) async { + mockAuthData = null; + + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: DigestAuthFields( + authData: mockAuthData, + updateAuth: mockUpdateAuth, + ), + ), + ), + ); + + // Test each algorithm option + final algorithms = ['MD5', 'MD5-sess', 'SHA-256', 'SHA-256-sess']; + + for (final algorithm in algorithms) { + // Tap the dropdown + await tester.tap(find.byType(ADPopupMenu)); + await tester.pumpAndSettle(); + + // Select the algorithm + await tester.tap(find.text(algorithm).last); + await tester.pumpAndSettle(); + + // Verify the selection + expect(find.text(algorithm), findsOneWidget); + } + }); + + testWidgets('trims whitespace from all field inputs', + (WidgetTester tester) async { + mockAuthData = null; + + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: DigestAuthFields( + authData: mockAuthData, + updateAuth: mockUpdateAuth, + ), + ), + ), + ); + + // Enter username with whitespace + final usernameField = find.byType(AuthTextField).first; + await tester.tap(usernameField); + await tester.enterText(usernameField, ' testuser '); + await tester.pumpAndSettle(); + + // Verify that updateAuth was called with trimmed values + expect(capturedAuthUpdates.length, greaterThan(0)); + final lastUpdate = capturedAuthUpdates.last; + expect(lastUpdate?.digest?.username, 'testuser'); + }); + }); +} diff --git a/test/screens/common_widgets/auth/jwt_auth_fields_test.dart b/test/screens/common_widgets/auth/jwt_auth_fields_test.dart new file mode 100644 index 000000000..ff02166bf --- /dev/null +++ b/test/screens/common_widgets/auth/jwt_auth_fields_test.dart @@ -0,0 +1,372 @@ +import 'package:apidash/screens/common_widgets/auth/jwt_auth_fields.dart'; +import 'package:apidash/widgets/widgets.dart'; +import 'package:apidash_core/apidash_core.dart'; +import 'package:apidash_design_system/apidash_design_system.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + group('JwtAuthFields Widget Tests', () { + late AuthModel? mockAuthData; + late Function(AuthModel?) mockUpdateAuth; + late List capturedAuthUpdates; + + setUp(() { + capturedAuthUpdates = []; + mockUpdateAuth = (AuthModel? authModel) { + capturedAuthUpdates.add(authModel); + }; + }); + + testWidgets('renders with default values when authData is null', + (WidgetTester tester) async { + mockAuthData = null; + + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: JwtAuthFields( + authData: mockAuthData, + updateAuth: mockUpdateAuth, + ), + ), + ), + ); + + expect(find.text('Add JWT token to'), findsOneWidget); + expect(find.text('Algorithm'), findsOneWidget); + expect(find.text('Payload (JSON format)'), findsOneWidget); + expect(find.byType(ADPopupMenu), findsNWidgets(2)); + expect(find.text('Header'), findsOneWidget); + expect(find.text('HS256'), findsOneWidget); + }); + + testWidgets('renders with existing JWT auth data', + (WidgetTester tester) async { + mockAuthData = const AuthModel( + type: APIAuthType.jwt, + jwt: AuthJwtModel( + secret: 'test-secret', + privateKey: '', + payload: '{"sub": "1234567890"}', + addTokenTo: 'header', + algorithm: 'HS256', + isSecretBase64Encoded: false, + headerPrefix: 'Bearer', + queryParamKey: 'token', + header: 'Authorization', + ), + ); + + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: JwtAuthFields( + authData: mockAuthData, + updateAuth: mockUpdateAuth, + ), + ), + ), + ); + + expect(find.text('Add JWT token to'), findsOneWidget); + expect(find.text('Algorithm'), findsOneWidget); + expect(find.text('Header'), findsOneWidget); + expect(find.text('HS256'), findsOneWidget); + }); + + testWidgets('shows secret field for HMAC algorithms', + (WidgetTester tester) async { + mockAuthData = const AuthModel( + type: APIAuthType.jwt, + jwt: AuthJwtModel( + secret: 'test-secret', + privateKey: '', + payload: '{}', + addTokenTo: 'header', + algorithm: 'HS256', + isSecretBase64Encoded: false, + headerPrefix: 'Bearer', + queryParamKey: 'token', + header: 'Authorization', + ), + ); + + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: JwtAuthFields( + authData: mockAuthData, + updateAuth: mockUpdateAuth, + ), + ), + ), + ); + + expect(find.text('Secret Key'), findsExactly(2)); + expect(find.text('Secret is Base64 encoded'), findsOneWidget); + expect(find.byType(AuthTextField), findsOneWidget); + expect(find.byType(CheckboxListTile), findsOneWidget); + }); + + testWidgets('shows private key field for RSA algorithms', + (WidgetTester tester) async { + mockAuthData = const AuthModel( + type: APIAuthType.jwt, + jwt: AuthJwtModel( + secret: '', + privateKey: 'test-private-key', + payload: '{}', + addTokenTo: 'header', + algorithm: 'RS256', + isSecretBase64Encoded: false, + headerPrefix: 'Bearer', + queryParamKey: 'token', + header: 'Authorization', + ), + ); + + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: JwtAuthFields( + authData: mockAuthData, + updateAuth: mockUpdateAuth, + ), + ), + ), + ); + + expect(find.text('Private Key'), findsOneWidget); + expect(find.text('Secret Key'), findsNothing); + expect(find.byType(TextField), findsNWidgets(2)); // Private key + payload + }); + + testWidgets('updates auth data when add token to dropdown changes', + (WidgetTester tester) async { + mockAuthData = const AuthModel( + type: APIAuthType.jwt, + jwt: AuthJwtModel( + secret: 'secret', + privateKey: '', + payload: '{}', + addTokenTo: 'header', + algorithm: 'HS256', + isSecretBase64Encoded: false, + headerPrefix: 'Bearer', + queryParamKey: 'token', + header: 'Authorization', + ), + ); + + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: JwtAuthFields( + authData: mockAuthData, + updateAuth: mockUpdateAuth, + ), + ), + ), + ); + + // Find and tap the first dropdown (add token to) + await tester.tap(find.byType(ADPopupMenu).first); + await tester.pumpAndSettle(); + + // Select Query Parameters option + await tester.tap(find.text('Query Params').last); + await tester.pumpAndSettle(); + + // Verify that updateAuth was called + expect(capturedAuthUpdates.length, greaterThan(0)); + final lastUpdate = capturedAuthUpdates.last; + expect(lastUpdate?.jwt?.addTokenTo, 'query'); + }); + + testWidgets('updates auth data when algorithm dropdown changes', + (WidgetTester tester) async { + mockAuthData = const AuthModel( + type: APIAuthType.jwt, + jwt: AuthJwtModel( + secret: 'secret', + privateKey: '', + payload: '{}', + addTokenTo: 'header', + algorithm: 'HS256', + isSecretBase64Encoded: false, + headerPrefix: 'Bearer', + queryParamKey: 'token', + header: 'Authorization', + ), + ); + + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: JwtAuthFields( + authData: mockAuthData, + updateAuth: mockUpdateAuth, + ), + ), + ), + ); + + // Find and tap the second dropdown (algorithm) + await tester.tap(find.byType(ADPopupMenu).last); + await tester.pumpAndSettle(); + + // Select RS256 option + await tester.tap(find.text('RS256').last); + await tester.pumpAndSettle(); + + // Verify that updateAuth was called + expect(capturedAuthUpdates.length, greaterThan(0)); + final lastUpdate = capturedAuthUpdates.last; + expect(lastUpdate?.jwt?.algorithm, 'RS256'); + }); + + testWidgets('updates auth data when secret changes', + (WidgetTester tester) async { + mockAuthData = const AuthModel( + type: APIAuthType.jwt, + jwt: AuthJwtModel( + secret: 'old-secret', + privateKey: '', + payload: '{}', + addTokenTo: 'header', + algorithm: 'HS256', + isSecretBase64Encoded: false, + headerPrefix: 'Bearer', + queryParamKey: 'token', + header: 'Authorization', + ), + ); + + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: JwtAuthFields( + authData: mockAuthData, + updateAuth: mockUpdateAuth, + ), + ), + ), + ); + + // Find the secret field + final secretField = find.byType(AuthTextField).first; + await tester.tap(secretField); + await tester.enterText(secretField, 'new-secret'); + await tester.pumpAndSettle(); + + // Verify that updateAuth was called + expect(capturedAuthUpdates.length, greaterThan(0)); + final lastUpdate = capturedAuthUpdates.last; + expect(lastUpdate?.jwt?.secret, 'new-secret'); + }); + + testWidgets('updates auth data when payload changes', + (WidgetTester tester) async { + mockAuthData = const AuthModel( + type: APIAuthType.jwt, + jwt: AuthJwtModel( + secret: 'secret', + privateKey: '', + payload: '{}', + addTokenTo: 'header', + algorithm: 'HS256', + isSecretBase64Encoded: false, + headerPrefix: 'Bearer', + queryParamKey: 'token', + header: 'Authorization', + ), + ); + + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: JwtAuthFields( + authData: mockAuthData, + updateAuth: mockUpdateAuth, + ), + ), + ), + ); + + // Find the payload field (TextField) + final payloadField = find.byType(TextField).last; + await tester.tap(payloadField); + await tester.enterText(payloadField, '{"sub": "1234567890"}'); + await tester.pumpAndSettle(); + + // Verify that updateAuth was called + expect(capturedAuthUpdates.length, greaterThan(0)); + final lastUpdate = capturedAuthUpdates.last; + expect(lastUpdate?.jwt?.payload, '{"sub": "1234567890"}'); + }); + + testWidgets('updates auth data when Base64 checkbox changes', + (WidgetTester tester) async { + mockAuthData = const AuthModel( + type: APIAuthType.jwt, + jwt: AuthJwtModel( + secret: 'secret', + privateKey: '', + payload: '{}', + addTokenTo: 'header', + algorithm: 'HS256', + isSecretBase64Encoded: false, + headerPrefix: 'Bearer', + queryParamKey: 'token', + header: 'Authorization', + ), + ); + + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: JwtAuthFields( + authData: mockAuthData, + updateAuth: mockUpdateAuth, + ), + ), + ), + ); + + // Find and tap the checkbox + await tester.tap(find.byType(CheckboxListTile)); + await tester.pumpAndSettle(); + + // Verify that updateAuth was called + expect(capturedAuthUpdates.length, greaterThan(0)); + final lastUpdate = capturedAuthUpdates.last; + expect(lastUpdate?.jwt?.isSecretBase64Encoded, true); + }); + + testWidgets('initializes with correct default values', + (WidgetTester tester) async { + mockAuthData = null; + + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: JwtAuthFields( + authData: mockAuthData, + updateAuth: mockUpdateAuth, + ), + ), + ), + ); + + // Default token location should be header + expect(find.text('Header'), findsOneWidget); + + // Default algorithm should be HS256 + expect(find.text('HS256'), findsOneWidget); + + // Default Base64 encoded should be false + expect(find.byType(CheckboxListTile), findsOneWidget); + }); + }); +}