Migrate to null safety (#102)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 3bb8c5a..dd402ba 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,9 +1,11 @@
+# 2.0.0-dev
+
+* Migrate to null safety.
+
 # 1.6.3
 
 * Added optional `codeVerifier` parameter to `AuthorizationCodeGrant` constructor.
 
-# 1.6.2-dev
-
 # 1.6.1
 
 * Added fix to make sure that credentials are only refreshed once when multiple calls are made.
diff --git a/example/main.dart b/example/main.dart
index 1560cdd..4bd3cbc 100644
--- a/example/main.dart
+++ b/example/main.dart
@@ -76,7 +76,7 @@
   var client = await createClient();
 
   // Once you have a Client, you can use it just like any other HTTP client.
-  print(await client.read('http://example.com/protected-resources.txt'));
+  print(await client.read(Uri.http('example.com', 'protected-resources.txt')));
 
   // Once we're done with the client, save the credentials file. This ensures
   // that if the credentials were automatically refreshed while using the
@@ -91,5 +91,5 @@
 
 Future<Uri> listen(Uri url) async {
   // Client implementation detail
-  return null;
+  return Uri();
 }
diff --git a/lib/src/authorization_code_grant.dart b/lib/src/authorization_code_grant.dart
index 963e5b2..4a7ef8a 100644
--- a/lib/src/authorization_code_grant.dart
+++ b/lib/src/authorization_code_grant.dart
@@ -58,7 +58,7 @@
   /// available may not be able to make sure the client secret is kept a secret.
   /// This is fine; OAuth2 servers generally won't rely on knowing with
   /// certainty that a client is who it claims to be.
-  final String secret;
+  final String? secret;
 
   /// A URL provided by the authorization server that serves as the base for the
   /// URL that the resource owner will be redirected to to authorize this
@@ -78,7 +78,7 @@
   /// Callback to be invoked whenever the credentials are refreshed.
   ///
   /// This will be passed as-is to the constructed [Client].
-  final CredentialsRefreshedCallback _onCredentialsRefreshed;
+  final CredentialsRefreshedCallback? _onCredentialsRefreshed;
 
   /// Whether to use HTTP Basic authentication for authorizing the client.
   final bool _basicAuth;
@@ -87,18 +87,18 @@
   final String _delimiter;
 
   /// The HTTP client used to make HTTP requests.
-  http.Client _httpClient;
+  http.Client? _httpClient;
 
   /// The URL to which the resource owner will be redirected after they
   /// authorize this client with the authorization server.
-  Uri _redirectEndpoint;
+  Uri? _redirectEndpoint;
 
   /// The scopes that the client is requesting access to.
-  List<String> _scopes;
+  List<String>? _scopes;
 
   /// An opaque string that users of this library may specify that will be
   /// included in the response query parameters.
-  String _stateString;
+  String? _stateString;
 
   /// The current state of the grant object.
   _State _state = _State.initial;
@@ -148,13 +148,13 @@
   AuthorizationCodeGrant(
       this.identifier, this.authorizationEndpoint, this.tokenEndpoint,
       {this.secret,
-      String delimiter,
+      String? delimiter,
       bool basicAuth = true,
-      http.Client httpClient,
-      CredentialsRefreshedCallback onCredentialsRefreshed,
-      Map<String, dynamic> Function(MediaType contentType, String body)
+      http.Client? httpClient,
+      CredentialsRefreshedCallback? onCredentialsRefreshed,
+      Map<String, dynamic> Function(MediaType? contentType, String body)?
           getParameters,
-      String codeVerifier})
+      String? codeVerifier})
       : _basicAuth = basicAuth,
         _httpClient = httpClient ?? http.Client(),
         _delimiter = delimiter ?? ' ',
