summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAndroid Build Coastguard Worker <android-build-coastguard-worker@google.com>2023-04-12 02:06:35 +0000
committerAndroid Build Coastguard Worker <android-build-coastguard-worker@google.com>2023-04-12 02:06:35 +0000
commit07c744f15bbd1ef7a80822b11fd64f456d5815e9 (patch)
tree01fa5548c4008feb39394d711c2a27602ef487d9
parent5c8f4b36062d16607df9af6479964acccbed3088 (diff)
parenta7c5fc96d3c4b0353e282548b228ab6d490e2d40 (diff)
downloadtools-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
-rw-r--r--google-login-plugin/BUILD8
-rw-r--r--google-login-plugin/google-login-as.iml9
-rw-r--r--google-login-plugin/src/META-INF/google-login-plugin.xml2
-rw-r--r--google-login-plugin/src/com/google/gct/login/GoogleLogin.kt26
-rw-r--r--google-login-plugin/src/com/google/gct/login/GoogleLoginState.java110
-rw-r--r--google-login-plugin/src/com/google/gct/login/Interceptor.kt50
-rw-r--r--google-login-plugin/src/com/google/gct/login/OAuthScopeRegistry.java4
-rw-r--r--google-login-plugin/testSrc/com/google/gct/login/FakeOAuthDataStore.kt27
-rw-r--r--google-login-plugin/testSrc/com/google/gct/login/FakeUiFacade.kt37
-rw-r--r--google-login-plugin/testSrc/com/google/gct/login/GoogleLoginStateTest.kt115
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