Skip to content

Commit

Permalink
Fixes #1350 - Detect if a project does not have App Engine application (
Browse files Browse the repository at this point in the history
#1426)

* WIP

* extract ProjectSelectorSelectionChangedListener

* extract ProjectSelectorSelectionChangedListener into top level class in internal package

* refactor internal classes into top level classes, more tests, extract Google API creation from ProjectRepository

* minor changes, javadoc

* import org.eclipse.ui.browser

* refactor to use OpenUriSelectionListener

* add timeout to api calls, rename api methods, make fields static/final

* handle long error message, display only error message instead of full json response

* test fix

* better error message handling

* store App Engine application in GcpProject and query it in project selector only if needed, reduce default timeout for GoogleApiFactory

* changed message to remove 'Click here'

* another tweak to the missing app engine message
  • Loading branch information
akerekes authored Feb 22, 2017
1 parent a379ad9 commit a5705c0
Show file tree
Hide file tree
Showing 28 changed files with 880 additions and 75 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,10 @@
import com.google.cloud.tools.eclipse.appengine.deploy.standard.StandardDeployPreferences;
import com.google.cloud.tools.eclipse.login.IGoogleLoginService;
import com.google.cloud.tools.eclipse.login.ui.AccountSelectorObservableValue;
import com.google.cloud.tools.eclipse.projectselector.GcpProject;
import com.google.cloud.tools.eclipse.projectselector.ProjectRepository;
import com.google.cloud.tools.eclipse.projectselector.ProjectRepositoryException;
import com.google.cloud.tools.eclipse.projectselector.ProjectSelector;
import com.google.cloud.tools.eclipse.projectselector.model.GcpProject;
import com.google.cloud.tools.eclipse.test.util.ui.ShellTestResource;
import com.google.cloud.tools.login.Account;
import java.util.Arrays;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
/*
* Copyright 2017 Google Inc.
*
* 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.cloud.tools.eclipse.appengine.deploy.ui.internal;

import static org.mockito.Matchers.any;
import static org.mockito.Matchers.anyString;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

import com.google.api.client.auth.oauth2.Credential;
import com.google.cloud.tools.eclipse.appengine.deploy.ui.internal.ProjectSelectorSelectionChangedListener;
import com.google.cloud.tools.eclipse.login.ui.AccountSelector;
import com.google.cloud.tools.eclipse.projectselector.ProjectRepository;
import com.google.cloud.tools.eclipse.projectselector.ProjectRepositoryException;
import com.google.cloud.tools.eclipse.projectselector.ProjectSelector;
import com.google.cloud.tools.eclipse.projectselector.model.AppEngine;
import com.google.cloud.tools.eclipse.projectselector.model.GcpProject;
import org.eclipse.jface.viewers.SelectionChangedEvent;
import org.eclipse.jface.viewers.StructuredSelection;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.runners.MockitoJUnitRunner;

@RunWith(MockitoJUnitRunner.class)
public class ProjectSelectorSelectionChangedListenerTest {

private static final String EXPECTED_LINK =
"https://console.cloud.google.com/appengine/create?lang=java&project=projectId";
private static final String EXPECTED_MESSAGE_WHEN_NO_APPLICATION =
"This project does not have an App Engine application which is\n"
+ "required for deployment. <a href=\"" + EXPECTED_LINK + "\">Create an App Engine "
+ "application in the\nCloud Console</a>.";
private static final String EXPECTED_MESSAGE_WHEN_EXCEPTION =
"An error occurred while retrieving App Engine application:\ntestException";

@Mock private AccountSelector accountSelector;
@Mock private ProjectSelector projectSelector;
@Mock private ProjectRepository projectRepository;
@Mock private SelectionChangedEvent event;

private ProjectSelectorSelectionChangedListener listener;

@Before
public void setUp() throws Exception {
listener = new ProjectSelectorSelectionChangedListener(accountSelector, projectRepository,
projectSelector);
}

@Test
public void testSelectionChanged_emptySelection() {
when(event.getSelection()).thenReturn(new StructuredSelection());
listener.selectionChanged(event);
verify(projectSelector).clearStatusLink();
}

@Test
public void testSelectionChanged_repositoryException() throws ProjectRepositoryException {
initSelectionAndAccountSelector();
when(projectRepository.getAppEngineApplication(any(Credential.class), anyString()))
.thenThrow(new ProjectRepositoryException("testException"));

listener.selectionChanged(event);
verify(projectSelector).setStatusLink(EXPECTED_MESSAGE_WHEN_EXCEPTION, null /* tooltip */);
}