@@ -182,24 +182,19 @@
   ///
   /// It is a [StateError] to call this more than once.
   Uri getAuthorizationUrl(Uri redirect,
-      {Iterable<String> scopes, String state}) {
+      {Iterable<String>? scopes, String? state}) {
     if (_state != _State.initial) {
       throw StateError('The authorization URL has already been generated.');
     }
     _state = _State.awaitingResponse;
 
-    if (scopes == null) {
-      scopes = [];
-    } else {
-      scopes = scopes.toList();
-    }
-
+    var scopeList = scopes?.toList() ?? <String>[];
     var codeChallenge = base64Url
         .encode(sha256.convert(ascii.encode(_codeVerifier)).bytes)
         .replaceAll('=', '');
 
     _redirectEndpoint = redirect;
-    _scopes = scopes;
+    _scopes = scopeList;
     _stateString = state;
     var parameters = {
       'response_type': 'code',
@@ -210,7 +205,7 @@
     };
 
     if (state != null) parameters['state'] = state;
-    if (scopes.isNotEmpty) parameters['scope'] = scopes.join(_delimiter);
+    if (scopeList.isNotEmpty) parameters['scope'] = scopeList.join(_delimiter);
 
     return addQueryParameters(authorizationEndpoint, parameters);
   }
@@ -257,7 +252,7 @@
       var description = parameters['error_description'];
       var uriString = parameters['error_uri'];
       var uri = uriString == null ? null : Uri.parse(uriString);
-      throw AuthorizationException(parameters['error'], description, uri);
+      throw AuthorizationException(parameters['error']!, description, uri);
     } else if (!parameters.containsKey('code')) {
       throw FormatException('Invalid OAuth response for '
           '"$authorizationEndpoint": did not contain required parameter '
@@ -295,7 +290,7 @@
 
   /// This works just like [handleAuthorizationCode], except it doesn't validate
   /// the state beforehand.
-  Future<Client> _handleAuthorizationCode(String authorizationCode) async {
+  Future<Client> _handleAuthorizationCode(String? authorizationCode) async {
     var startTime = DateTime.now();
 
     var headers = <String, String>{};
@@ -307,6 +302,7 @@
       'code_verifier': _codeVerifier
     };
 
+    var secret = this.secret;
     if (_basicAuth && secret != null) {
       headers['Authorization'] = basicAuthHeader(identifier, secret);
     } else {
@@ -317,7 +313,7 @@
     }
 
     var response =
-        await _httpClient.post(tokenEndpoint, headers: headers, body: body);
+        await _httpClient!.post(tokenEndpoint, headers: headers, body: body);
 
     var credentials = handleAccessTokenResponse(
         response, tokenEndpoint, startTime, _scopes, _delimiter,
@@ -342,7 +338,7 @@
   /// [Client] created by this grant, so it's not safe to close the grant and
   /// continue using the client.
   void close() {
-    if (_httpClient != null) _httpClient.close();
+    _httpClient?.close();
     _httpClient = null;
   }
 }
diff --git a/lib/src/authorization_exception.dart b/lib/src/authorization_exception.dart
index a1b6dd9..14a5a3c 100644
--- a/lib/src/authorization_exception.dart
+++ b/lib/src/authorization_exception.dart
@@ -14,13 +14,13 @@
   /// The description of the error, provided by the server.
   ///
   /// May be `null` if the server provided no description.
-  final String description;
+  final String? description;
 
   /// A URL for a page that describes the error in more detail, provided by the
   /// server.
   ///
   /// May be `null` if the server provided no URL.
-  final Uri uri;
+  final Uri? uri;
 
   /// Creates an AuthorizationException.
   AuthorizationException(this.error, this.description, this.uri);
diff --git a/lib/src/client.dart b/lib/src/client.dart
index 7a78aa3..dc50e43 100644
--- a/lib/src/client.dart
+++ b/lib/src/client.dart
@@ -4,6 +4,7 @@
 
 import 'dart:async';
 
+import 'package:collection/collection.dart';
 import 'package:http/http.dart' as http;
 import 'package:http_parser/http_parser.dart';
 
@@ -40,7 +41,7 @@
   /// pair that any client may use.
   ///
   /// This is usually global to the program using this library.
-  final String identifier;
+  final String? identifier;
 
   /// The client secret for this client.
   ///
@@ -55,7 +56,7 @@
   /// available may not be able to make sure the client secret is kept a secret.
   /// This is fine; OAuth2 servers generally won't rely on knowing with
   /// certainty that a client is who it claims to be.
-  final String secret;
+  final String? secret;
 
   /// The credentials this client uses to prove to the resource server that it's
   /// authorized.
@@ -66,13 +67,13 @@
   Credentials _credentials;
 
   /// Callback to be invoked whenever the credentials refreshed.
-  final CredentialsRefreshedCallback _onCredentialsRefreshed;
+  final CredentialsRefreshedCallback? _onCredentialsRefreshed;
 
   /// Whether to use HTTP Basic authentication for authorizing the client.
   final bool _basicAuth;
 
   /// The underlying HTTP client.
-  http.Client _httpClient;
+  http.Client? _httpClient;
 
   /// Creates a new client from a pre-existing set of credentials.
   ///
@@ -87,9 +88,9 @@
   Client(this._credentials,
       {this.identifier,
       this.secret,
-      CredentialsRefreshedCallback onCredentialsRefreshed,
+      CredentialsRefreshedCallback? onCredentialsRefreshed,
       bool basicAuth = true,
-      http.Client httpClient})
+      http.Client? httpClient})
       : _basicAuth = basicAuth,
         _onCredentialsRefreshed = onCredentialsRefreshed,
         _httpClient = httpClient ?? http.Client() {
@@ -110,33 +111,32 @@
     }
 
     request.headers['authorization'] = 'Bearer ${credentials.accessToken}';
-    var response = await _httpClient.send(request);
+    var response = await _httpClient!.send(request);
 
     if (response.statusCode != 401) return response;
     if (!response.headers.containsKey('www-authenticate')) return response;
 
-    var challenges;
+    List<AuthenticationChallenge> challenges;
     try {
       challenges = AuthenticationChallenge.parseHeader(
-          response.headers['www-authenticate']);
+          response.headers['www-authenticate']!);
     } on FormatException {
       return response;
     }
 
