From 3d995d484ce3436ebbb891640bcd48771abeb00b Mon Sep 17 00:00:00 2001 From: stuartmorgan Date: Thu, 12 May 2022 20:29:07 -0400 Subject: [PATCH] [google_sign_in] Switch unit tests to mock platform implementation (#5703) --- .../google_sign_in/CHANGELOG.md | 3 +- .../google_sign_in/pubspec.yaml | 2 + .../test/google_sign_in_test.dart | 406 +++++++----------- .../test/google_sign_in_test.mocks.dart | 100 +++++ 4 files changed, 260 insertions(+), 251 deletions(-) create mode 100644 packages/google_sign_in/google_sign_in/test/google_sign_in_test.mocks.dart diff --git a/packages/google_sign_in/google_sign_in/CHANGELOG.md b/packages/google_sign_in/google_sign_in/CHANGELOG.md index 8416e81f2412..9edadf7b0469 100644 --- a/packages/google_sign_in/google_sign_in/CHANGELOG.md +++ b/packages/google_sign_in/google_sign_in/CHANGELOG.md @@ -1,6 +1,7 @@ ## NEXT -* Fixes tests to recognize new default `forceCodeForRefreshToken` request attribute. +* Updates tests to use a mock platform instead of relying on default + method channel implementation internals. ## 5.3.1 diff --git a/packages/google_sign_in/google_sign_in/pubspec.yaml b/packages/google_sign_in/google_sign_in/pubspec.yaml index e58b27af08b7..d1c13c6a8ec4 100644 --- a/packages/google_sign_in/google_sign_in/pubspec.yaml +++ b/packages/google_sign_in/google_sign_in/pubspec.yaml @@ -29,6 +29,7 @@ dependencies: google_sign_in_web: ^0.10.0 dev_dependencies: + build_runner: ^2.1.10 flutter_driver: sdk: flutter flutter_test: @@ -36,6 +37,7 @@ dev_dependencies: http: ^0.13.0 integration_test: sdk: flutter + mockito: ^5.1.0 # The example deliberately includes limited-use secrets. false_secrets: diff --git a/packages/google_sign_in/google_sign_in/test/google_sign_in_test.dart b/packages/google_sign_in/google_sign_in/test/google_sign_in_test.dart index 3b0654637d68..61acfd81bf09 100644 --- a/packages/google_sign_in/google_sign_in/test/google_sign_in_test.dart +++ b/packages/google_sign_in/google_sign_in/test/google_sign_in_test.dart @@ -4,229 +4,176 @@ import 'dart:async'; -import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:google_sign_in/google_sign_in.dart'; -import 'package:google_sign_in/testing.dart'; import 'package:google_sign_in_platform_interface/google_sign_in_platform_interface.dart'; +import 'package:mockito/annotations.dart'; +import 'package:mockito/mockito.dart'; +import 'google_sign_in_test.mocks.dart'; + +@GenerateMocks([GoogleSignInPlatform]) void main() { - TestWidgetsFlutterBinding.ensureInitialized(); + late MockGoogleSignInPlatform mockPlatform; group('GoogleSignIn', () { - const MethodChannel channel = MethodChannel( - 'plugins.flutter.io/google_sign_in', - ); - - const Map kUserData = { - 'email': 'john.doe@gmail.com', - 'id': '8162538176523816253123', - 'photoUrl': 'https://lh5.googleusercontent.com/photo.jpg', - 'displayName': 'John Doe', - 'serverAuthCode': '789' - }; - - const Map kDefaultResponses = { - 'init': null, - 'signInSilently': kUserData, - 'signIn': kUserData, - 'signOut': null, - 'disconnect': null, - 'isSignedIn': true, - 'requestScopes': true, - 'getTokens': { - 'idToken': '123', - 'accessToken': '456', - 'serverAuthCode': '789', - }, - }; - - final List log = []; - late Map responses; - late GoogleSignIn googleSignIn; + final GoogleSignInUserData kDefaultUser = GoogleSignInUserData( + email: 'john.doe@gmail.com', + id: '8162538176523816253123', + photoUrl: 'https://lh5.googleusercontent.com/photo.jpg', + displayName: 'John Doe', + serverAuthCode: '789'); setUp(() { - responses = Map.from(kDefaultResponses); - channel.setMockMethodCallHandler((MethodCall methodCall) { - log.add(methodCall); - final dynamic response = responses[methodCall.method]; - if (response != null && response is Exception) { - return Future.error('$response'); - } - return Future.value(response); - }); - googleSignIn = GoogleSignIn(); - log.clear(); + mockPlatform = MockGoogleSignInPlatform(); + when(mockPlatform.isMock).thenReturn(true); + when(mockPlatform.signInSilently()) + .thenAnswer((Invocation _) async => kDefaultUser); + when(mockPlatform.signIn()) + .thenAnswer((Invocation _) async => kDefaultUser); + + GoogleSignInPlatform.instance = mockPlatform; }); test('signInSilently', () async { + final GoogleSignIn googleSignIn = GoogleSignIn(); + await googleSignIn.signInSilently(); + expect(googleSignIn.currentUser, isNotNull); - expect( - log, - [ - _isSignInMethodCall(), - isMethodCall('signInSilently', arguments: null), - ], - ); + _verifyInit(mockPlatform); + verify(mockPlatform.signInSilently()); }); test('signIn', () async { + final GoogleSignIn googleSignIn = GoogleSignIn(); + await googleSignIn.signIn(); + expect(googleSignIn.currentUser, isNotNull); - expect( - log, - [ - _isSignInMethodCall(), - isMethodCall('signIn', arguments: null), - ], - ); + _verifyInit(mockPlatform); + verify(mockPlatform.signIn()); }); test('signIn prioritize clientId parameter when available', () async { const String fakeClientId = 'fakeClientId'; - googleSignIn = GoogleSignIn(clientId: fakeClientId); + final GoogleSignIn googleSignIn = GoogleSignIn(clientId: fakeClientId); + await googleSignIn.signIn(); - expect(googleSignIn.currentUser, isNotNull); - expect( - log, - [ - isMethodCall('init', arguments: { - 'signInOption': 'SignInOption.standard', - 'scopes': [], - 'hostedDomain': null, - 'clientId': fakeClientId, - 'forceCodeForRefreshToken': false, - }), - isMethodCall('signIn', arguments: null), - ], - ); + + _verifyInit(mockPlatform, clientId: fakeClientId); + verify(mockPlatform.signIn()); }); test('signOut', () async { + final GoogleSignIn googleSignIn = GoogleSignIn(); + await googleSignIn.signOut(); - expect(googleSignIn.currentUser, isNull); - expect(log, [ - _isSignInMethodCall(), - isMethodCall('signOut', arguments: null), - ]); + + _verifyInit(mockPlatform); + verify(mockPlatform.signOut()); }); test('disconnect; null response', () async { - await googleSignIn.disconnect(); - expect(googleSignIn.currentUser, isNull); - expect( - log, - [ - _isSignInMethodCall(), - isMethodCall('disconnect', arguments: null), - ], - ); - }); + final GoogleSignIn googleSignIn = GoogleSignIn(); - test('disconnect; empty response as on iOS', () async { - responses['disconnect'] = {}; await googleSignIn.disconnect(); + expect(googleSignIn.currentUser, isNull); - expect( - log, - [ - _isSignInMethodCall(), - isMethodCall('disconnect', arguments: null), - ], - ); + _verifyInit(mockPlatform); + verify(mockPlatform.disconnect()); }); test('isSignedIn', () async { + final GoogleSignIn googleSignIn = GoogleSignIn(); + when(mockPlatform.isSignedIn()).thenAnswer((Invocation _) async => true); + final bool result = await googleSignIn.isSignedIn(); + expect(result, isTrue); - expect(log, [ - _isSignInMethodCall(), - isMethodCall('isSignedIn', arguments: null), - ]); + _verifyInit(mockPlatform); + verify(mockPlatform.isSignedIn()); }); test('signIn works even if a previous call throws error in other zone', () async { - responses['signInSilently'] = Exception('Not a user'); + final GoogleSignIn googleSignIn = GoogleSignIn(); + + when(mockPlatform.signInSilently()).thenThrow(Exception('Not a user')); await runZonedGuarded(() async { expect(await googleSignIn.signInSilently(), isNull); }, (Object e, StackTrace st) {}); expect(await googleSignIn.signIn(), isNotNull); - expect( - log, - [ - _isSignInMethodCall(), - isMethodCall('signInSilently', arguments: null), - isMethodCall('signIn', arguments: null), - ], - ); + _verifyInit(mockPlatform); + verify(mockPlatform.signInSilently()); + verify(mockPlatform.signIn()); }); test('concurrent calls of the same method trigger sign in once', () async { + final GoogleSignIn googleSignIn = GoogleSignIn(); final List> futures = >[ googleSignIn.signInSilently(), googleSignIn.signInSilently(), ]; + expect(futures.first, isNot(futures.last), reason: 'Must return new Future'); + final List users = await Future.wait(futures); + expect(googleSignIn.currentUser, isNotNull); expect(users, [ googleSignIn.currentUser, googleSignIn.currentUser ]); - expect( - log, - [ - _isSignInMethodCall(), - isMethodCall('signInSilently', arguments: null), - ], - ); + _verifyInit(mockPlatform); + verify(mockPlatform.signInSilently()).called(1); }); test('can sign in after previously failed attempt', () async { - responses['signInSilently'] = Exception('Not a user'); + final GoogleSignIn googleSignIn = GoogleSignIn(); + when(mockPlatform.signInSilently()).thenThrow(Exception('Not a user')); + expect(await googleSignIn.signInSilently(), isNull); expect(await googleSignIn.signIn(), isNotNull); - expect( - log, - [ - _isSignInMethodCall(), - isMethodCall('signInSilently', arguments: null), - isMethodCall('signIn', arguments: null), - ], - ); + + _verifyInit(mockPlatform); + verify(mockPlatform.signInSilently()); + verify(mockPlatform.signIn()); }); test('concurrent calls of different signIn methods', () async { + final GoogleSignIn googleSignIn = GoogleSignIn(); final List> futures = >[ googleSignIn.signInSilently(), googleSignIn.signIn(), ]; expect(futures.first, isNot(futures.last)); + final List users = await Future.wait(futures); - expect( - log, - [ - _isSignInMethodCall(), - isMethodCall('signInSilently', arguments: null), - ], - ); + expect(users.first, users.last, reason: 'Must return the same user'); expect(googleSignIn.currentUser, users.last); + _verifyInit(mockPlatform); + verify(mockPlatform.signInSilently()); + verifyNever(mockPlatform.signIn()); }); test('can sign in after aborted flow', () async { - responses['signIn'] = null; + final GoogleSignIn googleSignIn = GoogleSignIn(); + + when(mockPlatform.signIn()).thenAnswer((Invocation _) async => null); expect(await googleSignIn.signIn(), isNull); - responses['signIn'] = kUserData; + + when(mockPlatform.signIn()) + .thenAnswer((Invocation _) async => kDefaultUser); expect(await googleSignIn.signIn(), isNotNull); }); test('signOut/disconnect methods always trigger native calls', () async { + final GoogleSignIn googleSignIn = GoogleSignIn(); final List> futures = >[ googleSignIn.signOut(), @@ -234,20 +181,16 @@ void main() { googleSignIn.disconnect(), googleSignIn.disconnect(), ]; + await Future.wait(futures); - expect( - log, - [ - _isSignInMethodCall(), - isMethodCall('signOut', arguments: null), - isMethodCall('signOut', arguments: null), - isMethodCall('disconnect', arguments: null), - isMethodCall('disconnect', arguments: null), - ], - ); + + _verifyInit(mockPlatform); + verify(mockPlatform.signOut()).called(2); + verify(mockPlatform.disconnect()).called(2); }); test('queue of many concurrent calls', () async { + final GoogleSignIn googleSignIn = GoogleSignIn(); final List> futures = >[ googleSignIn.signInSilently(), @@ -255,183 +198,146 @@ void main() { googleSignIn.signIn(), googleSignIn.disconnect(), ]; + await Future.wait(futures); - expect( - log, - [ - _isSignInMethodCall(), - isMethodCall('signInSilently', arguments: null), - isMethodCall('signOut', arguments: null), - isMethodCall('signIn', arguments: null), - isMethodCall('disconnect', arguments: null), - ], - ); + + _verifyInit(mockPlatform); + verifyInOrder([ + mockPlatform.signInSilently(), + mockPlatform.signOut(), + mockPlatform.signIn(), + mockPlatform.disconnect(), + ]); }); test('signInSilently suppresses errors by default', () async { - channel.setMockMethodCallHandler((MethodCall methodCall) { - throw 'I am an error'; - }); + final GoogleSignIn googleSignIn = GoogleSignIn(); + when(mockPlatform.signInSilently()).thenThrow(Exception('I am an error')); expect(await googleSignIn.signInSilently(), isNull); // should not throw }); - test('signInSilently forwards errors', () async { - channel.setMockMethodCallHandler((MethodCall methodCall) { - throw 'I am an error'; - }); + test('signInSilently forwards exceptions', () async { + final GoogleSignIn googleSignIn = GoogleSignIn(); + when(mockPlatform.signInSilently()).thenThrow(Exception('I am an error')); expect(googleSignIn.signInSilently(suppressErrors: false), - throwsA(isInstanceOf())); + throwsA(isInstanceOf())); }); test('signInSilently allows re-authentication to be requested', () async { + final GoogleSignIn googleSignIn = GoogleSignIn(); await googleSignIn.signInSilently(); expect(googleSignIn.currentUser, isNotNull); await googleSignIn.signInSilently(reAuthenticate: true); - expect( - log, - [ - _isSignInMethodCall(), - isMethodCall('signInSilently', arguments: null), - isMethodCall('signInSilently', arguments: null), - ], - ); + _verifyInit(mockPlatform); + verify(mockPlatform.signInSilently()).called(2); }); test('can sign in after init failed before', () async { - int initCount = 0; - channel.setMockMethodCallHandler((MethodCall methodCall) { - if (methodCall.method == 'init') { - initCount++; - if (initCount == 1) { - throw 'First init fails'; - } - } - return Future.value(responses[methodCall.method]); - }); - expect(googleSignIn.signIn(), throwsA(isInstanceOf())); + final GoogleSignIn googleSignIn = GoogleSignIn(); + + when(mockPlatform.init()).thenThrow(Exception('First init fails')); + expect(googleSignIn.signIn(), throwsA(isInstanceOf())); + + when(mockPlatform.init()).thenAnswer((Invocation _) async {}); expect(await googleSignIn.signIn(), isNotNull); }); test('created with standard factory uses correct options', () async { - googleSignIn = GoogleSignIn.standard(); + final GoogleSignIn googleSignIn = GoogleSignIn.standard(); await googleSignIn.signInSilently(); expect(googleSignIn.currentUser, isNotNull); - expect( - log, - [ - _isSignInMethodCall(), - isMethodCall('signInSilently', arguments: null), - ], - ); + _verifyInit(mockPlatform); + verify(mockPlatform.signInSilently()); }); test('created with defaultGamesSignIn factory uses correct options', () async { - googleSignIn = GoogleSignIn.games(); + final GoogleSignIn googleSignIn = GoogleSignIn.games(); await googleSignIn.signInSilently(); expect(googleSignIn.currentUser, isNotNull); - expect( - log, - [ - _isSignInMethodCall(signInOption: 'SignInOption.games'), - isMethodCall('signInSilently', arguments: null), - ], - ); + _verifyInit(mockPlatform, signInOption: SignInOption.games); + verify(mockPlatform.signInSilently()); }); test('authentication', () async { + final GoogleSignIn googleSignIn = GoogleSignIn(); + when(mockPlatform.getTokens( + email: anyNamed('email'), + shouldRecoverAuth: anyNamed('shouldRecoverAuth'))) + .thenAnswer((Invocation _) async => GoogleSignInTokenData( + idToken: '123', + accessToken: '456', + serverAuthCode: '789', + )); + await googleSignIn.signIn(); - log.clear(); final GoogleSignInAccount user = googleSignIn.currentUser!; final GoogleSignInAuthentication auth = await user.authentication; expect(auth.accessToken, '456'); expect(auth.idToken, '123'); - expect( - log, - [ - isMethodCall('getTokens', arguments: { - 'email': 'john.doe@gmail.com', - 'shouldRecoverAuth': true, - }), - ], - ); + verify(mockPlatform.getTokens( + email: 'john.doe@gmail.com', shouldRecoverAuth: true)); }); test('requestScopes returns true once new scope is granted', () async { + final GoogleSignIn googleSignIn = GoogleSignIn(); + when(mockPlatform.requestScopes(any)) + .thenAnswer((Invocation _) async => true); + await googleSignIn.signIn(); final bool result = await googleSignIn.requestScopes(['testScope']); expect(result, isTrue); - expect( - log, - [ - _isSignInMethodCall(), - isMethodCall('signIn', arguments: null), - isMethodCall('requestScopes', arguments: { - 'scopes': ['testScope'], - }), - ], - ); - }); - }); - - group('GoogleSignIn with fake backend', () { - const FakeUser kUserData = FakeUser( - id: '8162538176523816253123', - displayName: 'John Doe', - email: 'john.doe@gmail.com', - photoUrl: 'https://lh5.googleusercontent.com/photo.jpg', - serverAuthCode: '789'); - - late GoogleSignIn googleSignIn; - - setUp(() { - final MethodChannelGoogleSignIn platformInstance = - GoogleSignInPlatform.instance as MethodChannelGoogleSignIn; - platformInstance.channel.setMockMethodCallHandler( - (FakeSignInBackend()..user = kUserData).handleMethodCall); - googleSignIn = GoogleSignIn(); + _verifyInit(mockPlatform); + verify(mockPlatform.signIn()); + verify(mockPlatform.requestScopes(['testScope'])); }); test('user starts as null', () async { + final GoogleSignIn googleSignIn = GoogleSignIn(); expect(googleSignIn.currentUser, isNull); }); test('can sign in and sign out', () async { + final GoogleSignIn googleSignIn = GoogleSignIn(); await googleSignIn.signIn(); final GoogleSignInAccount user = googleSignIn.currentUser!; - expect(user.displayName, equals(kUserData.displayName)); - expect(user.email, equals(kUserData.email)); - expect(user.id, equals(kUserData.id)); - expect(user.photoUrl, equals(kUserData.photoUrl)); - expect(user.serverAuthCode, equals(kUserData.serverAuthCode)); + expect(user.displayName, equals(kDefaultUser.displayName)); + expect(user.email, equals(kDefaultUser.email)); + expect(user.id, equals(kDefaultUser.id)); + expect(user.photoUrl, equals(kDefaultUser.photoUrl)); + expect(user.serverAuthCode, equals(kDefaultUser.serverAuthCode)); await googleSignIn.disconnect(); expect(googleSignIn.currentUser, isNull); }); test('disconnect when signout already succeeds', () async { + final GoogleSignIn googleSignIn = GoogleSignIn(); await googleSignIn.disconnect(); expect(googleSignIn.currentUser, isNull); }); }); } -Matcher _isSignInMethodCall({String signInOption = 'SignInOption.standard'}) { - return isMethodCall('init', arguments: { - 'signInOption': signInOption, - 'scopes': [], - 'hostedDomain': null, - 'clientId': null, - 'forceCodeForRefreshToken': false, - }); +void _verifyInit( + MockGoogleSignInPlatform mockSignIn, { + SignInOption signInOption = SignInOption.standard, + String? clientId, +}) { + verify(mockSignIn.init( + signInOption: signInOption, + scopes: [], + hostedDomain: null, + clientId: clientId, + )); } diff --git a/packages/google_sign_in/google_sign_in/test/google_sign_in_test.mocks.dart b/packages/google_sign_in/google_sign_in/test/google_sign_in_test.mocks.dart new file mode 100644 index 000000000000..4e669628391c --- /dev/null +++ b/packages/google_sign_in/google_sign_in/test/google_sign_in_test.mocks.dart @@ -0,0 +1,100 @@ +// Mocks generated by Mockito 5.1.0 from annotations +// in google_sign_in/test/google_sign_in_test.dart. +// Do not manually edit this file. + +import 'dart:async' as _i4; + +import 'package:google_sign_in_platform_interface/google_sign_in_platform_interface.dart' + as _i3; +import 'package:google_sign_in_platform_interface/src/types.dart' as _i2; +import 'package:mockito/mockito.dart' as _i1; + +// ignore_for_file: type=lint +// ignore_for_file: avoid_redundant_argument_values +// ignore_for_file: avoid_setters_without_getters +// ignore_for_file: comment_references +// ignore_for_file: implementation_imports +// ignore_for_file: invalid_use_of_visible_for_testing_member +// ignore_for_file: prefer_const_constructors +// ignore_for_file: unnecessary_parenthesis +// ignore_for_file: camel_case_types + +class _FakeGoogleSignInTokenData_0 extends _i1.Fake + implements _i2.GoogleSignInTokenData {} + +/// A class which mocks [GoogleSignInPlatform]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockGoogleSignInPlatform extends _i1.Mock + implements _i3.GoogleSignInPlatform { + MockGoogleSignInPlatform() { + _i1.throwOnMissingStub(this); + } + + @override + bool get isMock => + (super.noSuchMethod(Invocation.getter(#isMock), returnValue: false) + as bool); + @override + _i4.Future init( + {List? scopes = const [], + _i2.SignInOption? signInOption = _i2.SignInOption.standard, + String? hostedDomain, + String? clientId}) => + (super.noSuchMethod( + Invocation.method(#init, [], { + #scopes: scopes, + #signInOption: signInOption, + #hostedDomain: hostedDomain, + #clientId: clientId + }), + returnValue: Future.value(), + returnValueForMissingStub: Future.value()) as _i4.Future); + @override + _i4.Future initWithParams(_i2.SignInInitParameters? params) => + (super.noSuchMethod(Invocation.method(#initWithParams, [params]), + returnValue: Future.value(), + returnValueForMissingStub: Future.value()) as _i4.Future); + @override + _i4.Future<_i2.GoogleSignInUserData?> signInSilently() => + (super.noSuchMethod(Invocation.method(#signInSilently, []), + returnValue: Future<_i2.GoogleSignInUserData?>.value()) + as _i4.Future<_i2.GoogleSignInUserData?>); + @override + _i4.Future<_i2.GoogleSignInUserData?> signIn() => + (super.noSuchMethod(Invocation.method(#signIn, []), + returnValue: Future<_i2.GoogleSignInUserData?>.value()) + as _i4.Future<_i2.GoogleSignInUserData?>); + @override + _i4.Future<_i2.GoogleSignInTokenData> getTokens( + {String? email, bool? shouldRecoverAuth}) => + (super.noSuchMethod( + Invocation.method(#getTokens, [], + {#email: email, #shouldRecoverAuth: shouldRecoverAuth}), + returnValue: Future<_i2.GoogleSignInTokenData>.value( + _FakeGoogleSignInTokenData_0())) + as _i4.Future<_i2.GoogleSignInTokenData>); + @override + _i4.Future signOut() => + (super.noSuchMethod(Invocation.method(#signOut, []), + returnValue: Future.value(), + returnValueForMissingStub: Future.value()) as _i4.Future); + @override + _i4.Future disconnect() => + (super.noSuchMethod(Invocation.method(#disconnect, []), + returnValue: Future.value(), + returnValueForMissingStub: Future.value()) as _i4.Future); + @override + _i4.Future isSignedIn() => + (super.noSuchMethod(Invocation.method(#isSignedIn, []), + returnValue: Future.value(false)) as _i4.Future); + @override + _i4.Future clearAuthCache({String? token}) => (super.noSuchMethod( + Invocation.method(#clearAuthCache, [], {#token: token}), + returnValue: Future.value(), + returnValueForMissingStub: Future.value()) as _i4.Future); + @override + _i4.Future requestScopes(List? scopes) => + (super.noSuchMethod(Invocation.method(#requestScopes, [scopes]), + returnValue: Future.value(false)) as _i4.Future); +}