@Test
public void testSelectionChanged_noAppEngineApplication() throws ProjectRepositoryException {
initSelectionAndAccountSelector();
when(projectRepository.getAppEngineApplication(any(Credential.class), anyString()))
.thenReturn(AppEngine.NO_APPENGINE_APPLICATION);

listener.selectionChanged(event);
verify(projectSelector).setStatusLink(EXPECTED_MESSAGE_WHEN_NO_APPLICATION, EXPECTED_LINK);
}

@Test
public void testSelectionChanged_hasAppEngineApplication() throws ProjectRepositoryException {
initSelectionAndAccountSelector();
when(projectRepository.getAppEngineApplication(any(Credential.class), anyString()))
.thenReturn(AppEngine.withId("id"));

listener.selectionChanged(event);
verify(projectSelector).clearStatusLink();
}

private void initSelectionAndAccountSelector() {
StructuredSelection selection =
new StructuredSelection(new GcpProject("projectName", "projectId"));
when(event.getSelection()).thenReturn(selection);
when(accountSelector.getSelectedCredential()).thenReturn(mock(Credential.class));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ Import-Package: com.google.api.client.auth.oauth2,
com.google.cloud.tools.eclipse.login,
com.google.cloud.tools.eclipse.login.ui,
com.google.cloud.tools.eclipse.projectselector,
com.google.cloud.tools.eclipse.projectselector.model,
com.google.cloud.tools.eclipse.sdk.ui,
com.google.cloud.tools.eclipse.ui.util,
com.google.cloud.tools.eclipse.ui.util.console,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
import com.google.cloud.tools.appengine.cloudsdk.CloudSdkOutOfDateException;
import com.google.cloud.tools.eclipse.appengine.ui.AppEngineImages;
import com.google.cloud.tools.eclipse.login.IGoogleLoginService;
import com.google.cloud.tools.eclipse.projectselector.GoogleApiFactory;
import com.google.cloud.tools.eclipse.projectselector.ProjectRepository;
import com.google.common.base.Preconditions;
import org.eclipse.core.databinding.ValidationStatusProvider;
Expand Down Expand Up @@ -90,7 +91,8 @@ protected Control createDialogArea(final Composite parent) {

Composite container = new Composite(dialogArea, SWT.NONE);
content = new StandardDeployPreferencesPanel(container, project, loginService,
getLayoutChangedHandler(), true /* requireValues */, new ProjectRepository());
getLayoutChangedHandler(), true /* requireValues */,
new ProjectRepository(new GoogleApiFactory()));
GridDataFactory.fillDefaults().grab(true, false).applyTo(content);

// we pull in Dialog's content margins which are zeroed out by TitleAreaDialog
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,14 @@

package com.google.cloud.tools.eclipse.appengine.deploy.ui;

import com.google.cloud.tools.eclipse.appengine.facets.AppEngineFlexFacet;
import com.google.cloud.tools.eclipse.appengine.facets.AppEngineStandardFacet;
import com.google.cloud.tools.eclipse.login.IGoogleLoginService;
import com.google.cloud.tools.eclipse.projectselector.GoogleApiFactory;
import com.google.cloud.tools.eclipse.projectselector.ProjectRepository;
import com.google.cloud.tools.eclipse.util.AdapterUtil;
import java.util.logging.Level;
import java.util.logging.Logger;

import org.eclipse.core.resources.IProject;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.jface.databinding.preference.PreferencePageSupport;
Expand All @@ -34,12 +39,6 @@
import org.eclipse.wst.common.project.facet.core.IProjectFacet;
import org.eclipse.wst.common.project.facet.core.ProjectFacetsManager;

import com.google.cloud.tools.eclipse.appengine.facets.AppEngineFlexFacet;
import com.google.cloud.tools.eclipse.appengine.facets.AppEngineStandardFacet;
import com.google.cloud.tools.eclipse.login.IGoogleLoginService;
import com.google.cloud.tools.eclipse.projectselector.ProjectRepository;
import com.google.cloud.tools.eclipse.util.AdapterUtil;

/**
* Displays the App Engine deployment page for the selected project in the property page dialog.
* The contents of the App Engine deployment page vary depending on if the selected project
Expand Down Expand Up @@ -157,7 +156,7 @@ private DeployPreferencesPanel getPreferencesPanel(IProject project,
setTitle(Messages.getString("standard.page.title"));
return new StandardDeployPreferencesPanel(
container, project, loginService, getLayoutChangedHandler(), false /* requireValues */,
new ProjectRepository());
new ProjectRepository(new GoogleApiFactory()));
} else if (AppEngineFlexFacet.hasAppEngineFacet(facetedProject)) {
setTitle(Messages.getString("flex.page.title"));
return new FlexDeployPreferencesPanel(container, project);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,14 @@
package com.google.cloud.tools.eclipse.appengine.deploy.ui;

import com.google.api.client.auth.oauth2.Credential;
import com.google.cloud.tools.eclipse.appengine.deploy.ui.internal.ProjectSelectorSelectionChangedListener;
import com.google.cloud.tools.eclipse.login.IGoogleLoginService;
import com.google.cloud.tools.eclipse.login.ui.AccountSelector;
import com.google.cloud.tools.eclipse.login.ui.AccountSelectorObservableValue;
import com.google.cloud.tools.eclipse.projectselector.GcpProject;
import com.google.cloud.tools.eclipse.projectselector.ProjectRepository;
import com.google.cloud.tools.eclipse.projectselector.ProjectRepositoryException;
import com.google.cloud.tools.eclipse.projectselector.ProjectSelector;
import com.google.cloud.tools.eclipse.projectselector.model.GcpProject;
import com.google.cloud.tools.eclipse.ui.util.FontUtil;
import com.google.cloud.tools.eclipse.ui.util.databinding.BucketNameValidator;
import com.google.cloud.tools.eclipse.ui.util.databinding.ProjectSelectorValidator;
Expand Down Expand Up @@ -286,14 +287,18 @@ private void createProjectIdSection() {
GridDataFactory.swtDefaults().align(SWT.BEGINNING, SWT.BEGINNING).applyTo(projectIdLabel);
projectSelector = new ProjectSelector(this);
GridDataFactory.fillDefaults().align(SWT.FILL, SWT.CENTER)
.grab(true, false).hint(300, 100).applyTo(projectSelector);
.grab(true, false).hint(400, 150).applyTo(projectSelector);
accountSelector.addSelectionListener(new Runnable() {
@Override
public void run() {
Credential selectedCredential = accountSelector.getSelectedCredential();
projectSelector.setProjects(retrieveProjects(selectedCredential));
}
});
projectSelector.addSelectionChangedListener(
new ProjectSelectorSelectionChangedListener(accountSelector,
projectRepository,
projectSelector));
}

private void createProjectVersionSection() {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
/*
* Copyright 2017 Google Inc.
*
* 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.cloud.tools.eclipse.appengine.deploy.ui.internal;

import com.google.api.client.auth.oauth2.Credential;
import com.google.cloud.tools.eclipse.appengine.deploy.ui.Messages;
import com.google.cloud.tools.eclipse.login.ui.AccountSelector;
import com.google.cloud.tools.eclipse.projectselector.ProjectRepository;
import com.google.cloud.tools.eclipse.projectselector.ProjectRepositoryException;
import com.google.cloud.tools.eclipse.projectselector.ProjectSelector;
import com.google.cloud.tools.eclipse.projectselector.model.AppEngine;
import com.google.cloud.tools.eclipse.projectselector.model.GcpProject;
import java.text.MessageFormat;
import org.eclipse.jface.viewers.ISelectionChangedListener;
import org.eclipse.jface.viewers.IStructuredSelection;
import org.eclipse.jface.viewers.SelectionChangedEvent;

public class ProjectSelectorSelectionChangedListener implements ISelectionChangedListener {

private static String CREATE_APP_LINK =
"https://console.cloud.google.com/appengine/create?lang=java&project={0}";

private final AccountSelector accountSelector;
private final ProjectRepository projectRepository;
private final ProjectSelector projectSelector;

public ProjectSelectorSelectionChangedListener(AccountSelector accountSelector,
ProjectRepository projectRepository,
ProjectSelector projectSelector) {
this.accountSelector = accountSelector;
this.projectRepository = projectRepository;
this.projectSelector = projectSelector;
}

@Override
public void selectionChanged(SelectionChangedEvent event) {
IStructuredSelection selection = (IStructuredSelection) event.getSelection();
try {
if (!selection.isEmpty()) {
GcpProject project = (GcpProject) selection.getFirstElement();
boolean hasAppEngineApplication = hasAppEngineApplication(project);
if (!hasAppEngineApplication) {
String link = MessageFormat.format(CREATE_APP_LINK, project.getId());
projectSelector.setStatusLink(
Messages.getString("projectselector.missing.appengine.application.link",
link), link);
} else {
projectSelector.clearStatusLink();
}
} else {
projectSelector.clearStatusLink();
}
} catch (ProjectRepositoryException ex) {
projectSelector.setStatusLink(Messages.getString("projectselector.retrieveapplication.error.message",
ex.getLocalizedMessage()),
null /* tooltip */);
}
}