-    var challenge = challenges.firstWhere(
-        (challenge) => challenge.scheme == 'bearer',
-        orElse: () => null);
+    var challenge = challenges
+        .firstWhereOrNull((challenge) => challenge.scheme == 'bearer');
     if (challenge == null) return response;
 
     var params = challenge.parameters;
     if (!params.containsKey('error')) return response;
 
-    throw AuthorizationException(params['error'], params['error_description'],
-        params['error_uri'] == null ? null : Uri.parse(params['error_uri']));
+    throw AuthorizationException(params['error']!, params['error_description'],
+        params['error_uri'] == null ? null : Uri.parse(params['error_uri']!));
   }
 
   /// A [Future] used to track whether [refreshCredentials] is running.
-  Future<Credentials> _refreshingFuture;
+  Future<Credentials>? _refreshingFuture;
 
   /// Explicitly refreshes this client's credentials. Returns this client.
   ///
@@ -147,7 +147,7 @@
   /// You may request different scopes than the default by passing in
   /// [newScopes]. These must be a subset of the scopes in the
   /// [Credentials.scopes] field of [Client.credentials].
-  Future<Client> refreshCredentials([List<String> newScopes]) async {
+  Future<Client> refreshCredentials([List<String>? newScopes]) async {
     if (!credentials.canRefresh) {
       var prefix = 'OAuth credentials';
       if (credentials.isExpired) prefix = '$prefix have expired and';
@@ -166,7 +166,7 @@
           basicAuth: _basicAuth,
           httpClient: _httpClient,
         );
-        _credentials = await _refreshingFuture;
+        _credentials = await _refreshingFuture!;
         _onCredentialsRefreshed?.call(_credentials);
       } finally {
         _refreshingFuture = null;
@@ -181,7 +181,7 @@
   /// Closes this client and its underlying HTTP client.
   @override
   void close() {
-    if (_httpClient != null) _httpClient.close();
+    _httpClient?.close();
     _httpClient = null;
   }
 }
diff --git a/lib/src/client_credentials_grant.dart b/lib/src/client_credentials_grant.dart
index c6d34b9..ab13a3f 100644
--- a/lib/src/client_credentials_grant.dart
+++ b/lib/src/client_credentials_grant.dart
@@ -40,12 +40,12 @@
 /// its body as a UTF-8-decoded string. It should return a map in the same
 /// format as the [standard JSON response](https://tools.ietf.org/html/rfc6749#section-5.1)
 Future<Client> clientCredentialsGrant(
-    Uri authorizationEndpoint, String identifier, String secret,
-    {Iterable<String> scopes,
+    Uri authorizationEndpoint, String? identifier, String? secret,
+    {Iterable<String>? scopes,
     bool basicAuth = true,
-    http.Client httpClient,
-    String delimiter,
-    Map<String, dynamic> Function(MediaType contentType, String body)
+    http.Client? httpClient,
+    String? delimiter,
+    Map<String, dynamic> Function(MediaType? contentType, String body)?
         getParameters}) async {
   delimiter ??= ' ';
   var startTime = DateTime.now();
@@ -56,7 +56,7 @@
 
   if (identifier != null) {
     if (basicAuth) {
-      headers['Authorization'] = basicAuthHeader(identifier, secret);
+      headers['Authorization'] = basicAuthHeader(identifier, secret!);
     } else {
       body['client_id'] = identifier;
       if (secret != null) body['client_secret'] = secret;
@@ -71,8 +71,8 @@
   var response = await httpClient.post(authorizationEndpoint,
       headers: headers, body: body);
 
-  var credentials = await handleAccessTokenResponse(
-      response, authorizationEndpoint, startTime, scopes, delimiter,
+  var credentials = await handleAccessTokenResponse(response,
+      authorizationEndpoint, startTime, scopes?.toList() ?? [], delimiter,
       getParameters: getParameters);
   return Client(credentials,
       identifier: identifier, secret: secret, httpClient: httpClient);
diff --git a/lib/src/credentials.dart b/lib/src/credentials.dart
index 2b40b05..88a8f45 100644
--- a/lib/src/credentials.dart
+++ b/lib/src/credentials.dart
@@ -42,7 +42,7 @@
   /// credentials.
   ///
   /// This may be `null`, indicating that the credentials can't be refreshed.
-  final String refreshToken;
+  final String? refreshToken;
 
   /// The token that is received from the authorization server to enable
   /// End-Users to be Authenticated, contains Claims, represented as a
@@ -52,25 +52,25 @@
   /// requested (or not supported).
   ///
   /// [spec]: https://openid.net/specs/openid-connect-core-1_0.html#IDToken
-  final String idToken;
+  final String? idToken;
 
   /// The URL of the authorization server endpoint that's used to refresh the
   /// credentials.
   ///
   /// This may be `null`, indicating that the credentials can't be refreshed.
-  final Uri tokenEndpoint;
+  final Uri? tokenEndpoint;
 
   /// The specific permissions being requested from the authorization server.
   ///
   /// The scope strings are specific to the authorization server and may be
   /// found in its documentation.
-  final List<String> scopes;
+  final List<String>? scopes;
 
   /// The date at which these credentials will expire.
   ///
   /// This is likely to be a few seconds earlier than the server's idea of the
   /// expiration date.
-  final DateTime expiration;
+  final DateTime? expiration;
 
   /// The function used to parse parameters from a host's response.
   final GetParameters _getParameters;
@@ -80,8 +80,10 @@
   /// Note that it's possible the credentials will expire shortly after this is
   /// called. However, since the client's expiration date is kept a few seconds
   /// earlier than the server's, there should be enough leeway to rely on this.
-  bool get isExpired =>
-      expiration != null && DateTime.now().isAfter(expiration);
+  bool get isExpired {
+    var expiration = this.expiration;
+    return expiration != null && DateTime.now().isAfter(expiration);
+  }
 
   /// Whether it's possible to refresh these credentials.
   bool get canRefresh => refreshToken != null && tokenEndpoint != null;
@@ -110,10 +112,10 @@
       {this.refreshToken,
       this.idToken,
       this.tokenEndpoint,
-      Iterable<String> scopes,
+      Iterable<String>? scopes,
       this.expiration,
-      String delimiter,
-      Map<String, dynamic> Function(MediaType mediaType, String body)
+      String? delimiter,
+      Map<String, dynamic> Function(MediaType? mediaType, String body)?
           getParameters})
       : scopes = UnmodifiableListView(
             // Explicitly type-annotate the list literal to work around
@@ -183,11 +185,9 @@
         'accessToken': accessToken,
         'refreshToken': refreshToken,
         'idToken': idToken,
-        'tokenEndpoint':
-            tokenEndpoint == null ? null : tokenEndpoint.toString(),
+        'tokenEndpoint': tokenEndpoint?.toString(),
         'scopes': scopes,
-        'expiration':
-            expiration == null ? null : expiration.millisecondsSinceEpoch
+        'expiration': expiration?.millisecondsSinceEpoch
       });
 
   /// Returns a new set of refreshed credentials.
@@ -203,11 +203,11 @@
   /// [AuthorizationException] if refreshing the credentials fails, or a
   /// [FormatError] if the authorization server returns invalid responses.
   Future<Credentials> refresh(
-      {String identifier,
-      String secret,
-      Iterable<String> newScopes,
+      {String? identifier,
+      String? secret,
+      Iterable<String>? newScopes,
       bool basicAuth = true,
-      http.Client httpClient}) async {
+      http.Client? httpClient}) async {
     var scopes = this.scopes;
     if (newScopes != null) scopes = newScopes.toList();
     scopes ??= [];
@@ -218,6 +218,7 @@
     }
 
     var startTime = DateTime.now();
+    var tokenEndpoint = this.tokenEndpoint;
     if (refreshToken == null) {
       throw StateError("Can't refresh credentials without a refresh "
           'token.');
@@ -232,7 +233,7 @@
     if (scopes.isNotEmpty) body['scope'] = scopes.join(_delimiter);
 
     if (basicAuth && secret != null) {
-      headers['Authorization'] = basicAuthHeader(identifier, secret);
+      headers['Authorization'] = basicAuthHeader(identifier!, secret);
     } else {
       if (identifier != null) body['client_id'] = identifier;
       if (secret != null) body['client_secret'] = secret;
diff --git a/lib/src/handle_access_token_response.dart b/lib/src/handle_access_token_response.dart
index 7517af1..370ffcb 100644
--- a/lib/src/handle_access_token_response.dart
+++ b/lib/src/handle_access_token_response.dart
@@ -31,8 +31,8 @@
 ///
 /// [standard JSON response]: https://tools.ietf.org/html/rfc6749#section-5.1
 Credentials handleAccessTokenResponse(http.Response response, Uri tokenEndpoint,
-    DateTime startTime, List<String> scopes, String delimiter,
-    {Map<String, dynamic> Function(MediaType contentType, String body)
+    DateTime startTime, List<String>? scopes, String delimiter,
+    {Map<String, dynamic> Function(MediaType? contentType, String body)?
         getParameters}) {
   getParameters ??= parseJsonParameters;
 
@@ -81,7 +81,7 @@
       }
     }
 
-    var scope = parameters['scope'] as String;
+    var scope = parameters['scope'] as String?;
     if (scope != null) scopes = scope.split(delimiter);
 
     var expiration = expiresIn == null
@@ -109,8 +109,9 @@
   // off-spec.
   if (response.statusCode != 400 && response.statusCode != 401) {
     var reason = '';
-    if (response.reasonPhrase != null && response.reasonPhrase.isNotEmpty) {
-      ' ${response.reasonPhrase}';
+    var reasonPhrase = response.reasonPhrase;
+    if (reasonPhrase != null && reasonPhrase.isNotEmpty) {
+      reason = ' $reasonPhrase';
     }
     throw FormatException('OAuth request for "$tokenEndpoint" failed '
         'with status ${response.statusCode}$reason.\n\n${response.body}');
diff --git a/lib/src/parameters.dart b/lib/src/parameters.dart
index e8290fc..789f056 100644
--- a/lib/src/parameters.dart
+++ b/lib/src/parameters.dart
@@ -8,13 +8,13 @@
 
 /// The type of a callback that parses parameters from an HTTP response.
 typedef GetParameters = Map<String, dynamic> Function(
-    MediaType contentType, String body);
+    MediaType? contentType, String body);
 
 /// Parses parameters from a response with a JSON body, as per the [OAuth2
 /// spec][].
 ///
 /// [OAuth2 spec]: https://tools.ietf.org/html/rfc6749#section-5.1
-Map<String, dynamic> parseJsonParameters(MediaType contentType, String body) {
+Map<String, dynamic> parseJsonParameters(MediaType? contentType, String body) {
   // The spec requires a content-type of application/json, but some endpoints
   // (e.g. Dropbox) serve it as text/javascript instead.
   if (contentType == null ||
diff --git a/lib/src/resource_owner_password_grant.dart b/lib/src/resource_owner_password_grant.dart
index 118f7c9..f54ac85 100644
--- a/lib/src/resource_owner_password_grant.dart
+++ b/lib/src/resource_owner_password_grant.dart
@@ -46,14 +46,14 @@
 /// [standard JSON response]: https://tools.ietf.org/html/rfc6749#section-5.1
 Future<Client> resourceOwnerPasswordGrant(
     Uri authorizationEndpoint, String username, String password,
-    {String identifier,
-    String secret,
-    Iterable<String> scopes,
+    {String? identifier,
+    String? secret,
+    Iterable<String>? scopes,
     bool basicAuth = true,
-    CredentialsRefreshedCallback onCredentialsRefreshed,
-    http.Client httpClient,
-    String delimiter,
-    Map<String, dynamic> Function(MediaType contentType, String body)
+    CredentialsRefreshedCallback? onCredentialsRefreshed,
+    http.Client? httpClient,
+    String? delimiter,
+    Map<String, dynamic> Function(MediaType? contentType, String body)?
         getParameters}) async {
   delimiter ??= ' ';
   var startTime = DateTime.now();
@@ -68,7 +68,7 @@
 
   if (identifier != null) {
     if (basicAuth) {
-      headers['Authorization'] = basicAuthHeader(identifier, secret);
+      headers['Authorization'] = basicAuthHeader(identifier, secret!);
     } else {
       body['client_id'] = identifier;
       if (secret != null) body['client_secret'] = secret;
@@ -84,7 +84,7 @@
       headers: headers, body: body);
 
   var credentials = await handleAccessTokenResponse(
-      response, authorizationEndpoint, startTime, scopes, delimiter,
+      response, authorizationEndpoint, startTime, scopes?.toList(), delimiter,
       getParameters: getParameters);
   return Client(credentials,
       identifier: identifier,
diff --git a/pubspec.yaml b/pubspec.yaml
index 3fad972..ab42be2 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -1,5 +1,5 @@
 name: oauth2
-version: 1.6.3
+version: 2.0.0-dev
 homepage: https://github.com/dart-lang/oauth2
 description: >-
   A client library for authenticating with a remote service via OAuth2 on
@@ -7,13 +7,14 @@
   OAuth2 credentials.
 
 environment:
-  sdk: '>=2.0.0 <3.0.0'
+  sdk: '>=2.12.0-0 <3.0.0'
 
 dependencies:
-  http: '>=0.11.0 <0.13.0'
-  http_parser: '>=1.0.0 <4.0.0'
-  crypto: '^2.1.3'
+  collection: ^1.15.0
+  crypto: ^3.0.0
+  http: ^0.13.0
+  http_parser: ^4.0.0
 
 dev_dependencies:
-  pedantic: ^1.2.0
-  test: ^1.0.0
+  pedantic: ^1.10.0
+  test: ^1.16.0
diff --git a/test/authorization_code_grant_test.dart b/test/authorization_code_grant_test.dart
index f74c061..8010bcf 100644
--- a/test/authorization_code_grant_test.dart
+++ b/test/authorization_code_grant_test.dart
@@ -14,8 +14,8 @@
 final redirectUrl = Uri.parse('http://example.com/redirect');
 
 void main() {
-  ExpectClient client;
-  oauth2.AuthorizationCodeGrant grant;
+  late ExpectClient client;
+  late oauth2.AuthorizationCodeGrant grant;
   setUp(() {
     client = ExpectClient();
     grant = oauth2.AuthorizationCodeGrant(
@@ -223,7 +223,7 @@
       expect(grant.handleAuthorizationCode('auth code'), throwsStateError);
     });
 
-    test('sends an authorization code request', () {
+    test('sends an authorization code request', () async {
       grant.getAuthorizationUrl(redirectUrl);
       client.expectRequest((request) {
         expect(request.method, equals('POST'));
@@ -249,11 +249,10 @@
             headers: {'content-type': 'application/json'}));
       });
 
-      expect(grant.handleAuthorizationCode('auth code'),
-          completion(predicate((client) {
-        expect(client.credentials.accessToken, equals('access token'));
-        return true;
-      })));
+      expect(
+          await grant.handleAuthorizationCode('auth code'),
+          isA<oauth2.Client>().having((c) => c.credentials.accessToken,
+              'credentials.accessToken', 'access token'));
     });
   });
 
@@ -302,7 +301,8 @@
           completion(equals('access token')));
     });
 
-    test('.handleAuthorizationCode sends an authorization code request', () {
+    test('.handleAuthorizationCode sends an authorization code request',
+        () async {
       grant.getAuthorizationUrl(redirectUrl);
       client.expectRequest((request) {
         expect(request.method, equals('POST'));
@@ -328,11 +328,10 @@
             headers: {'content-type': 'application/json'}));
       });
 
-      expect(grant.handleAuthorizationCode('auth code'),
-          completion(predicate((client) {
-        expect(client.credentials.accessToken, equals('access token'));
-        return true;
-      })));
+      expect(
+          await grant.handleAuthorizationCode('auth code'),
+          isA<oauth2.Client>().having((c) => c.credentials.accessToken,
+              'credentials.accessToken', 'access token'));
     });
   });
 
diff --git a/test/handle_access_token_response_test.dart b/test/handle_access_token_response_test.dart
index 10aef48..4561749 100644
--- a/test/handle_access_token_response_test.dart
+++ b/test/handle_access_token_response_test.dart
@@ -18,7 +18,7 @@
 final DateTime startTime = DateTime.now();
 
 oauth2.Credentials handle(http.Response response,
-        {GetParameters getParameters}) =>
+        {GetParameters? getParameters}) =>
     handleAccessTokenResponse(
         response, tokenEndpoint, startTime, ['scope'], ' ',
         getParameters: getParameters);
@@ -141,7 +141,7 @@
     });
 
     test('with no content-type causes a FormatException', () {
-      expect(() => handleSuccess(contentType: null), throwsFormatException);
+      expect(() => handleSuccess(contentType: ''), throwsFormatException);
     });
 
     test('with a non-JSON content-type causes a FormatException', () {
@@ -221,7 +221,7 @@
         'with expires-in sets the expiration to ten seconds earlier than the '
         'server says', () {
       var credentials = handleSuccess(expiresIn: 100);
-      expect(credentials.expiration.millisecondsSinceEpoch,
+      expect(credentials.expiration?.millisecondsSinceEpoch,
           startTime.millisecondsSinceEpoch + 90 * 1000);
     });
 
diff --git a/test/utils.dart b/test/utils.dart
index df43af5..f66e7df 100644
--- a/test/utils.dart
+++ b/test/utils.dart
@@ -18,7 +18,7 @@
         super(fn);
 
   factory ExpectClient() {
-    ExpectClient client;
+    late ExpectClient client;
     client = ExpectClient._((request) => client._handleRequest(request));
     return client;
   }