diff options
author | Android Build Coastguard Worker <android-build-coastguard-worker@google.com> | 2023-04-12 02:06:35 +0000 |
---|---|---|
committer | Android Build Coastguard Worker <android-build-coastguard-worker@google.com> | 2023-04-12 02:06:35 +0000 |
commit | 07c744f15bbd1ef7a80822b11fd64f456d5815e9 (patch) | |
tree | 01fa5548c4008feb39394d711c2a27602ef487d9 | |
parent | 5c8f4b36062d16607df9af6479964acccbed3088 (diff) | |
parent | a7c5fc96d3c4b0353e282548b228ab6d490e2d40 (diff) | |
download | tools-studio-main.tar.gz |
Snap for 9922117 from a7c5fc96d3c4b0353e282548b228ab6d490e2d40 to studio-giraffe-releasestudio-2022.3.1-rc1studio-2022.3.1-beta2studio-2022.3.1studio-main
Change-Id: If901adf02d5100db433b968ca5e491ae97e7613a
10 files changed, 362 insertions, 26 deletions
diff --git a/google-login-plugin/BUILD b/google-login-plugin/BUILD index 0762e04..387d3cc 100644 --- a/google-login-plugin/BUILD +++ b/google-login-plugin/BUILD @@ -26,5 +26,13 @@ iml_module( "//prebuilts/tools/common/google-api-java-client/1.20.0:google-api-java-client", "//prebuilts/tools/common/m2:jsr305-2.0.1", "//tools/adt/idea/.idea/libraries:oauth2", + "//tools/base/flags:studio.android.sdktools.flags[module]", + "//tools/adt/idea/android-common:intellij.android.common[module]", + "//tools/adt/idea/.idea/libraries:studio-grpc", + "//tools/adt/idea/.idea/libraries:truth[test]", + "//tools/analytics-library/tracker:analytics-tracker[module]", + "//tools/analytics-library/testing:android.sdktools.analytics-testing[module, test]", + "//tools/base/testutils:studio.android.sdktools.testutils[module, test]", + "//tools/adt/idea/.idea/libraries:studio-analytics-proto", ], ) diff --git a/google-login-plugin/google-login-as.iml b/google-login-plugin/google-login-as.iml index cfb91e3..43b9c26 100644 --- a/google-login-plugin/google-login-as.iml +++ b/google-login-plugin/google-login-as.iml @@ -40,5 +40,14 @@ </library> </orderEntry> <orderEntry type="library" name="oauth2" level="project" /> + <orderEntry type="module" module-name="android.sdktools.flags" /> + <orderEntry type="module" module-name="intellij.android.common" /> + <orderEntry type="library" name="studio-grpc" level="project" /> + <orderEntry type="library" scope="TEST" name="truth" level="project" /> + <orderEntry type="module" module-name="analytics-tracker" scope="TEST" /> + <orderEntry type="module" module-name="android.sdktools.analytics-testing" scope="TEST" /> + <orderEntry type="module" module-name="android.sdktools.testutils" scope="TEST" /> + <orderEntry type="library" name="studio-analytics-proto" level="project" /> + <orderEntry type="module" module-name="analytics-tracker" /> </component> </module>
\ No newline at end of file diff --git a/google-login-plugin/src/META-INF/google-login-plugin.xml b/google-login-plugin/src/META-INF/google-login-plugin.xml index 96af048..f02c175 100644 --- a/google-login-plugin/src/META-INF/google-login-plugin.xml +++ b/google-login-plugin/src/META-INF/google-login-plugin.xml @@ -29,6 +29,8 @@ serviceInterface="com.google.gct.login.GoogleLogin" serviceImplementation="com.google.gct.login.GoogleLoginImpl" /> <iconMapper mappingFile="GoogleLoginIntUiIconMappings.json"/> + + <notificationGroup id="Google Login" displayType="BALLOON" /> </extensions> <extensionPoints> diff --git a/google-login-plugin/src/com/google/gct/login/GoogleLogin.kt b/google-login-plugin/src/com/google/gct/login/GoogleLogin.kt index 9bf57ff..35ac62c 100644 --- a/google-login-plugin/src/com/google/gct/login/GoogleLogin.kt +++ b/google-login-plugin/src/com/google/gct/login/GoogleLogin.kt @@ -15,12 +15,14 @@ */ package com.google.gct.login +import com.android.tools.idea.io.grpc.ClientInterceptor import com.google.api.client.auth.oauth2.AuthorizationCodeRequestUrl import com.google.api.client.googleapis.auth.oauth2.GoogleOAuthConstants import com.google.gct.login.common.OAuthData import com.google.gct.login.common.OAuthDataStore import com.google.gct.login.common.UiFacade import com.google.gct.login.common.VerificationCodeHolder +import com.google.wireless.android.sdk.stats.GoogleLoginPluginEvent import com.intellij.ide.BrowserUtil import com.intellij.ide.DataManager import com.intellij.openapi.actionSystem.CommonDataKeys @@ -79,10 +81,22 @@ interface GoogleLogin { fun getCredential() = activeUser?.credential fun isConnected() = activeUser?.googleLoginState?.isConnected == true fun getEmail() = activeUser?.googleLoginState?.email + + /** + * Returns the oAuth2 token associated with the active user. + * + * Fetches a new token if one does not exist or is expired. + */ @Throws(IOException::class) - fun fetchOAuth2Token() = activeUser?.googleLoginState?.fetchOAuth2Token() + fun fetchOAuth2Token(): String? = activeUser?.googleLoginState?.fetchAccessToken() + fun createRequestFactory() = activeUser?.googleLoginState?.createRequestFactory(null) + /** + * Gets a gRpc interceptor that will insert the active user's authorization header. + */ + fun getActiveUserAuthInterceptor(): ClientInterceptor = Interceptor { fetchOAuth2Token() } + companion object { @JvmStatic val instance: GoogleLogin @@ -111,7 +125,12 @@ class GoogleLoginImpl : GoogleLogin { LOG.info("Added fake Google user: $user") users.addUser(CredentialedUser(user)) } - ?: run { dataStore.initializeUsers() } + ?: run { + dataStore.initializeUsers() + if (isLoggedIn) { + GoogleLoginState.trackEvent(GoogleLoginPluginEvent.EventKind.LOGGED_IN_ON_STUDIO_START) + } + } } } @@ -222,7 +241,8 @@ class GoogleLoginImpl : GoogleLogin { clientInfo.info, OAuthScopeRegistry.getScopes(), AndroidPreferencesOAuthDataStore(), - uiFacade + uiFacade, + !initializingUsers // Don't track login changes if the users are being initialized. ) return if (initializingUsers && !state.isLoggedIn) { // Logs user out if oauth scope for active user's credentials diff --git a/google-login-plugin/src/com/google/gct/login/GoogleLoginState.java b/google-login-plugin/src/com/google/gct/login/GoogleLoginState.java index 7fcc689..df9879a 100644 --- a/google-login-plugin/src/com/google/gct/login/GoogleLoginState.java +++ b/google-login-plugin/src/com/google/gct/login/GoogleLoginState.java @@ -15,7 +15,9 @@ */ package com.google.gct.login; +import com.android.tools.analytics.UsageTracker; import com.google.api.client.auth.oauth2.Credential; +import com.google.api.client.auth.oauth2.TokenResponseException; import com.google.api.client.googleapis.auth.oauth2.GoogleAuthorizationCodeTokenRequest; import com.google.api.client.googleapis.auth.oauth2.GoogleCredential; import com.google.api.client.googleapis.auth.oauth2.GoogleRefreshTokenRequest; @@ -36,6 +38,11 @@ import com.google.gct.login.common.OAuthDataStore; import com.google.gct.login.common.UiFacade; import com.google.gct.login.common.VerificationCodeHolder; import com.google.gson.Gson; +import com.google.wireless.android.sdk.stats.AndroidStudioEvent; +import com.google.wireless.android.sdk.stats.GoogleLoginPluginEvent; +import com.intellij.notification.NotificationAction; +import com.intellij.notification.NotificationGroup; +import com.intellij.notification.NotificationType; import com.intellij.openapi.diagnostic.Logger; import java.io.IOException; import java.io.InputStreamReader; @@ -44,8 +51,10 @@ import java.nio.charset.StandardCharsets; import java.util.Collection; import java.util.GregorianCalendar; import java.util.SortedSet; +import java.util.function.Function; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; +import org.jetbrains.annotations.VisibleForTesting; /** * Provides methods for logging into and out of Google services via OAuth 2.0, and for fetching @@ -59,6 +68,9 @@ import org.jetbrains.annotations.Nullable; public class GoogleLoginState { private static final String GET_EMAIL_URL = "https://openidconnect.googleapis.com/v1/userinfo"; + private static final String EXPIRED_OR_REVOKED_MESSAGE = "Token has been expired or revoked."; + private static final NotificationGroup notificationGroup = NotificationGroup.findRegisteredGroup("Google Login"); + private static boolean isNotificationShowing = false; private static final JsonFactory jsonFactory = new JacksonFactory(); private static final HttpTransport transport = new NetHttpTransport(); @@ -76,10 +88,26 @@ public class GoogleLoginState { private boolean isLoggedIn; private String email; private boolean connected; // whether we connected to the internet + private final Function<VerificationCodeHolder, GoogleAuthorizationCodeTokenRequest> authCodeTokenRequestFactory; private final Collection<LoginListener> listeners; /** + * Logs the login state change event + * @param eventKind - GoogleLoginPluginEvent.EventKind that is to be logged + */ + static void trackEvent(GoogleLoginPluginEvent.EventKind eventKind) { + UsageTracker.log( + AndroidStudioEvent.newBuilder() + .setKind(AndroidStudioEvent.EventKind.GOOGLE_LOGIN_EVENT) + .setGoogleLoginEvent( + GoogleLoginPluginEvent.newBuilder().setEvent(eventKind).build() + ) + ); + } + + + /** * Construct a new platform-specific {@code GoogleLoginState} for a specified client application * and specified authorization scopes. * @@ -92,7 +120,26 @@ public class GoogleLoginState { */ public GoogleLoginState( String clientId, String clientSecret, SortedSet<String> oAuthScopes, - OAuthDataStore authDataStore, UiFacade uiFacade) { + OAuthDataStore authDataStore, UiFacade uiFacade, boolean trackLogin) { + this(clientId, clientSecret, oAuthScopes, + authDataStore, uiFacade, trackLogin, + (verificationCodeHolder) -> new GoogleAuthorizationCodeTokenRequest( + transport, + jsonFactory, + clientId, + clientSecret, + verificationCodeHolder.getVerificationCode(), + verificationCodeHolder.getRedirectUrl() + ) + ); + } + + @VisibleForTesting + public GoogleLoginState( + String clientId, String clientSecret, SortedSet<String> oAuthScopes, + OAuthDataStore authDataStore, UiFacade uiFacade, boolean trackLogin, + Function<VerificationCodeHolder, GoogleAuthorizationCodeTokenRequest> authCodeTokenRequestFactory + ) { this.clientId = clientId; this.clientSecret = clientSecret; this.oAuthScopes = oAuthScopes; @@ -101,10 +148,11 @@ public class GoogleLoginState { this.isLoggedIn = false; this.email = ""; + this.authCodeTokenRequestFactory = authCodeTokenRequestFactory; connected = true; // assume we're connected until checkCredentials is called listeners = Lists.newLinkedList(); - retrieveSavedCredentials(); + retrieveSavedCredentials(trackLogin); } /** @@ -149,8 +197,8 @@ public class GoogleLoginState { * @throws IOException if something goes wrong while fetching the token. * */ - public String fetchAccessToken() throws IOException { - if (!checkLoggedIn(null)) { + public synchronized String fetchAccessToken() throws IOException { + if (!isLoggedIn) { return null; } if (accessTokenExpiryTime != 0) { @@ -189,14 +237,11 @@ public class GoogleLoginState { * Makes a request to get an OAuth2 access token from the OAuth2 refresh * token. This token is short lived. * - * @return an OAuth2 token, or null if there was an error or if the user - * wasn't signed in and canceled signing in. * @throws IOException if something goes wrong while fetching the token. - * */ - public String fetchOAuth2Token() throws IOException { + private void fetchOAuth2Token() throws IOException { if (!checkLoggedIn(null)) { - return null; + return; } try { @@ -207,12 +252,33 @@ public class GoogleLoginState { oAuth2Credential.setAccessToken(accessToken); accessTokenExpiryTime = new GregorianCalendar().getTimeInMillis() / 1000 + authResponse.getExpiresInSeconds(); + } catch (TokenResponseException e) { + if (e.getDetails().getErrorDescription().equals(EXPIRED_OR_REVOKED_MESSAGE)) { + getLogger().warn(EXPIRED_OR_REVOKED_MESSAGE, e); + if (!isNotificationShowing) { + isNotificationShowing = true; + // TODO(b/275738836): Use FORCE_LOGOUT to represent this logout action. + GoogleLogin.getInstance().logOut(false); + notificationGroup.createNotification( + "Authentication error", + "Your session has expired. Please login again", + NotificationType.WARNING + ).addAction( + NotificationAction.createExpiring( + "Login...", + (action, notification) -> GoogleLogin.getInstance().logIn() + ) + ).whenExpired( + () -> isNotificationShowing = false + ).notify(null); + } + throw e; + } } catch (IOException e) { getLogger().warn("Could not obtain an OAuth2 access token.", e); throw e; } saveCredentials(); - return accessToken; } public Credential getCredential() { @@ -276,14 +342,7 @@ public class GoogleLoginState { return false; } - GoogleAuthorizationCodeTokenRequest authRequest = - new GoogleAuthorizationCodeTokenRequest( - transport, - jsonFactory, - clientId, - clientSecret, - verificationCodeHolder.getVerificationCode(), - verificationCodeHolder.getRedirectUrl()); + GoogleAuthorizationCodeTokenRequest authRequest = authCodeTokenRequestFactory.apply(verificationCodeHolder); GoogleTokenResponse authResponse; try { authResponse = authRequest.execute(); @@ -295,6 +354,7 @@ public class GoogleLoginState { return false; } isLoggedIn = true; + trackEvent(GoogleLoginPluginEvent.EventKind.LOGIN_WITH_SUCCESS); updateUserCredentials(authResponse); return true; } @@ -320,7 +380,7 @@ public class GoogleLoginState { if (logOut) { email = ""; isLoggedIn = false; - + trackEvent(GoogleLoginPluginEvent.EventKind.LOGOUT_WITH_SUCCESS); authDataStore.clearStoredOAuthData(); notifyLoginStatusChange(false); @@ -348,13 +408,13 @@ public class GoogleLoginState { oAuth2Credential = makeCredential(); accessTokenExpiryTime = System.currentTimeMillis() / 1000 + tokenResponse.getExpiresInSeconds(); - email = queryEmail(); + email = queryEmail(createRequestFactory(null)); saveCredentials(); uiFacade.notifyStatusIndicator(); notifyLoginStatusChange(true); } - private void retrieveSavedCredentials() { + private void retrieveSavedCredentials(boolean trackLogin) { OAuthData savedAuthState = authDataStore.loadOAuthData(); @@ -372,6 +432,9 @@ public class GoogleLoginState { this.email = savedAuthState.getStoredEmail(); isLoggedIn = true; + if (trackLogin) { + trackEvent(GoogleLoginPluginEvent.EventKind.LOGIN_WITH_SUCCESS); + } if (!oAuthScopes.equals(savedAuthState.getStoredScopes())) { getLogger().warn( @@ -406,12 +469,13 @@ public class GoogleLoginState { } } - private String queryEmail() { + @VisibleForTesting + protected static String queryEmail(HttpRequestFactory requestFactory) { String url = GET_EMAIL_URL; HttpResponse resp; try { - HttpRequest get = createRequestFactory(null).buildGetRequest(new GenericUrl(url)); + HttpRequest get = requestFactory.buildGetRequest(new GenericUrl(url)); resp = get.execute(); Type responseData = new TypeToken<LoginResponseHolder>() {}.getType(); LoginResponseHolder loginResponseHolder = diff --git a/google-login-plugin/src/com/google/gct/login/Interceptor.kt b/google-login-plugin/src/com/google/gct/login/Interceptor.kt new file mode 100644 index 0000000..6ca762d --- /dev/null +++ b/google-login-plugin/src/com/google/gct/login/Interceptor.kt @@ -0,0 +1,50 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.gct.login + +import com.android.tools.idea.io.grpc.CallOptions +import com.android.tools.idea.io.grpc.Channel +import com.android.tools.idea.io.grpc.ClientCall +import com.android.tools.idea.io.grpc.ClientInterceptor +import com.android.tools.idea.io.grpc.ForwardingClientCall +import com.android.tools.idea.io.grpc.Metadata +import com.android.tools.idea.io.grpc.MethodDescriptor + + +private val AUTHORIZATION_HEADER = + Metadata.Key.of("authorization", Metadata.ASCII_STRING_MARSHALLER) + +internal class Interceptor(private val authTokenFetcher: () -> String?) : ClientInterceptor { + override fun <ReqT, RespT> interceptCall( + method: MethodDescriptor<ReqT, RespT>, + callOptions: CallOptions?, + next: Channel + ): ClientCall<ReqT, RespT> { + var call = next.newCall(method, callOptions) + call = + object : ForwardingClientCall.SimpleForwardingClientCall<ReqT, RespT>(call) { + override fun start(responseListener: Listener<RespT>, headers: Metadata) { + authTokenFetcher() + ?.takeUnless { it.isEmpty() } + ?.let { headers.put(AUTHORIZATION_HEADER, "Bearer $it") } + + super.start(responseListener, headers) + } + } + + return call + } +} diff --git a/google-login-plugin/src/com/google/gct/login/OAuthScopeRegistry.java b/google-login-plugin/src/com/google/gct/login/OAuthScopeRegistry.java index 941261c..564e74a 100644 --- a/google-login-plugin/src/com/google/gct/login/OAuthScopeRegistry.java +++ b/google-login-plugin/src/com/google/gct/login/OAuthScopeRegistry.java @@ -15,6 +15,7 @@ */ package com.google.gct.login; +import com.android.tools.idea.flags.StudioFlags; import org.jetbrains.annotations.NotNull; import java.util.Collections; @@ -34,6 +35,9 @@ class OAuthScopeRegistry { scopes.add("https://www.googleapis.com/auth/cloud-platform"); scopes.add("https://www.googleapis.com/auth/firebase"); scopes.add("https://www.googleapis.com/auth/actions.builder"); + if (StudioFlags.PLAY_VITALS_ENABLED.get()) { + scopes.add("https://www.googleapis.com/auth/playdeveloperreporting"); + } SCOPES = Collections.unmodifiableSortedSet(scopes); } diff --git a/google-login-plugin/testSrc/com/google/gct/login/FakeOAuthDataStore.kt b/google-login-plugin/testSrc/com/google/gct/login/FakeOAuthDataStore.kt new file mode 100644 index 0000000..e9e6ef4 --- /dev/null +++ b/google-login-plugin/testSrc/com/google/gct/login/FakeOAuthDataStore.kt @@ -0,0 +1,27 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.gct.login + +import com.google.gct.login.common.OAuthData +import com.google.gct.login.common.OAuthDataStore + +class FakeOAuthDataStore(private val oauthData: OAuthData): OAuthDataStore { + override fun saveOAuthData(credentials: OAuthData?) = Unit + + override fun loadOAuthData(): OAuthData = oauthData + + override fun clearStoredOAuthData() = Unit +}
\ No newline at end of file diff --git a/google-login-plugin/testSrc/com/google/gct/login/FakeUiFacade.kt b/google-login-plugin/testSrc/com/google/gct/login/FakeUiFacade.kt new file mode 100644 index 0000000..a8e2438 --- /dev/null +++ b/google-login-plugin/testSrc/com/google/gct/login/FakeUiFacade.kt @@ -0,0 +1,37 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.gct.login + +import com.google.gct.login.common.UiFacade +import com.google.gct.login.common.VerificationCodeHolder + +class FakeUiFacade: UiFacade { + + private val verificationCodeHolder = VerificationCodeHolder("code", "url") + override fun obtainVerificationCodeFromExternalUserInteraction(title: String?): VerificationCodeHolder { + return verificationCodeHolder + } + + override fun showErrorDialog(title: String?, message: String?) { + TODO("Not yet implemented") + } + + override fun askYesOrNo(title: String?, message: String?): Boolean { + TODO("Not yet implemented") + } + + override fun notifyStatusIndicator() = Unit +}
\ No newline at end of file diff --git a/google-login-plugin/testSrc/com/google/gct/login/GoogleLoginStateTest.kt b/google-login-plugin/testSrc/com/google/gct/login/GoogleLoginStateTest.kt new file mode 100644 index 0000000..f77942b --- /dev/null +++ b/google-login-plugin/testSrc/com/google/gct/login/GoogleLoginStateTest.kt @@ -0,0 +1,115 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.gct.login + +import com.android.testutils.MockitoKt.any +import com.android.testutils.MockitoKt.mock +import com.android.testutils.MockitoKt.whenever +import com.android.testutils.VirtualTimeScheduler +import com.android.tools.analytics.TestUsageTracker +import com.android.tools.analytics.UsageTracker +import com.google.api.client.googleapis.auth.oauth2.GoogleAuthorizationCodeTokenRequest +import com.google.api.client.googleapis.auth.oauth2.GoogleTokenResponse +import com.google.common.truth.Truth.assertThat +import com.google.gct.login.common.OAuthData +import com.google.wireless.android.sdk.stats.AndroidStudioEvent +import com.google.wireless.android.sdk.stats.GoogleLoginPluginEvent +import org.junit.After +import org.junit.Before +import org.junit.Test +import org.mockito.Mockito.mockStatic +import org.mockito.Mockito.doReturn + +class GoogleLoginStateTest { + private val tracker = TestUsageTracker(VirtualTimeScheduler()) + private val fakeUiFacade = FakeUiFacade() + private val googleTokenResponse = GoogleTokenResponse().apply { + refreshToken = "refreshToken" + accessToken = "accessToken" + expiresInSeconds = 100 + } + private val mockGoogleAuthorizationCodeTokenRequest: GoogleAuthorizationCodeTokenRequest = mock() + // Mock GoogleLoginState to mock response for queryEmail(). + // This is not used for testing. + private val mockGoogleLoginState = mockStatic(GoogleLoginState::class.java) + + @Before + fun setUp() { + UsageTracker.setWriterForTest(tracker) + doReturn(googleTokenResponse).whenever(mockGoogleAuthorizationCodeTokenRequest).execute() + mockGoogleLoginState.whenever<Any> { GoogleLoginState.queryEmail(any()) }.thenAnswer { "test_user@gmail.com" } + mockGoogleLoginState.whenever<Any> { GoogleLoginState.trackEvent(any()) }.thenCallRealMethod() + } + + @After + fun tearDown() { + mockGoogleLoginState.close() + } + + @Test + fun metricsNotLoggedWhenUserLoggedInWhenInitializing() { + val googleLoginState = GoogleLoginState("", "", sortedSetOf(), + getOAuthDataStore("accessToken", "refreshToken"), + fakeUiFacade, false) { _ -> mockGoogleAuthorizationCodeTokenRequest } + assertThat(googleLoginState.isLoggedIn).isTrue() + + assertThat(tracker.usages).isEmpty() + } + + @Test + fun metricsLoggedWhenUserLoggedInWhenNotInitializing() { + val googleLoginState = GoogleLoginState("", "", sortedSetOf(), + getOAuthDataStore("accessToken", "refreshToken"), + fakeUiFacade, true) { _ -> mockGoogleAuthorizationCodeTokenRequest } + assertThat(googleLoginState.isLoggedIn).isTrue() + + assertThat(tracker.usages.size).isEqualTo(1) + val studioEvent = tracker.usages[0].studioEvent + studioEvent.checkStudioEvent(GoogleLoginPluginEvent.EventKind.LOGIN_WITH_SUCCESS) + } + + @Test + fun metricsLoggedWhenUserLogInStateChanges() { + val googleLoginState = GoogleLoginState("", "", sortedSetOf(), getOAuthDataStore(), + fakeUiFacade, false) { _ -> mockGoogleAuthorizationCodeTokenRequest } + assertThat(googleLoginState.isLoggedIn).isFalse() + + googleLoginState.logIn("") + assertThat(googleLoginState.isLoggedIn).isTrue() + + val loginUsage = tracker.usages + assertThat(loginUsage.size).isEqualTo(1) + val loginStudioEvent = loginUsage[0].studioEvent + loginStudioEvent.checkStudioEvent(GoogleLoginPluginEvent.EventKind.LOGIN_WITH_SUCCESS) + + tracker.usages.clear() + + googleLoginState.logOut(false) + + val logoutUsage = tracker.usages + assertThat(logoutUsage.size).isEqualTo(1) + val logoutStudioEvent = logoutUsage[0].studioEvent + logoutStudioEvent.checkStudioEvent(GoogleLoginPluginEvent.EventKind.LOGOUT_WITH_SUCCESS) + } + + private fun AndroidStudioEvent.checkStudioEvent(event: GoogleLoginPluginEvent.EventKind) { + assertThat(kind).isEqualTo(AndroidStudioEvent.EventKind.GOOGLE_LOGIN_EVENT) + assertThat(googleLoginEvent.event).isEqualTo(event) + } + + private fun getOAuthDataStore(accessToken: String? = null, refreshToken: String? = null) = + FakeOAuthDataStore(OAuthData(accessToken, refreshToken, null, setOf(), 0)) +}
\ No newline at end of file |