blob: 119ee50a383bac264b0caa5c830e4b38f740e78c [file] [log] [blame]
// Copyright 2013 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
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';
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
group('GoogleSignIn', () {
const MethodChannel channel = MethodChannel(
'plugins.flutter.io/google_sign_in',
);
const Map<String, String> kUserData = <String, String>{
'email': 'john.doe@gmail.com',
'id': '8162538176523816253123',
'photoUrl': 'https://lh5.googleusercontent.com/photo.jpg',
'displayName': 'John Doe',
'serverAuthCode': '789'
};
const Map<String, dynamic> kDefaultResponses = <String, dynamic>{
'init': null,
'signInSilently': kUserData,
'signIn': kUserData,
'signOut': null,
'disconnect': null,
'isSignedIn': true,
'requestScopes': true,
'getTokens': <dynamic, dynamic>{
'idToken': '123',
'accessToken': '456',
'serverAuthCode': '789',
},
};
final List<MethodCall> log = <MethodCall>[];
late Map<String, dynamic> responses;
late GoogleSignIn googleSignIn;
setUp(() {
responses = Map<String, dynamic>.from(kDefaultResponses);
channel.setMockMethodCallHandler((MethodCall methodCall) {
log.add(methodCall);
final dynamic response = responses[methodCall.method];
if (response != null && response is Exception) {
return Future<dynamic>.error('$response');
}
return Future<dynamic>.value(response);
});
googleSignIn = GoogleSignIn();
log.clear();
});
test('signInSilently', () async {
await googleSignIn.signInSilently();
expect(googleSignIn.currentUser, isNotNull);
expect(
log,
<Matcher>[
_isSignInMethodCall(),
isMethodCall('signInSilently', arguments: null),
],
);
});
test('signIn', () async {
await googleSignIn.signIn();
expect(googleSignIn.currentUser, isNotNull);
expect(
log,
<Matcher>[
_isSignInMethodCall(),
isMethodCall('signIn', arguments: null),
],
);
});
test('signIn prioritize clientId parameter when available', () async {
const String fakeClientId = 'fakeClientId';
googleSignIn = GoogleSignIn(clientId: fakeClientId);
await googleSignIn.signIn();
expect(googleSignIn.currentUser, isNotNull);
expect(
log,
<Matcher>[
isMethodCall('init', arguments: <String, dynamic>{
'signInOption': 'SignInOption.standard',
'scopes': <String>[],
'hostedDomain': null,
'clientId': fakeClientId,
}),
isMethodCall('signIn', arguments: null),
],
);
});
test('signOut', () async {
await googleSignIn.signOut();
expect(googleSignIn.currentUser, isNull);
expect(log, <Matcher>[
_isSignInMethodCall(),
isMethodCall('signOut', arguments: null),
]);
});
test('disconnect; null response', () async {
await googleSignIn.disconnect();
expect(googleSignIn.currentUser, isNull);
expect(
log,
<Matcher>[
_isSignInMethodCall(),
isMethodCall('disconnect', arguments: null),
],
);
});
test('disconnect; empty response as on iOS', () async {
responses['disconnect'] = <String, dynamic>{};
await googleSignIn.disconnect();
expect(googleSignIn.currentUser, isNull);
expect(
log,
<Matcher>[
_isSignInMethodCall(),
isMethodCall('disconnect', arguments: null),
],
);
});
test('isSignedIn', () async {
final bool result = await googleSignIn.isSignedIn();
expect(result, isTrue);
expect(log, <Matcher>[
_isSignInMethodCall(),
isMethodCall('isSignedIn', arguments: null),
]);
});
test('signIn works even if a previous call throws error in other zone',
() async {
responses['signInSilently'] = Exception('Not a user');
await runZonedGuarded(() async {
expect(await googleSignIn.signInSilently(), isNull);
}, (Object e, StackTrace st) {});
expect(await googleSignIn.signIn(), isNotNull);
expect(
log,
<Matcher>[
_isSignInMethodCall(),
isMethodCall('signInSilently', arguments: null),
isMethodCall('signIn', arguments: null),
],
);
});
test('concurrent calls of the same method trigger sign in once', () async {
final List<Future<GoogleSignInAccount?>> futures =
<Future<GoogleSignInAccount?>>[
googleSignIn.signInSilently(),
googleSignIn.signInSilently(),
];
expect(futures.first, isNot(futures.last),
reason: 'Must return new Future');
final List<GoogleSignInAccount?> users = await Future.wait(futures);
expect(googleSignIn.currentUser, isNotNull);
expect(users, <GoogleSignInAccount?>[
googleSignIn.currentUser,
googleSignIn.currentUser
]);
expect(
log,
<Matcher>[
_isSignInMethodCall(),
isMethodCall('signInSilently', arguments: null),
],
);
});
test('can sign in after previously failed attempt', () async {
responses['signInSilently'] = Exception('Not a user');
expect(await googleSignIn.signInSilently(), isNull);
expect(await googleSignIn.signIn(), isNotNull);
expect(
log,
<Matcher>[
_isSignInMethodCall(),
isMethodCall('signInSilently', arguments: null),
isMethodCall('signIn', arguments: null),
],
);
});
test('concurrent calls of different signIn methods', () async {
final List<Future<GoogleSignInAccount?>> futures =
<Future<GoogleSignInAccount?>>[
googleSignIn.signInSilently(),
googleSignIn.signIn(),
];
expect(futures.first, isNot(futures.last));
final List<GoogleSignInAccount?> users = await Future.wait(futures);
expect(
log,
<Matcher>[
_isSignInMethodCall(),
isMethodCall('signInSilently', arguments: null),
],
);
expect(users.first, users.last, reason: 'Must return the same user');
expect(googleSignIn.currentUser, users.last);
});
test('can sign in after aborted flow', () async {
responses['signIn'] = null;
expect(await googleSignIn.signIn(), isNull);
responses['signIn'] = kUserData;
expect(await googleSignIn.signIn(), isNotNull);
});
test('signOut/disconnect methods always trigger native calls', () async {
final List<Future<GoogleSignInAccount?>> futures =
<Future<GoogleSignInAccount?>>[
googleSignIn.signOut(),
googleSignIn.signOut(),
googleSignIn.disconnect(),
googleSignIn.disconnect(),
];
await Future.wait(futures);
expect(
log,
<Matcher>[
_isSignInMethodCall(),
isMethodCall('signOut', arguments: null),
isMethodCall('signOut', arguments: null),
isMethodCall('disconnect', arguments: null),
isMethodCall('disconnect', arguments: null),
],
);
});
test('queue of many concurrent calls', () async {
final List<Future<GoogleSignInAccount?>> futures =
<Future<GoogleSignInAccount?>>[
googleSignIn.signInSilently(),
googleSignIn.signOut(),
googleSignIn.signIn(),
googleSignIn.disconnect(),
];
await Future.wait(futures);
expect(
log,
<Matcher>[
_isSignInMethodCall(),
isMethodCall('signInSilently', arguments: null),
isMethodCall('signOut', arguments: null),
isMethodCall('signIn', arguments: null),
isMethodCall('disconnect', arguments: null),
],
);
});
test('signInSilently suppresses errors by default', () async {
channel.setMockMethodCallHandler((MethodCall methodCall) {
throw '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';
});
expect(googleSignIn.signInSilently(suppressErrors: false),
throwsA(isInstanceOf<PlatformException>()));
});
test('signInSilently allows re-authentication to be requested', () async {
await googleSignIn.signInSilently();
expect(googleSignIn.currentUser, isNotNull);
await googleSignIn.signInSilently(reAuthenticate: true);
expect(
log,
<Matcher>[
_isSignInMethodCall(),
isMethodCall('signInSilently', arguments: null),
isMethodCall('signInSilently', arguments: null),
],
);
});
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<dynamic>.value(responses[methodCall.method]);
});
expect(googleSignIn.signIn(), throwsA(isInstanceOf<PlatformException>()));
expect(await googleSignIn.signIn(), isNotNull);
});
test('created with standard factory uses correct options', () async {
googleSignIn = GoogleSignIn.standard();
await googleSignIn.signInSilently();
expect(googleSignIn.currentUser, isNotNull);
expect(
log,
<Matcher>[
_isSignInMethodCall(),
isMethodCall('signInSilently', arguments: null),
],
);
});
test('created with defaultGamesSignIn factory uses correct options',
() async {
googleSignIn = GoogleSignIn.games();
await googleSignIn.signInSilently();
expect(googleSignIn.currentUser, isNotNull);
expect(
log,
<Matcher>[
_isSignInMethodCall(signInOption: 'SignInOption.games'),
isMethodCall('signInSilently', arguments: null),
],
);
});
test('authentication', () async {
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,
<Matcher>[
isMethodCall('getTokens', arguments: <String, dynamic>{
'email': 'john.doe@gmail.com',
'shouldRecoverAuth': true,
}),
],
);
});
test('requestScopes returns true once new scope is granted', () async {
await googleSignIn.signIn();
final bool result =
await googleSignIn.requestScopes(<String>['testScope']);
expect(result, isTrue);
expect(
log,
<Matcher>[
_isSignInMethodCall(),
isMethodCall('signIn', arguments: null),
isMethodCall('requestScopes', arguments: <String, dynamic>{
'scopes': <String>['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();
});
test('user starts as null', () async {
expect(googleSignIn.currentUser, isNull);
});
test('can sign in and sign out', () async {
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));
await googleSignIn.disconnect();
expect(googleSignIn.currentUser, isNull);
});
test('disconnect when signout already succeeds', () async {
await googleSignIn.disconnect();
expect(googleSignIn.currentUser, isNull);
});
});
}
Matcher _isSignInMethodCall({String signInOption = 'SignInOption.standard'}) {
return isMethodCall('init', arguments: <String, dynamic>{
'signInOption': signInOption,
'scopes': <String>[],
'hostedDomain': null,
'clientId': null,
});
}