blob: cc57417b4672fdc66b1fd487847ded52d416707c [file] [log] [blame]
/*
* 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.directaccess.ui.actions
import com.android.adblib.testingutils.CoroutineTestUtils.runBlockingWithTimeout
import com.android.adblib.testingutils.CoroutineTestUtils.yieldUntil
import com.android.flags.junit.FlagRule
import com.android.sdklib.deviceprovisioner.DeviceHandle
import com.android.sdklib.deviceprovisioner.DeviceProvisioner
import com.android.sdklib.deviceprovisioner.DeviceState
import com.android.testutils.MockitoKt.mock
import com.android.testutils.MockitoKt.whenever
import com.android.testutils.waitForCondition
import com.android.tools.adbbridge.Reservation
import com.android.tools.adtui.swing.HeadlessDialogRule
import com.android.tools.adtui.swing.createModalDialogAndInteractWithIt
import com.android.tools.adtui.swing.findAllDescendants
import com.android.tools.adtui.swing.popup.JBPopupRule
import com.android.tools.idea.concurrency.AndroidDispatchers
import com.android.tools.idea.devicemanager.DeviceType
import com.android.tools.idea.deviceprovisioner.DeviceProvisionerService
import com.android.tools.idea.flags.StudioFlags
import com.android.tools.idea.io.grpc.Status
import com.android.tools.idea.io.grpc.StatusRuntimeException
import com.android.tools.idea.testing.disposable
import com.google.common.truth.Truth.assertThat
import com.google.gct.directaccess.CloudProjectEntry
import com.google.gct.directaccess.DirectAccessCloudProjectManager
import com.google.gct.directaccess.DirectAccessPermissionStatus.Companion.parseFrom
import com.google.gct.directaccess.DirectAccessPersistentStateComponent
import com.google.gct.directaccess.DirectAccessService
import com.google.gct.directaccess.DirectAccessServiceSetup
import com.google.gct.directaccess.FULL_PERMISSIONS_SET
import com.google.gct.directaccess.RefreshableStateFlow
import com.google.gct.directaccess.SERVICES_USE
import com.google.gct.directaccess.TestUtils.deviceInfoListProvider
import com.google.gct.directaccess.VIEWER_PERMISSIONS_SET
import com.google.gct.directaccess.provisioner.DeviceInfo
import com.google.gct.directaccess.provisioner.DeviceSelection
import com.google.gct.directaccess.provisioner.DirectAccessDeviceHandle
import com.google.gct.directaccess.provisioner.DirectAccessDeviceSource
import com.google.gct.directaccess.ui.DirectAccessProjectSelectorImpl
import com.google.gct.directaccess.ui.ERROR_FETCHING_FIREBASE_PROJECT
import com.google.gct.directaccess.ui.NO_PROJECTS_AVAILABLE
import com.google.gct.directaccess.ui.ONBOARDING_WORKFLOW_KEY
import com.google.gct.directaccess.ui.SelectDeviceDialog
import com.google.gct.login2.LoginFeature
import com.google.gct.login2.LoginUsersRule
import com.google.services.firebase.FirebaseLoginFeature
import com.google.services.firebase.FirebaseProjectClientRule
import com.google.services.firebase.directaccess.client.FakeDirectAccessReservationManager
import com.intellij.icons.AllIcons
import com.intellij.ide.HelpTooltip
import com.intellij.ide.ui.customization.CustomActionsSchema
import com.intellij.ide.util.PropertiesComponent
import com.intellij.openapi.actionSystem.CommonDataKeys
import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.components.service
import com.intellij.openapi.project.Project
import com.intellij.openapi.ui.ComboBox
import com.intellij.testFramework.ProjectRule
import com.intellij.testFramework.RunsInEdt
import com.intellij.testFramework.TestActionEvent
import com.intellij.testFramework.replaceService
import com.intellij.ui.components.AnActionLink
import com.intellij.ui.components.JBLabel
import icons.FirebaseIcons
import icons.StudioIcons
import java.awt.event.MouseEvent
import javax.swing.JLabel
import javax.swing.JPanel
import kotlin.time.Duration.Companion.seconds
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.takeWhile
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext
import org.junit.After
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.rules.RuleChain
import org.mockito.Mockito.any
import org.mockito.Mockito.doAnswer
import org.mockito.Mockito.doReturn
const val SELECT_PROJECT_ID = "SelectProjectAction"
private val TIMEOUT = 100.seconds
class SelectProjectActionTest {
private val apiDisabledProject = "apiDisabledProject"
private val unsupportedTestProjectWithServiceUse = "unsupportedTestProjectWithServiceUse"
private val unsupportedTestProjectWithoutServiceUse = "unsupportedTestProjectWithoutServiceUse"
private val viewerTestProject = "viewerTestProject"
private val unknownPermissionTestProject = "unknownPermissionTestProject"
private val supportedProjectName = "supportedTestProject"
private val noQuotaProjectName = "noQuotaTestProject"
private val blazeProjectName = "blazeTestProject"
private val projectRule = ProjectRule()
private val popupRule = JBPopupRule()
private val loginUsersRule = LoginUsersRule()
private val firebaseProjectClientRule = FirebaseProjectClientRule()
// Simulate the fake properties component using a map
private val fakePropertiesComponent = mutableMapOf<Project, String>()
@get:Rule
val ruleChain =
RuleChain.outerRule(FlagRule(StudioFlags.ENABLE_SETTINGS_ACCOUNT_UI, true))
.around(projectRule)
.around(HeadlessDialogRule())
.around(popupRule)
.around(loginUsersRule)
.around(firebaseProjectClientRule)!!
private val scope = CoroutineScope(Dispatchers.IO)
private val cloudProjectManagerFlow = MutableStateFlow<DirectAccessCloudProjectManager?>(null)
private val permissionFlow =
RefreshableStateFlow(scope, Long.MAX_VALUE) {
when (cloudProjectManagerFlow.value?.cloudProject?.name) {
unsupportedTestProjectWithServiceUse -> parseFrom(setOf(SERVICES_USE))
unsupportedTestProjectWithoutServiceUse -> parseFrom(FULL_PERMISSIONS_SET - SERVICES_USE)
supportedProjectName,
noQuotaProjectName,
blazeProjectName -> parseFrom(FULL_PERMISSIONS_SET)
viewerTestProject -> parseFrom(VIEWER_PERMISSIONS_SET)
unknownPermissionTestProject ->
parseFrom(FULL_PERMISSIONS_SET - VIEWER_PERMISSIONS_SET + SERVICES_USE)
else -> parseFrom(emptySet())
}
}
private var exceptionToThrow: StatusRuntimeException? = null
private val preselectedDeviceInfo =
DeviceInfo(
id = "shiba",
brand = "google",
name = "Pixel 8",
manufacturer = "Google",
codename = "shiba",
api = 34,
type = DeviceType.PHONE,
screenX = 1080,
screenY = 2400,
screenDensity = 420,
deviceAvailabilityEstimateSeconds = 30,
)
@Before
fun setUp() {
PropertiesComponent.getInstance().setValue(ONBOARDING_WORKFLOW_KEY, false)
}
@After
fun tearDown() {
PropertiesComponent.getInstance().setValue(ONBOARDING_WORKFLOW_KEY, false)
}
@RunsInEdt
@Test
fun testSelectProjectAction() = runBlockingWithTimeout {
val mockDirectAccessServiceSetup = mock<DirectAccessServiceSetup>()
doReturn(deviceInfoListProvider())
.whenever(mockDirectAccessServiceSetup)
.getAccessibleDeviceInfoList(null)
ApplicationManager.getApplication()
.replaceService(
DirectAccessServiceSetup::class.java,
mockDirectAccessServiceSetup,
projectRule.disposable,
)
val devices = MutableStateFlow(listOf<DeviceHandle>())
val mockProvisioner = mock<DeviceProvisioner>()
val mockDeviceProvisionerService = mock<DeviceProvisionerService>()
doReturn(devices).whenever(mockProvisioner).devices
doReturn(mockProvisioner).whenever(mockDeviceProvisionerService).deviceProvisioner
projectRule.project.replaceService(
DeviceProvisionerService::class.java,
mockDeviceProvisionerService,
projectRule.disposable,
)
val mockDirectAccessService = mock<DirectAccessService>()
doReturn(cloudProjectManagerFlow).whenever(mockDirectAccessService).cloudProjectManager
doReturn(scope).whenever(mockDirectAccessService).scope
val mockDeviceSelectionListFlow = MutableStateFlow(listOf<DeviceSelection>())
doReturn(mockDeviceSelectionListFlow).whenever(mockDirectAccessService).deviceSelectionListFlow
doAnswer {
val cloudProjectName = it.arguments[0] as? String
cloudProjectName?.let { name -> fakePropertiesComponent[projectRule.project] = name }
cloudProjectManagerFlow.value =
createCloudProjectManager(
scope,
cloudProjectName,
cloudProjectName == supportedProjectName || cloudProjectName == noQuotaProjectName,
cloudProjectName == noQuotaProjectName,
)
runBlocking { permissionFlow.refresh() }
Unit
}
.whenever(mockDirectAccessService)
.selectCloudProject(any())
projectRule.project.replaceService(
DirectAccessService::class.java,
mockDirectAccessService,
projectRule.disposable,
)
assertThat(CustomActionsSchema.getInstance().getCorrectedAction(SELECT_PROJECT_ID))
.isInstanceOf(SelectProjectAction::class.java)
// Check if DirectAccessProjectSelector chooses the preferred project.
firebaseProjectClientRule.setupFirebaseClient(
throwErrorOnExecute = false,
returnMalformedJson = false,
projectList = listOf(apiDisabledProject, supportedProjectName),
)
val testSelector =
DirectAccessProjectSelectorImpl(projectRule.project, supportedProjectName, true, scope)
testSelector.isReady.takeWhile { !it }.collect()
assertThat(testSelector.selectedProject.value).isEqualTo(supportedProjectName)
firebaseProjectClientRule.setupFirebaseClient(
throwErrorOnExecute = false,
returnMalformedJson = false,
projectList =
listOf(
apiDisabledProject,
unknownPermissionTestProject,
unsupportedTestProjectWithoutServiceUse,
viewerTestProject,
unknownPermissionTestProject,
supportedProjectName,
noQuotaProjectName,
),
)
val selectDeviceAction = SelectProjectAction()
// Click the device selection button.
val mouseEvent = MouseEvent(JPanel(), MouseEvent.MOUSE_CLICKED, 0, 0, 0, 0, 1, true, 0)
val event =
TestActionEvent.createTestEvent(
selectDeviceAction,
{
when (it) {
CommonDataKeys.PROJECT.name -> projectRule.project
else -> null
}
},
mouseEvent,
)
withContext(AndroidDispatchers.uiThread) {
createModalDialogAndInteractWithIt({ selectDeviceAction.actionPerformed(event) }) {
// Start select action before login.
val dialog = it as SelectDeviceDialog
val action = dialog.rootPane.findAllDescendants<AnActionLink>().first()
assertThat(action.text).isEqualTo("Log in to Google")
action.doClick()
waitForCondition { loginUsersRule.loginService.isLoggedIn() }
waitForCondition {
dialog.rootPane.findAllDescendants<ComboBox<String>>().iterator().hasNext()
}
val comboBox = dialog.rootPane.findAllDescendants<ComboBox<String>>().first()
waitForCondition { comboBox.model.size > 1 }
// Select a project that does not support direct access.
exceptionToThrow =
Status.PERMISSION_DENIED.withDescription("Not authorized for project")
.asRuntimeException()
comboBox.model.selectedItem = unsupportedTestProjectWithServiceUse
waitForCondition {
cloudProjectManagerFlow.value?.cloudProject?.name == unsupportedTestProjectWithServiceUse
}
val label =
dialog.rootPane.findAllDescendants<JBLabel>().first { label ->
label.icon == StudioIcons.Common.ERROR
}
waitForCondition {
label
.getHelpToolTipText()
.contains(
"You do not have access to Device Streaming in project $unsupportedTestProjectWithServiceUse."
)
}
// Select a project with disabled Cloud Testing API
exceptionToThrow =
Status.PERMISSION_DENIED.withDescription(
"Cloud Testing API has not been used in project $apiDisabledProject before or it is disabled."
)
.asRuntimeException()
comboBox.model.selectedItem = apiDisabledProject
waitForCondition { cloudProjectManagerFlow.value?.cloudProject?.name == apiDisabledProject }
waitForCondition {
label
.getHelpToolTipText()
.contains(
"Cloud Testing API is not enabled in your project $apiDisabledProject. Enable it by visiting Google Cloud console."
)
}
// Select a project without service use permission
exceptionToThrow =
Status.PERMISSION_DENIED.withDescription(
"Grant the caller the roles/serviceusage.serviceUsageConsumer role, or a custom role with the serviceusage.services.use permission"
)
.asRuntimeException()
comboBox.model.selectedItem = unsupportedTestProjectWithoutServiceUse
waitForCondition {
cloudProjectManagerFlow.value?.cloudProject?.name ==
unsupportedTestProjectWithoutServiceUse
}
waitForCondition {
label
.getHelpToolTipText()
.contains(
"You do not have full access to Device Streaming in project $unsupportedTestProjectWithoutServiceUse. You are missing the following permissions:serviceusage.services.use"
)
}
dialog.clickDefaultButton()
}
val extraDeviceInfoList = deviceInfoListProvider() + preselectedDeviceInfo
createModalDialogAndInteractWithIt({ selectDeviceAction.actionPerformed(event) }) {
dialogWrapper ->
val dialog = dialogWrapper as SelectDeviceDialog
waitForCondition {
dialog.rootPane.findAllDescendants<ComboBox<String>>().iterator().hasNext()
}
val comboBox = dialog.rootPane.findAllDescendants<ComboBox<String>>().first()
val errorLabel =
dialog.rootPane.findAllDescendants<JBLabel>().first { label ->
label.icon == StudioIcons.Common.ERROR
}
val planTooltipLabel =
dialog.rootPane.findAllDescendants<JBLabel>().first { label ->
label.icon == AllIcons.General.ContextHelp
}
val planLabel =
dialog.rootPane.findAllDescendants<JBLabel>().first { label ->
label.text?.startsWith("Plan:") == true
}
// Select a project with viewer permission
exceptionToThrow = null
comboBox.model.selectedItem = viewerTestProject
waitForCondition { cloudProjectManagerFlow.value?.cloudProject?.name == viewerTestProject }
waitForCondition {
errorLabel
.getHelpToolTipText()
.contains(
"You do not have full access to Device Streaming in project $viewerTestProject. You are missing the following permissions:" +
permissionFlow.value.missingPermissions.joinToString("")
)
}
// Select a project with a mix of permission
comboBox.model.selectedItem = unknownPermissionTestProject
waitForCondition {
cloudProjectManagerFlow.value?.cloudProject?.name == unknownPermissionTestProject
}
waitForCondition {
errorLabel
.getHelpToolTipText()
.contains(
"You do not have full access to Device Streaming in project $unknownPermissionTestProject. You are missing the following permissions:" +
permissionFlow.value.missingPermissions.joinToString("")
)
}
val usedMinutesLabel =
dialog.rootPane.findAllDescendants<JBLabel>().first { usedLabel ->
usedLabel.text?.endsWith("mins used") == true
}
val remainingMinutesLabel =
dialog.rootPane.findAllDescendants<JBLabel>().first { usedLabel ->
usedLabel.text?.endsWith("mins remaining") == true
}
assertThat(usedMinutesLabel.text).isEqualTo("-- mins used")
assertThat(remainingMinutesLabel.text).isEqualTo("-- mins remaining")
assertThat(fakePropertiesComponent[projectRule.project])
.isEqualTo(unknownPermissionTestProject)
comboBox.model.selectedItem = ERROR_FETCHING_FIREBASE_PROJECT
assertThat(fakePropertiesComponent[projectRule.project])
.isNotEqualTo(ERROR_FETCHING_FIREBASE_PROJECT)
assertThat(fakePropertiesComponent[projectRule.project])
.isEqualTo(unknownPermissionTestProject)
comboBox.model.selectedItem = NO_PROJECTS_AVAILABLE
waitForCondition { cloudProjectManagerFlow.value == null }
assertThat(fakePropertiesComponent[projectRule.project])
.isEqualTo(unknownPermissionTestProject)
// Select a blaze project that supports direct access.
comboBox.model.selectedItem = blazeProjectName
waitForCondition { cloudProjectManagerFlow.value?.cloudProject?.name == blazeProjectName }
waitForCondition { planLabel.text == "Blaze Plan" }
waitForCondition {
planTooltipLabel
.getHelpToolTipText()
.contains("Blaze plans allow extended usage and is billed monthly.")
}
// Select a spark project that supports direct access.
comboBox.model.selectedItem = supportedProjectName
waitForCondition {
cloudProjectManagerFlow.value?.cloudProject?.name == supportedProjectName
}
waitForCondition { planLabel.text == "Spark Plan" }
waitForCondition {
planTooltipLabel
.getHelpToolTipText()
.contains("Spark plans provide limited usage at no cost.")
}
assertThat(fakePropertiesComponent[projectRule.project]).isEqualTo(supportedProjectName)
assertThat(errorLabel.getHelpToolTipText()).isEqualTo("")
waitForCondition { usedMinutesLabel.text == "60 mins used" }
waitForCondition { remainingMinutesLabel.text == "less than 15 mins remaining" }
// Select a spark project that's out of quota
comboBox.model.selectedItem = noQuotaProjectName
waitForCondition { cloudProjectManagerFlow.value?.cloudProject?.name == noQuotaProjectName }
waitForCondition { usedMinutesLabel.text == "70 mins used" }
waitForCondition { remainingMinutesLabel.text == "0 mins remaining" }
// Select a blaze project that supports direct access with monthly quota.
comboBox.model.selectedItem = blazeProjectName
waitForCondition { cloudProjectManagerFlow.value?.cloudProject?.name == blazeProjectName }
waitForCondition { planLabel.text == "Blaze Plan" }
waitForCondition {
planTooltipLabel
.getHelpToolTipText()
.contains("Blaze plans allow extended usage and is billed monthly.")
}
waitForCondition { usedMinutesLabel.text == "60 mins used" }
waitForCondition { remainingMinutesLabel.text == "Blaze Plan may incur charges" }
// Select a spark project that supports direct access with monthly quota.
comboBox.model.selectedItem = supportedProjectName
waitForCondition {
cloudProjectManagerFlow.value?.cloudProject?.name == supportedProjectName
}
waitForCondition { planLabel.text == "Spark Plan" }
waitForCondition {
planTooltipLabel
.getHelpToolTipText()
.contains(
"Spark plans provide limited usage at no cost. " +
"Switch to a Blaze plan with monthly billing to keep using the service after Spark minutes run out."
)
}
assertThat(fakePropertiesComponent[projectRule.project]).isEqualTo(supportedProjectName)
assertThat(errorLabel.getHelpToolTipText()).isEqualTo("")
waitForCondition { usedMinutesLabel.text == "60 mins used" }
waitForCondition { remainingMinutesLabel.text == "less than 15 mins remaining" }
mockDeviceSelectionListFlow.value = extraDeviceInfoList.map { DeviceSelection(false, it) }
waitForCondition {
PropertiesComponent.getInstance().getBoolean(ONBOARDING_WORKFLOW_KEY, false)
}
dialog.clickDefaultButton()
}
yieldUntil { mockDeviceSelectionListFlow.value.any { it.isSelected } }
val selectedDeviceInfo = mockDeviceSelectionListFlow.value.first { it.isSelected }.deviceInfo
assertThat(selectedDeviceInfo).isEqualTo(preselectedDeviceInfo)
// Verify DeviceSource after updating selection.
val deviceSource = DirectAccessDeviceSource.Provider().createDeviceSource(projectRule.project)
assertThat(deviceSource!!.profiles.map { it.name })
.isEqualTo(extraDeviceInfoList.map { it.name })
}
// Start a device and the selector will be disabled.
val mockDeviceHandle = mock<DirectAccessDeviceHandle>()
doReturn(mock<DeviceState.Connected>()).whenever(mockDeviceHandle).state
devices.value = listOf(mockDeviceHandle)
withContext(AndroidDispatchers.uiThread) {
createModalDialogAndInteractWithIt({ selectDeviceAction.actionPerformed(event) }) { dialog ->
val selector = dialog.rootPane.findAllDescendants<ComboBox<String>>().first()
assertThat(selector.isEnabled).isFalse()
assertThat(selector.toolTipText).isEqualTo("Return all devices to change projects")
dialog.clickDefaultButton()
}
}
selectDeviceAction.update(event)
assertThat(selectDeviceAction.templatePresentation.icon).isEqualTo(FirebaseIcons.ACTION_ICON)
}
@RunsInEdt
@Test
fun testAuthorizeLink() = runBlockingWithTimeout {
// Log in as a user without the firebase feature
loginUsersRule.setActiveUser("test@google.com", features = setOf())
val selectDeviceAction = SelectProjectAction()
projectRule.project.service<DirectAccessPersistentStateComponent>().state.selectedCloudProject =
supportedProjectName
firebaseProjectClientRule.setupFirebaseClient(
throwErrorOnExecute = false,
returnMalformedJson = false,
projectList = listOf(apiDisabledProject, supportedProjectName),
)
// Click the device selection button.
val mouseEvent = MouseEvent(JPanel(), MouseEvent.MOUSE_CLICKED, 0, 0, 0, 0, 1, true, 0)
val event =
TestActionEvent.createTestEvent(
selectDeviceAction,
{
when (it) {
CommonDataKeys.PROJECT.name -> projectRule.project
else -> null
}
},
mouseEvent,
)
withContext(AndroidDispatchers.uiThread) {
createModalDialogAndInteractWithIt({ selectDeviceAction.actionPerformed(event) }) {
val dialog = it as SelectDeviceDialog
val action = dialog.rootPane.findAllDescendants<AnActionLink>().first()
assertThat(action.text).isEqualTo("Authorize Firebase")
action.doClick()
waitForCondition { LoginFeature.feature<FirebaseLoginFeature>().isLoggedIn() }
waitForCondition {
val comboBox = dialog.rootPane.findAllDescendants<ComboBox<String>>().firstOrNull()
comboBox?.model?.selectedItem == supportedProjectName
}
}
}
projectRule.project.service<DirectAccessPersistentStateComponent>().state.selectedCloudProject =
null
}
@Test
fun testIconWhenCloudProjectManagerNull() = runBlockingWithTimeout {
val selectDeviceAction = SelectProjectAction()
val event =
TestActionEvent.createTestEvent {
when (it) {
CommonDataKeys.PROJECT.name -> projectRule.project
else -> null
}
}
selectDeviceAction.update(event)
assertThat(event.presentation.icon).isEqualTo(FirebaseIcons.ACTION_ICON)
}
@Test
fun testActionNotVisibleWhenProjectIsNull() = runBlockingWithTimeout {
val selectDeviceAction = SelectProjectAction()
val event = TestActionEvent.createTestEvent { null }
selectDeviceAction.update(event)
assertThat(event.presentation.isVisible).isFalse()
}
@Test(expected = IllegalArgumentException::class)
fun testActionPerformedThrowsExceptionWhenProjectIsNull() = runBlockingWithTimeout {
val selectDeviceAction = SelectProjectAction()
val event = TestActionEvent.createTestEvent { null }
selectDeviceAction.update(event)
selectDeviceAction.actionPerformed(event)
}
@Test
fun testDescription() {
assertThat(SelectProjectAction().templatePresentation.description)
.isEqualTo("Open the Device Streaming dialog to select Firebase project and devices")
}
private fun createCloudProjectManager(
scope: CoroutineScope,
name: String?,
isAuthorized: Boolean,
outOfQuota: Boolean,
): DirectAccessCloudProjectManager? {
if (name == null) {
return null
}
val mockCloudProjectManager = mock<DirectAccessCloudProjectManager>()
doReturn(CloudProjectEntry("", name)).whenever(mockCloudProjectManager).cloudProject
val directAccessReservationManager =
object : FakeDirectAccessReservationManager() {
override fun listReservations(): List<Reservation> {
if (isAuthorized) return listOf()
throw RuntimeException("unauthorized")
}
}
doReturn(directAccessReservationManager).whenever(mockCloudProjectManager).reservationManager
val reservationListFlow =
RefreshableStateFlow(scope, Long.MAX_VALUE) {
if (isAuthorized) Pair(directAccessReservationManager.listReservations(), null)
else Pair(null, exceptionToThrow)
}
doReturn(reservationListFlow).whenever(mockCloudProjectManager).reservationListFlowWithException
doReturn(Pair(if (outOfQuota) 70L else 60L, 70L)).whenever(mockCloudProjectManager).usageQuota
val accessibleDeviceInfoListFlow =
RefreshableStateFlow(scope, Long.MAX_VALUE) {
if (isAuthorized) deviceInfoListProvider() + preselectedDeviceInfo else listOf()
}
doReturn(accessibleDeviceInfoListFlow)
.whenever(mockCloudProjectManager)
.accessibleDeviceInfoListFlow
doReturn(permissionFlow).whenever(mockCloudProjectManager).permissionFlow
val isBillingEnabledFlow =
RefreshableStateFlow(scope, Long.MAX_VALUE) {
when (name) {
supportedProjectName,
noQuotaProjectName -> false
blazeProjectName -> true
else -> null
}
}
doReturn(isBillingEnabledFlow).whenever(mockCloudProjectManager).isBillingEnabledFlow
return mockCloudProjectManager
}
}
private fun waitForCondition(condition: () -> Boolean) = waitForCondition(TIMEOUT, condition)
private fun JBLabel.getHelpToolTipText(): String {
if (!isVisible) return ""
val tooltip = HelpTooltip.getTooltipFor(this) ?: return ""
val tooltipPanel = tooltip.createTipPanel()
val text = buildString { tooltipPanel.findAllDescendants<JLabel>().forEach { append(it.text) } }
return text.replace(Regex("<[^>]*>"), "").replace("\n", "").replace(Regex(" +"), " ").trim()
}