/**
* Lazily queries the backend whether the specified project has an App Engine application.
* <p>
* The result of the query is stored in the object and the next time it is returned from there
* saving a roundtrip to the backend.
*/
private boolean hasAppEngineApplication(GcpProject project) throws ProjectRepositoryException {
if (!project.hasAppEngineInfo()) {
Credential selectedCredential = accountSelector.getSelectedCredential();
AppEngine appEngine =
projectRepository.getAppEngineApplication(selectedCredential, project.getId());
project.setAppEngine(appEngine);
}
return project.getAppEngine() != AppEngine.NO_APPENGINE_APPLICATION;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -38,10 +38,14 @@ tooltip.version=The version of the app that will be created or replaced by this
tooltip.manual.promote.link=To manually promote a version use the Google Cloud Console: {0}
tooltip.stop.previous.version=If checked, stops the previously running version when deploying a \
new version that receives all traffic.
tooltip.staging.bucket=The Google Cloud Storage bucket used to stage files for the deployment. \
tooltip.staging.bucket=The Google Cloud Storage bucket used to stage files for deployment. \
If not specified, the application''s default code bucket is used.
projectselector.retrieveproject.error.title=Failed to retrieve projects
projectselector.retrieveproject.error.title=An error happened while retrieving projects: {0}
projectselector.retrieveproject.error.message=An error occurred while retrieving projects: {0}
projectselector.retrieveapplication.error.title=Failed to retrieve App Engine application
projectselector.retrieveapplication.error.message=An error occurred while retrieving App Engine application:\n{0}
projectselector.missing.appengine.application.link=This project does not have an App Engine application which is\n\
required for deployment. <a href="{0}">Create an App Engine application in the\nCloud Console</a>.

# Flex deploy settings
browse.button=Browse
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,10 @@ Bundle-Classpath: lib/google-api-client-1.22.0.jar,
.
Export-Package: com.google.api.client.auth.oauth2;x-friends:="com.google.cloud.tools.eclipse.appengine.deploy,com.google.cloud.tools.eclipse.appengine.deploy.ui,com.google.cloud.tools.eclipse.projectselector",
com.google.api.client.googleapis;x-friends:="com.google.cloud.tools.eclipse.projectselector",
com.google.api.client.googleapis.json;x-friends:="com.google.cloud.tools.eclipse.projectselector",
com.google.api.client.googleapis.services;x-friends:="com.google.cloud.tools.eclipse.projectselector",
com.google.api.client.googleapis.services.json;x-friends:="com.google.cloud.tools.eclipse.projectselector",
com.google.api.client.googleapis.testing.json;x-friends:="com.google.cloud.tools.eclipse.projectselector",
com.google.api.client.http;x-friends:="com.google.cloud.tools.eclipse.projectselector",
com.google.api.client.http.javanet;x-friends:="com.google.cloud.tools.eclipse.projectselector",
com.google.api.client.json;x-friends:="com.google.cloud.tools.eclipse.projectselector",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ Bundle-Version: 0.1.0.qualifier
Fragment-Host: com.google.cloud.tools.eclipse.projectselector
Require-Bundle: org.hamcrest;bundle-version="1.1.0",
org.junit;bundle-version="4.12.0"
Import-Package: com.google.cloud.tools.eclipse.test.util.ui,
Import-Package: com.google.api.client.googleapis.testing.json,
com.google.cloud.tools.eclipse.test.util.ui,
org.mockito;provider=google;version="1.10.19",
org.mockito.runners;provider=google;version="1.10.19",
org.mockito.stubbing;provider=google;version="1.10.19",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
/*
* Copyright 2017 Google Inc.
*
* 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.cloud.tools.eclipse.projectselector;

import static org.hamcrest.CoreMatchers.containsString;
import static org.junit.Assert.assertThat;
import static org.mockito.Mockito.mock;

import com.google.api.client.auth.oauth2.Credential;
import com.google.api.services.appengine.v1.Appengine.Apps;
import com.google.api.services.cloudresourcemanager.CloudResourceManager.Projects;
import com.google.cloud.tools.eclipse.util.CloudToolsInfo;
import java.io.IOException;
import org.junit.Test;

public class GoogleApiFactoryTest {

@Test
public void testNewAppsApi_userAgentIsSet() throws IOException {
Apps api = new GoogleApiFactory().newAppsApi(mock(Credential.class));
assertThat(api.get("").getRequestHeaders().getUserAgent(),
containsString(CloudToolsInfo.USER_AGENT));
}

@Test
public void testNewProjectsApi_userAgentIsSet() throws IOException {
Projects api = new GoogleApiFactory().newProjectsApi(mock(Credential.class));
assertThat(api.get("").getRequestHeaders().getUserAgent(),
containsString(CloudToolsInfo.USER_AGENT));
}
}
Loading

0 comments on commit a5705c0

Please sign in to comment.