diff --git a/CHANGELOG.md b/CHANGELOG.md index d77049e..d58bde7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +## 2.0.0 + +- BREAKING: `TokenStorage` now provides the option to asynchronously retrieve tokens. +- Fix internal lint errors +- Upgrade dependencies + ## 1.0.2 - Automatically refreshes expired client tokens diff --git a/README.md b/README.md index 931ba1c..a527f34 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ Easily authenticate using OAuth 2.0 client/password grants. Install passputter from [pub.dev](https://pub.dev/packages/passputter): ```yaml -passputter: ^1.0.2 +passputter: ^2.0.0 ``` ## ✅ Prerequisites @@ -47,7 +47,7 @@ class HiveTokenStorage implements TokenStorage { static const _userTokenKey = 'userToken'; @override - OAuthToken? get clientToken { + FutureOr<OAuthToken?> get clientToken async { final tokenMap = _box.get(_clientTokenKey); if (tokenMap != null) { return OAuthToken.fromJson(tokenMap); @@ -57,7 +57,7 @@ class HiveTokenStorage implements TokenStorage { } @override - OAuthToken? get userToken { + FutureOr<OAuthToken?> get userToken async { final tokenMap = _box.get(_userTokenKey); if (tokenMap != null) { return OAuthToken.fromJson(tokenMap); diff --git a/lib/src/client_token_interceptor.dart b/lib/src/client_token_interceptor.dart index 4348e16..29d67a5 100644 --- a/lib/src/client_token_interceptor.dart +++ b/lib/src/client_token_interceptor.dart @@ -1,7 +1,6 @@ // 📦 Package imports: import 'package:clock/clock.dart'; import 'package:dio/dio.dart'; - // 🌎 Project imports: import 'package:passputter/passputter.dart'; import 'package:passputter/src/oauth_api_interface.dart'; @@ -38,7 +37,7 @@ class ClientTokenInterceptor extends Interceptor { RequestOptions options, RequestInterceptorHandler handler, ) async { - final token = tokenStorage.clientToken; + final token = await tokenStorage.clientToken; if (token == null) { try { // No token saved; get another one. diff --git a/lib/src/oauth_api_impl.dart b/lib/src/oauth_api_impl.dart index 56ede8f..3adfce8 100644 --- a/lib/src/oauth_api_impl.dart +++ b/lib/src/oauth_api_impl.dart @@ -1,6 +1,5 @@ // 📦 Package imports: import 'package:dio/dio.dart'; - // 🌎 Project imports: import 'package:passputter/src/oauth_api_interface.dart'; import 'package:passputter/src/oauth_token.dart'; @@ -23,7 +22,7 @@ class OAuthApiImpl implements OAuthApiInterface { required String clientId, required String clientSecret, }) async { - final r = await client.post( + final r = await client.post<String>( endpoint, data: <String, String>{ 'client_id': clientId, @@ -33,7 +32,7 @@ class OAuthApiImpl implements OAuthApiInterface { options: Options(contentType: Headers.formUrlEncodedContentType), ); - return OAuthToken.fromJson(r.data); + return OAuthToken.fromJson(r.data!); } @override @@ -42,7 +41,7 @@ class OAuthApiImpl implements OAuthApiInterface { required String clientId, required String clientSecret, }) async { - final r = await client.post( + final r = await client.post<String>( endpoint, data: <String, String>{ 'refresh_token': refreshToken, @@ -53,7 +52,7 @@ class OAuthApiImpl implements OAuthApiInterface { options: Options(contentType: Headers.formUrlEncodedContentType), ); - return OAuthToken.fromJson(r.data); + return OAuthToken.fromJson(r.data!); } @override @@ -63,7 +62,7 @@ class OAuthApiImpl implements OAuthApiInterface { required String clientId, required String clientSecret, }) async { - final r = await client.post( + final r = await client.post<String>( endpoint, data: <String, String>{ 'username': username, @@ -75,6 +74,6 @@ class OAuthApiImpl implements OAuthApiInterface { options: Options(contentType: Headers.formUrlEncodedContentType), ); - return OAuthToken.fromJson(r.data); + return OAuthToken.fromJson(r.data!); } } diff --git a/lib/src/oauth_token.dart b/lib/src/oauth_token.dart index 76f10d9..e52e64d 100644 --- a/lib/src/oauth_token.dart +++ b/lib/src/oauth_token.dart @@ -3,8 +3,10 @@ import 'dart:convert'; // 📦 Package imports: import 'package:clock/clock.dart'; +import 'package:meta/meta.dart'; /// An authentication token. +@immutable class OAuthToken { /// Constructs an [OAuthToken] const OAuthToken({ @@ -18,19 +20,19 @@ class OAuthToken { Map<String, dynamic> map, [ Clock clock = const Clock(), ]) { - final expiresIn = map['expires_in']; + final expiresIn = map['expires_in'] as int?; return OAuthToken( - token: map['access_token'], + token: map['access_token'] as String, expiresAt: expiresIn != null ? clock.now().add(Duration(seconds: expiresIn)) : null, - refreshToken: map['refresh_token'], + refreshToken: map['refresh_token'] as String?, ); } /// Constructs an [OAuthToken] from a JSON [source] factory OAuthToken.fromJson(String source) => - OAuthToken.fromMap(json.decode(source)); + OAuthToken.fromMap(json.decode(source) as Map<String, dynamic>); /// The token used to authenticate requests. /// diff --git a/lib/src/token_storage.dart b/lib/src/token_storage.dart index 674e29a..20ecf18 100644 --- a/lib/src/token_storage.dart +++ b/lib/src/token_storage.dart @@ -1,13 +1,15 @@ // 🌎 Project imports: +import 'dart:async'; + import 'oauth_token.dart'; /// Handles storage and retrieval of [OAuthToken]s. abstract class TokenStorage { /// Retrieves the currently saved client token if it exists, or none. - OAuthToken? get clientToken; + FutureOr<OAuthToken?> get clientToken; /// Retrieves the currently saved user token if it exists, or none. - OAuthToken? get userToken; + FutureOr<OAuthToken?> get userToken; /// Saves a new client [token]. /// diff --git a/lib/src/user_token_interceptor.dart b/lib/src/user_token_interceptor.dart index e3beaf1..08a197c 100644 --- a/lib/src/user_token_interceptor.dart +++ b/lib/src/user_token_interceptor.dart @@ -1,7 +1,6 @@ // 📦 Package imports: import 'package:clock/clock.dart'; import 'package:dio/dio.dart'; - // 🌎 Project imports: import 'package:passputter/passputter.dart'; import 'package:passputter/src/oauth_api_interface.dart'; @@ -38,7 +37,7 @@ class UserTokenInterceptor extends Interceptor { RequestOptions options, RequestInterceptorHandler handler, ) async { - final token = tokenStorage.userToken; + final token = await tokenStorage.userToken; if (token != null) { if (token.expiresAt != null && token.expiresAt!.isBefore(clock.now())) { final refreshToken = token.refreshToken; @@ -62,7 +61,6 @@ class UserTokenInterceptor extends Interceptor { return handler.reject( DioError( requestOptions: options, - type: DioErrorType.other, error: TokenExpiredException(token), ), ); diff --git a/pubspec.yaml b/pubspec.yaml index e527577..f0ae60d 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: passputter description: Easily authenticate using OAuth 2.0 client/password grants. -version: 1.0.2 +version: 2.0.0 repository: https://github.com/netsells/passputter environment: @@ -13,7 +13,7 @@ dependencies: dev_dependencies: import_sorter: ^4.5.0 mock_web_server: ^5.0.0-nullsafety.1 - mocktail: ^0.1.2 + mocktail: ^0.2.0 pretty_dio_logger: ^1.2.0-beta-1 test: ^1.17.3 time: ^2.0.0 diff --git a/test/src/oauth_api_impl_test.dart b/test/src/oauth_api_impl_test.dart index 1793945..d3bcf82 100644 --- a/test/src/oauth_api_impl_test.dart +++ b/test/src/oauth_api_impl_test.dart @@ -1,11 +1,10 @@ // 📦 Package imports: import 'package:dio/dio.dart'; import 'package:mocktail/mocktail.dart'; -import 'package:test/test.dart'; - // 🌎 Project imports: import 'package:passputter/src/oauth_api_impl.dart'; import 'package:passputter/src/oauth_token.dart'; +import 'package:test/test.dart'; class MockDio extends Mock implements Dio {} @@ -28,7 +27,7 @@ void main() { group('getClientToken', () { test('successfully returns token', () async { when( - () => dio.post( + () => dio.post<String>( endpoint, data: <String, String>{ 'client_id': clientId, @@ -41,10 +40,12 @@ void main() { (_) async => Response( requestOptions: RequestOptions(path: endpoint), statusCode: 200, - data: '''{ + data: ''' + { "access_token": "token", "refresh_token": "refresh" - }''', + } + ''', ), ); @@ -64,7 +65,7 @@ void main() { test('throws DioError if one is thrown by request', () async { when( - () => dio.post( + () => dio.post<String>( endpoint, data: <String, String>{ 'client_id': clientId, @@ -76,7 +77,7 @@ void main() { ).thenAnswer((_) => Future.error(tError)); expect( - () async => await oAuthApi.getClientToken( + () async => oAuthApi.getClientToken( clientId: clientId, clientSecret: clientSecret, ), @@ -90,7 +91,7 @@ void main() { test('successfully returns token', () async { when( - () => dio.post( + () => dio.post<String>( endpoint, data: <String, String>{ 'refresh_token': refreshToken, @@ -104,10 +105,12 @@ void main() { (_) async => Response( requestOptions: RequestOptions(path: endpoint), statusCode: 200, - data: '''{ + data: ''' + { "access_token": "token", "refresh_token": "refresh" - }''', + } + ''', ), ); @@ -128,7 +131,7 @@ void main() { test('throws DioError if one is thrown by request', () async { when( - () => dio.post( + () => dio.post<String>( endpoint, data: <String, String>{ 'refresh_token': refreshToken, @@ -141,7 +144,7 @@ void main() { ).thenAnswer((_) => Future.error(tError)); expect( - () async => await oAuthApi.getRefreshedToken( + () async => oAuthApi.getRefreshedToken( refreshToken: refreshToken, clientId: clientId, clientSecret: clientSecret, @@ -157,7 +160,7 @@ void main() { test('successfully returns token', () async { when( - () => dio.post( + () => dio.post<String>( endpoint, data: <String, String>{ 'username': username, @@ -172,10 +175,12 @@ void main() { (_) async => Response( requestOptions: RequestOptions(path: endpoint), statusCode: 200, - data: '''{ + data: ''' + { "access_token": "token", "refresh_token": "refresh" - }''', + } + ''', ), ); @@ -197,7 +202,7 @@ void main() { test('throws DioError if one is thrown by request', () async { when( - () => dio.post( + () => dio.post<String>( endpoint, data: <String, String>{ 'username': username, @@ -211,7 +216,7 @@ void main() { ).thenAnswer((_) => Future.error(tError)); expect( - () async => await oAuthApi.getUserToken( + () async => oAuthApi.getUserToken( username: username, password: password, clientId: clientId, diff --git a/test/src/user_token_interceptor_test.dart b/test/src/user_token_interceptor_test.dart index 40dc172..ad9ecde 100644 --- a/test/src/user_token_interceptor_test.dart +++ b/test/src/user_token_interceptor_test.dart @@ -2,14 +2,13 @@ import 'package:clock/clock.dart'; import 'package:dio/dio.dart'; import 'package:mocktail/mocktail.dart'; -import 'package:test/test.dart'; -import 'package:time/time.dart'; - // 🌎 Project imports: import 'package:passputter/passputter.dart'; import 'package:passputter/src/oauth_api_interface.dart'; import 'package:passputter/src/oauth_token.dart'; import 'package:passputter/src/token_expired_exception.dart'; +import 'package:test/test.dart'; +import 'package:time/time.dart'; class MockOAuthApi extends Mock implements OAuthApiInterface {} @@ -23,7 +22,7 @@ void main() { late UserTokenInterceptor interceptor; setUpAll(() { - registerFallbackValue<DioError>( + registerFallbackValue( DioError( requestOptions: RequestOptions( path: 'path', @@ -36,7 +35,7 @@ void main() { tokenStorage = InMemoryTokenStorage(); oAuthApi = MockOAuthApi(); handler = MockHandler(); - clock = Clock.fixed(DateTime(2021, 1, 1)); + clock = Clock.fixed(DateTime(2021)); interceptor = UserTokenInterceptor( tokenStorage: tokenStorage, oAuthApi: oAuthApi,