diff --git a/gcloud-java-resourcemanager/src/main/java/com/google/gcloud/resourcemanager/testing/LocalResourceManagerHelper.java b/gcloud-java-resourcemanager/src/main/java/com/google/gcloud/resourcemanager/testing/LocalResourceManagerHelper.java new file mode 100644 index 000000000000..beb824ac812d --- /dev/null +++ b/gcloud-java-resourcemanager/src/main/java/com/google/gcloud/resourcemanager/testing/LocalResourceManagerHelper.java @@ -0,0 +1,573 @@ +package com.google.gcloud.resourcemanager.testing; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; +import static java.net.HttpURLConnection.HTTP_OK; + +import com.google.api.client.json.JsonFactory; +import com.google.api.services.cloudresourcemanager.model.Project; +import com.google.common.base.Joiner; +import com.google.common.base.Objects; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSet; +import com.google.common.io.ByteStreams; + +import com.sun.net.httpserver.Headers; +import com.sun.net.httpserver.HttpExchange; +import com.sun.net.httpserver.HttpHandler; +import com.sun.net.httpserver.HttpServer; + +import org.joda.time.format.ISODateTimeFormat; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.InetSocketAddress; +import java.net.URI; +import java.net.URISyntaxException; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Random; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.logging.Level; +import java.util.logging.Logger; +import java.util.zip.GZIPInputStream; + +/** + * Utility to create a local Resource Manager mock for testing. + * + *

The mock runs in a separate thread, listening for HTTP requests on the local machine at an + * ephemeral port. + */ +@SuppressWarnings("restriction") +public class LocalResourceManagerHelper { + private static final Logger log = Logger.getLogger(LocalResourceManagerHelper.class.getName()); + private static final JsonFactory jsonFactory = + new com.google.api.client.json.jackson.JacksonFactory(); + private static final Random PROJECT_NUMBER_GENERATOR = new Random(); + private static final String VERSION = "v1beta1"; + private static final String CONTEXT = "/" + VERSION + "/projects"; + private static final URI BASE_CONTEXT; + private static final Set SUPPORTED_COMPRESSION_ENCODINGS = + ImmutableSet.of("gzip", "x-gzip"); + + static { + try { + BASE_CONTEXT = new URI(CONTEXT); + } catch (URISyntaxException e) { + throw new RuntimeException( + "Could not initialize LocalResourceManagerHelper due to URISyntaxException.", e); + } + } + + // see https://cloud.google.com/resource-manager/reference/rest/v1beta1/projects + private static final Set PERMISSIBLE_PROJECT_NAME_PUNCTUATION = + ImmutableSet.of('-', '\'', '"', ' ', '!'); + + private HttpServer server; + private final ConcurrentHashMap projects = new ConcurrentHashMap<>(); + private final int port; + + private static class Response { + private final int code; + private final String body; + + Response(int code, String body) { + this.code = code; + this.body = body; + } + + int code() { + return code; + } + + String body() { + return body; + } + } + + private enum Error { + ALREADY_EXISTS(409, "global", "alreadyExists", "ALREADY_EXISTS"), + PERMISSION_DENIED(403, "global", "forbidden", "PERMISSION_DENIED"), + // change failed precondition error code to 412 when #440 is fixed + FAILED_PRECONDITION(400, "global", "failedPrecondition", "FAILED_PRECONDITION"), + // change invalid argument error code to 412 when #440 is fixed + INVALID_ARGUMENT(400, "global", "badRequest", "INVALID_ARGUMENT"), + BAD_REQUEST(400, "global", "badRequest", "BAD_REQUEST"), + INTERNAL_ERROR(500, "global", "internalError", "INTERNAL_ERROR"); + + private final int code; + private final String domain; + private final String reason; + private final String status; + + Error(int code, String domain, String reason, String status) { + this.code = code; + this.domain = domain; + this.reason = reason; + this.status = status; + } + + Response response(String message) { + try { + return new Response(code, toJson(message)); + } catch (IOException e) { + return Error.INTERNAL_ERROR.response("Error when generating JSON error response"); + } + } + + private String toJson(String message) throws IOException { + Map errors = new HashMap<>(); + errors.put("domain", domain); + errors.put("message", message); + errors.put("reason", reason); + Map args = new HashMap<>(); + args.put("errors", ImmutableList.of(errors)); + args.put("code", code); + args.put("message", message); + args.put("status", status); + return jsonFactory.toString(ImmutableMap.of("error", args)); + } + } + + private class RequestHandler implements HttpHandler { + @Override + public void handle(HttpExchange exchange) { + // see https://cloud.google.com/resource-manager/reference/rest/ + Response response; + String path = BASE_CONTEXT.relativize(exchange.getRequestURI()).getPath(); + String requestMethod = exchange.getRequestMethod(); + try { + switch (requestMethod) { + case "POST": + if (path.endsWith(":undelete")) { + response = undelete(projectIdFromUri(path)); + } else { + String requestBody = + decodeContent(exchange.getRequestHeaders(), exchange.getRequestBody()); + response = create(jsonFactory.fromString(requestBody, Project.class)); + } + break; + case "DELETE": + response = delete(projectIdFromUri(path)); + break; + case "GET": + if (!path.isEmpty()) { + response = + get(projectIdFromUri(path), parseFields(exchange.getRequestURI().getQuery())); + } else { + response = list(parseListOptions(exchange.getRequestURI().getQuery())); + } + break; + case "PUT": + String requestBody = + decodeContent(exchange.getRequestHeaders(), exchange.getRequestBody()); + response = + replace(projectIdFromUri(path), jsonFactory.fromString(requestBody, Project.class)); + break; + default: + response = Error.BAD_REQUEST.response( + "The server could not understand the following request URI: " + requestMethod + " " + + path); + } + } catch (IOException e) { + response = Error.BAD_REQUEST.response(e.getMessage()); + } + writeResponse(exchange, response); + } + } + + private static void writeResponse(HttpExchange exchange, Response response) { + exchange.getResponseHeaders().set("Content-type", "application/json; charset=UTF-8"); + OutputStream outputStream = exchange.getResponseBody(); + try { + exchange.sendResponseHeaders(response.code(), response.body().length()); + outputStream.write(response.body().getBytes(StandardCharsets.UTF_8)); + outputStream.close(); + } catch (IOException e) { + log.log(Level.WARNING, "IOException encountered when sending response.", e); + } + } + + private static String decodeContent(Headers headers, InputStream inputStream) throws IOException { + List contentEncoding = headers.get("Content-encoding"); + InputStream input = inputStream; + try { + if (contentEncoding != null && !contentEncoding.isEmpty()) { + String encoding = contentEncoding.get(0); + if (SUPPORTED_COMPRESSION_ENCODINGS.contains(encoding)) { + input = new GZIPInputStream(inputStream); + } else if (!encoding.equals("identity")) { + throw new IOException( + "The request has the following unsupported HTTP content encoding: " + encoding); + } + } + return new String(ByteStreams.toByteArray(input), StandardCharsets.UTF_8); + } catch (IOException e) { + throw new IOException("Exception encountered when decoding request content.", e); + } + } + + private static String projectIdFromUri(String path) throws IOException { + if (path.isEmpty()) { + throw new IOException("The URI path '" + path + "' doesn't have a project ID."); + } + return path.split(":")[0]; + } + + private static String[] parseFields(String query) { + if (query != null && !query.isEmpty()) { + String[] querySplit = query.split("="); + return querySplit.length > 1 ? querySplit[1].split(",") : null; + } + return null; + } + + private static Map parseListOptions(String query) { + Map options = new HashMap<>(); + if (query != null) { + String[] args = query.split("&"); + for (String arg : args) { + String[] argEntry = arg.split("="); + switch (argEntry[0]) { + case "fields": + options.put("fields", argEntry[1].split(",")); + break; + case "filter": + options.put("filter", argEntry[1].split(" ")); + break; + case "pageToken": + // support pageToken when Cloud Resource Manager supports this (#421) + break; + case "pageSize": + // support pageSize when Cloud Resource Manager supports this (#421) + break; + } + } + } + return options; + } + + private static final String checkForProjectErrors(Project project) { + if (project.getProjectId() == null) { + return "Project ID cannot be empty."; + } + if (!isValidIdOrLabel(project.getProjectId(), 6, 30)) { + return "Project " + project.getProjectId() + " has an invalid ID." + + " See https://cloud.google.com/resource-manager/reference/rest/" + VERSION + "/projects" + + " for more information."; + } + if (project.getName() != null) { + for (char c : project.getName().toCharArray()) { + if (!PERMISSIBLE_PROJECT_NAME_PUNCTUATION.contains(c) && !Character.isLetterOrDigit(c)) { + return "Project " + project.getProjectId() + " has an invalid name." + + " See https://cloud.google.com/resource-manager/reference/rest/" + VERSION + + "/projects for more information."; + } + } + } + if (project.getLabels() != null) { + if (project.getLabels().size() > 256) { + return "Project " + project.getProjectId() + " exceeds the limit of 256 labels."; + } + for (Map.Entry entry : project.getLabels().entrySet()) { + if (!isValidIdOrLabel(entry.getKey(), 1, 63) + || !isValidIdOrLabel(entry.getValue(), 0, 63)) { + return "Project " + project.getProjectId() + " has an invalid label entry." + + " See https://cloud.google.com/resource-manager/reference/rest/" + VERSION + + "/projects for more information."; + } + } + } + return null; + } + + private static final boolean isValidIdOrLabel(String value, int minLength, int maxLength) { + for (char c : value.toCharArray()) { + if (c != '-' && !Character.isDigit(c) && (!Character.isLowerCase(c))) { + return false; + } + } + if (!value.isEmpty() && (!Character.isLetter(value.charAt(0)) || value.endsWith("-"))) { + return false; + } + return value.length() >= minLength && value.length() <= maxLength; + } + + Response create(Project project) { + String customErrorMessage = checkForProjectErrors(project); + if (customErrorMessage != null) { + return Error.INVALID_ARGUMENT.response(customErrorMessage); + } else { + project.setLifecycleState("ACTIVE"); + project.setProjectNumber(Math.abs(PROJECT_NUMBER_GENERATOR.nextLong() % Long.MAX_VALUE)); + project.setCreateTime(ISODateTimeFormat.dateTime().print(System.currentTimeMillis())); + if (projects.putIfAbsent(project.getProjectId(), project) != null) { + return Error.ALREADY_EXISTS.response( + "A project with the same project ID (" + project.getProjectId() + ") already exists."); + } + try { + String createdProjectStr = jsonFactory.toString(project); + return new Response(HTTP_OK, createdProjectStr); + } catch (IOException e) { + return Error.INTERNAL_ERROR.response("Error serializing project " + project.getProjectId()); + } + } + } + + Response delete(String projectId) { + Project project = projects.get(projectId); + if (project == null) { + // Currently the service returns 403 Permission Denied when trying to delete a project that + // doesn't exist. Here we mimic this behavior, but this line should be changed to throw a + // 404 Not Found error when the service fixes this (#440). + return Error.PERMISSION_DENIED.response( + "Error when deleting " + projectId + " because the project was not found."); + } + if (!project.getLifecycleState().equals("ACTIVE")) { + return Error.FAILED_PRECONDITION.response( + "Error when deleting " + projectId + " because the lifecycle state was not ACTIVE."); + } else { + project.setLifecycleState("DELETE_REQUESTED"); + return new Response(HTTP_OK, "{}"); + } + } + + Response get(String projectId, String[] fields) { + if (!projects.containsKey(projectId)) { + // Currently the service returns 403 Permission Denied when trying to get a project that + // doesn't exist. Here we mimic this behavior, but this line should be changed to throw a + // 404 Not Found error when the service fixes this (#440). + return Error.PERMISSION_DENIED.response("Project " + projectId + " not found."); + } + Project project = projects.get(projectId); + try { + return new Response(HTTP_OK, jsonFactory.toString(extractFields(project, fields))); + } catch (IOException e) { + return Error.INTERNAL_ERROR.response( + "Error when serializing project " + project.getProjectId()); + } + } + + Response list(Map options) { + // Use pageSize and pageToken options when Cloud Resource Manager does so (#421) + List projectsSerialized = new ArrayList<>(); + String[] filters = (String[]) options.get("filter"); + if (filters != null && !isValidFilter(filters)) { + return Error.INVALID_ARGUMENT.response("Could not parse the filter."); + } + String[] fields = (String[]) options.get("fields"); + for (Project p : projects.values()) { + Boolean includeProject = includeProject(p, filters); + if (includeProject) { + try { + projectsSerialized.add(jsonFactory.toString(extractFields(p, fields))); + } catch (IOException e) { + return Error.INTERNAL_ERROR.response( + "Error when serializing project " + p.getProjectId()); + } + } + } + StringBuilder responseBody = new StringBuilder(); + responseBody.append("{\"projects\": ["); + Joiner.on(",").appendTo(responseBody, projectsSerialized); + responseBody.append("]}"); + return new Response(HTTP_OK, responseBody.toString()); + } + + private static boolean isValidFilter(String[] filters) { + for (String filter : filters) { + String field = filter.toLowerCase().split(":")[0]; + if (!("id".equals(field) || "name".equals(field) || field.startsWith("labels."))) { + return false; + } + } + return true; + } + + private static boolean includeProject(Project project, String[] filters) { + if (filters == null) { + return true; + } + for (String filter : filters) { + String[] filterEntry = filter.toLowerCase().split(":"); + String filterType = filterEntry[0]; + if ("id".equals(filterType)) { + if (!satisfiesFilter(project.getProjectId(), filterEntry[1])) { + return false; + } + } else if ("name".equals(filterType)) { + if (!satisfiesFilter(project.getName(), filterEntry[1])) { + return false; + } + } else if (filterType.startsWith("labels.")) { + String labelKey = filterType.substring("labels.".length()); + if (project.getLabels() != null) { + String labelValue = project.getLabels().get(labelKey); + if (!satisfiesFilter(labelValue, filterEntry[1])) { + return false; + } + } + } + } + return true; + } + + private static boolean satisfiesFilter(String projectValue, String filterValue) { + if (projectValue == null) { + return false; + } + return "*".equals(filterValue) || filterValue.equals(projectValue.toLowerCase()); + } + + private static Project extractFields(Project fullProject, String[] fields) { + if (fields == null) { + return fullProject; + } + Project project = new Project(); + for (String field : fields) { + switch (field) { + case "createTime": + project.setCreateTime(fullProject.getCreateTime()); + break; + case "labels": + project.setLabels(fullProject.getLabels()); + break; + case "lifecycleState": + project.setLifecycleState(fullProject.getLifecycleState()); + break; + case "name": + project.setName(fullProject.getName()); + break; + case "parent": + project.setParent(fullProject.getParent()); + break; + case "projectId": + project.setProjectId(fullProject.getProjectId()); + break; + case "projectNumber": + project.setProjectNumber(fullProject.getProjectNumber()); + break; + } + } + return project; + } + + Response replace(String projectId, Project project) { + Project originalProject = projects.get(projectId); + if (originalProject == null) { + // Currently the service returns 403 Permission Denied when trying to replace a project that + // doesn't exist. Here we mimic this behavior, but this line should be changed to throw a + // 404 Not Found error when the service fixes this (#440). + return Error.PERMISSION_DENIED.response( + "Error when replacing " + projectId + " because the project was not found."); + } else if (!originalProject.getLifecycleState().equals("ACTIVE")) { + return Error.FAILED_PRECONDITION.response( + "Error when replacing " + projectId + " because the lifecycle state was not ACTIVE."); + } else if (!Objects.equal(originalProject.getParent(), project.getParent())) { + return Error.INVALID_ARGUMENT.response( + "The server currently only supports setting the parent once " + + "and does not allow unsetting it."); + } + originalProject.setName(project.getName()); + originalProject.setLabels(project.getLabels()); + originalProject.setParent(project.getParent()); + try { + return new Response(HTTP_OK, jsonFactory.toString(originalProject)); + } catch (IOException e) { + return Error.INTERNAL_ERROR.response("Error when serializing project " + projectId); + } + } + + Response undelete(String projectId) { + Project project = projects.get(projectId); + Response response; + if (project == null) { + // Currently the service returns 403 Permission Denied when trying to undelete a project that + // doesn't exist. Here we mimic this behavior, but this line should be changed to throw a + // 404 Not Found error when the service fixes this (#440). + response = Error.PERMISSION_DENIED.response( + "Error when undeleting " + projectId + " because the project was not found."); + } else if (!project.getLifecycleState().equals("DELETE_REQUESTED")) { + response = Error.FAILED_PRECONDITION.response("Error when undeleting " + projectId + + " because the lifecycle state was not DELETE_REQUESTED."); + } else { + project.setLifecycleState("ACTIVE"); + response = new Response(HTTP_OK, "{}"); + } + return response; + } + + private LocalResourceManagerHelper() { + try { + server = HttpServer.create(new InetSocketAddress(0), 0); + port = server.getAddress().getPort(); + server.createContext(CONTEXT, new RequestHandler()); + } catch (IOException e) { + throw new RuntimeException("Could not bind the mock Resource Manager server.", e); + } + } + + /** + * Creates a LocalResourceManagerHelper object that listens to requests on the local machine. + */ + public static LocalResourceManagerHelper create() { + return new LocalResourceManagerHelper(); + } + + /** + * Returns the port that the LocalResourceManagerHelper listens to for requests. + */ + public int port() { + return port; + } + + /** + * Starts the thread that runs the Resource Manager server. + */ + public void start() { + server.start(); + } + + /** + * Stops the thread that runs the mock Resource Manager server. + */ + public void stop() { + server.stop(1); + } + + /** + * Utility method to change the lifecycle state of the specified project. + * + * @return true if the lifecycle state was successfully updated, false otherwise. + */ + public boolean changeLifecycleState(String projectId, String lifecycleState) { + checkArgument( + "ACTIVE".equals(lifecycleState) || "DELETE_REQUESTED".equals(lifecycleState) + || "DELETE_IN_PROGRESS".equals(lifecycleState), + "Lifecycle state must be ACTIVE, DELETE_REQUESTED, or DELETE_IN_PROGRESS"); + Project project = projects.get(checkNotNull(projectId)); + if (project != null) { + project.setLifecycleState(lifecycleState); + return true; + } + return false; + } + + /** + * Utility method to remove the specified project. + * + *

This method can be used to fully remove a project (to mimic when the server completely + * deletes a project). + * + * @return true if the project was successfully deleted, false if the project didn't exist. + */ + public boolean removeProject(String projectId) { + return projects.remove(checkNotNull(projectId)) != null; + } +} diff --git a/gcloud-java-resourcemanager/src/main/java/com/google/gcloud/resourcemanager/testing/package-info.java b/gcloud-java-resourcemanager/src/main/java/com/google/gcloud/resourcemanager/testing/package-info.java new file mode 100644 index 000000000000..a2c07904ddbd --- /dev/null +++ b/gcloud-java-resourcemanager/src/main/java/com/google/gcloud/resourcemanager/testing/package-info.java @@ -0,0 +1,32 @@ +/* + * Copyright 2015 Google Inc. All Rights Reserved. + * + * 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. + */ + +/** + * A testing helper for Google Cloud Resource Manager. + * + *

A simple usage example: + *

Before the test: + *

 {@code
+ * LocalResourceManagerHelper resourceManagerHelper = LocalResourceManagerHelper.create();
+ * ResourceManager resourceManager = resourceManagerHelper.options().service();
+ * } 
+ * + *

After the test: + *

 {@code
+ * resourceManagerHelper.stop();
+ * } 
+ */ +package com.google.gcloud.resourcemanager.testing; diff --git a/gcloud-java-resourcemanager/src/test/java/com/google/gcloud/resourcemanager/LocalResourceManagerHelperTest.java b/gcloud-java-resourcemanager/src/test/java/com/google/gcloud/resourcemanager/LocalResourceManagerHelperTest.java new file mode 100644 index 000000000000..785aa88d49fb --- /dev/null +++ b/gcloud-java-resourcemanager/src/test/java/com/google/gcloud/resourcemanager/LocalResourceManagerHelperTest.java @@ -0,0 +1,550 @@ +package com.google.gcloud.resourcemanager; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import com.google.common.collect.ImmutableMap; +import com.google.gcloud.resourcemanager.testing.LocalResourceManagerHelper; +import com.google.gcloud.spi.DefaultResourceManagerRpc; +import com.google.gcloud.spi.ResourceManagerRpc; +import com.google.gcloud.spi.ResourceManagerRpc.Tuple; + +import org.junit.AfterClass; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Test; + +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; + +public class LocalResourceManagerHelperTest { + + private static final String DEFAULT_PARENT_ID = "12345"; + private static final String DEFAULT_PARENT_TYPE = "organization"; + private static final com.google.api.services.cloudresourcemanager.model.ResourceId PARENT = + new com.google.api.services.cloudresourcemanager.model.ResourceId() + .setId(DEFAULT_PARENT_ID) + .setType(DEFAULT_PARENT_TYPE); + private static final Map EMPTY_RPC_OPTIONS = ImmutableMap.of(); + private static final LocalResourceManagerHelper RESOURCE_MANAGER_HELPER = + LocalResourceManagerHelper.create(); + private static final ResourceManagerRpc rpc = new DefaultResourceManagerRpc( + ResourceManagerOptions.builder() + .host("http://localhost:" + RESOURCE_MANAGER_HELPER.port()) + .build()); + private static final com.google.api.services.cloudresourcemanager.model.Project PARTIAL_PROJECT = + new com.google.api.services.cloudresourcemanager.model.Project().setProjectId( + "partial-project"); + private static final com.google.api.services.cloudresourcemanager.model.Project COMPLETE_PROJECT = + new com.google.api.services.cloudresourcemanager.model.Project() + .setProjectId("complete-project") + .setName("full project") + .setLabels(ImmutableMap.of("k1", "v1", "k2", "v2")); + private static final com.google.api.services.cloudresourcemanager.model.Project + PROJECT_WITH_PARENT = + copyFrom(COMPLETE_PROJECT).setProjectId("project-with-parent-id").setParent(PARENT); + + @BeforeClass + public static void beforeClass() { + RESOURCE_MANAGER_HELPER.start(); + } + + private static com.google.api.services.cloudresourcemanager.model.Project copyFrom( + com.google.api.services.cloudresourcemanager.model.Project from) { + return new com.google.api.services.cloudresourcemanager.model.Project() + .setProjectId(from.getProjectId()) + .setName(from.getName()) + .setLabels(from.getLabels() != null ? ImmutableMap.copyOf(from.getLabels()) : null) + .setProjectNumber( + from.getProjectNumber() != null ? from.getProjectNumber().longValue() : null) + .setCreateTime(from.getCreateTime()) + .setLifecycleState(from.getLifecycleState()) + .setParent(from.getParent() != null ? from.getParent().clone() : null); + } + + private void clearProjects() { + for (com.google.api.services.cloudresourcemanager.model.Project project : + rpc.list(EMPTY_RPC_OPTIONS).y()) { + RESOURCE_MANAGER_HELPER.removeProject(project.getProjectId()); + } + } + + @Before + public void setUp() { + clearProjects(); + } + + @AfterClass + public static void afterClass() { + RESOURCE_MANAGER_HELPER.stop(); + } + + @Test + public void testCreate() { + com.google.api.services.cloudresourcemanager.model.Project returnedProject = + rpc.create(PARTIAL_PROJECT); + compareReadWriteFields(PARTIAL_PROJECT, returnedProject); + assertEquals("ACTIVE", returnedProject.getLifecycleState()); + assertNull(returnedProject.getLabels()); + assertNull(returnedProject.getName()); + assertNull(returnedProject.getParent()); + assertNotNull(returnedProject.getProjectNumber()); + assertNotNull(returnedProject.getCreateTime()); + try { + rpc.create(PARTIAL_PROJECT); + fail("Should fail, project already exists."); + } catch (ResourceManagerException e) { + assertEquals(409, e.code()); + assertTrue(e.getMessage().startsWith("A project with the same project ID") + && e.getMessage().endsWith("already exists.")); + } + returnedProject = rpc.create(PROJECT_WITH_PARENT); + compareReadWriteFields(PROJECT_WITH_PARENT, returnedProject); + assertEquals("ACTIVE", returnedProject.getLifecycleState()); + assertNotNull(returnedProject.getProjectNumber()); + assertNotNull(returnedProject.getCreateTime()); + } + + @Test + public void testIsInvalidProjectId() { + com.google.api.services.cloudresourcemanager.model.Project project = + new com.google.api.services.cloudresourcemanager.model.Project(); + String invalidIDMessageSubstring = "invalid ID"; + expectInvalidArgumentException(project, "Project ID cannot be empty."); + project.setProjectId("abcde"); + expectInvalidArgumentException(project, invalidIDMessageSubstring); + project.setProjectId("this-project-id-is-more-than-thirty-characters-long"); + expectInvalidArgumentException(project, invalidIDMessageSubstring); + project.setProjectId("project-id-with-invalid-character-?"); + expectInvalidArgumentException(project, invalidIDMessageSubstring); + project.setProjectId("-invalid-start-character"); + expectInvalidArgumentException(project, invalidIDMessageSubstring); + project.setProjectId("invalid-ending-character-"); + expectInvalidArgumentException(project, invalidIDMessageSubstring); + project.setProjectId("some-valid-project-id-12345"); + rpc.create(project); + assertNotNull(rpc.get(project.getProjectId(), EMPTY_RPC_OPTIONS)); + } + + private void expectInvalidArgumentException( + com.google.api.services.cloudresourcemanager.model.Project project, + String errorMessageSubstring) { + try { + rpc.create(project); + fail("Should fail because of an invalid argument."); + } catch (ResourceManagerException e) { + assertEquals(400, e.code()); + assertTrue(e.getMessage().contains(errorMessageSubstring)); + } + } + + @Test + public void testIsInvalidProjectName() { + com.google.api.services.cloudresourcemanager.model.Project project = + new com.google.api.services.cloudresourcemanager.model.Project().setProjectId( + "some-project-id"); + rpc.create(project); + assertNull(rpc.get(project.getProjectId(), EMPTY_RPC_OPTIONS).getName()); + RESOURCE_MANAGER_HELPER.removeProject(project.getProjectId()); + project.setName("This is a valid name-'\"!"); + rpc.create(project); + assertEquals(project.getName(), rpc.get(project.getProjectId(), EMPTY_RPC_OPTIONS).getName()); + RESOURCE_MANAGER_HELPER.removeProject(project.getProjectId()); + project.setName("invalid-character-,"); + try { + rpc.create(project); + fail("Should fail because of invalid project name."); + } catch (ResourceManagerException e) { + assertEquals(400, e.code()); + assertTrue(e.getMessage().contains("invalid name")); + } + } + + @Test + public void testIsInvalidProjectLabels() { + com.google.api.services.cloudresourcemanager.model.Project project = + new com.google.api.services.cloudresourcemanager.model.Project().setProjectId( + "some-valid-project-id"); + String invalidLabelMessageSubstring = "invalid label entry"; + project.setLabels(ImmutableMap.of("", "v1")); + expectInvalidArgumentException(project, invalidLabelMessageSubstring); + project.setLabels(ImmutableMap.of( + "this-project-label-is-more-than-sixty-three-characters-long-so-it-should-fail", "v1")); + expectInvalidArgumentException(project, invalidLabelMessageSubstring); + project.setLabels(ImmutableMap.of( + "k1", "this-project-label-is-more-than-sixty-three-characters-long-so-it-should-fail")); + expectInvalidArgumentException(project, invalidLabelMessageSubstring); + project.setLabels(ImmutableMap.of("k1?", "v1")); + expectInvalidArgumentException(project, invalidLabelMessageSubstring); + project.setLabels(ImmutableMap.of("k1", "v1*")); + expectInvalidArgumentException(project, invalidLabelMessageSubstring); + project.setLabels(ImmutableMap.of("-k1", "v1")); + expectInvalidArgumentException(project, invalidLabelMessageSubstring); + project.setLabels(ImmutableMap.of("k1", "-v1")); + expectInvalidArgumentException(project, invalidLabelMessageSubstring); + project.setLabels(ImmutableMap.of("k1-", "v1")); + expectInvalidArgumentException(project, invalidLabelMessageSubstring); + project.setLabels(ImmutableMap.of("k1", "v1-")); + expectInvalidArgumentException(project, invalidLabelMessageSubstring); + Map tooManyLabels = new HashMap<>(); + for (int i = 0; i < 257; i++) { + tooManyLabels.put("k" + Integer.toString(i), "v" + Integer.toString(i)); + } + project.setLabels(tooManyLabels); + expectInvalidArgumentException(project, "exceeds the limit of 256 labels"); + project.setLabels(ImmutableMap.of("k-1", "")); + rpc.create(project); + assertNotNull(rpc.get(project.getProjectId(), EMPTY_RPC_OPTIONS)); + assertTrue(rpc.get(project.getProjectId(), EMPTY_RPC_OPTIONS) + .getLabels() + .get("k-1") + .isEmpty()); + } + + @Test + public void testDelete() { + rpc.create(COMPLETE_PROJECT); + rpc.delete(COMPLETE_PROJECT.getProjectId()); + assertEquals( + "DELETE_REQUESTED", + rpc.get(COMPLETE_PROJECT.getProjectId(), EMPTY_RPC_OPTIONS).getLifecycleState()); + try { + rpc.delete("some-nonexistant-project-id"); + fail("Should fail because the project doesn't exist."); + } catch (ResourceManagerException e) { + assertEquals(403, e.code()); + assertTrue(e.getMessage().contains("not found.")); + } + } + + @Test + public void testDeleteWhenDeleteInProgress() { + rpc.create(COMPLETE_PROJECT); + RESOURCE_MANAGER_HELPER.changeLifecycleState( + COMPLETE_PROJECT.getProjectId(), "DELETE_IN_PROGRESS"); + try { + rpc.delete(COMPLETE_PROJECT.getProjectId()); + fail("Should fail because the project is not ACTIVE."); + } catch (ResourceManagerException e) { + assertEquals(400, e.code()); + assertTrue(e.getMessage().contains("the lifecycle state was not ACTIVE")); + } + } + + @Test + public void testDeleteWhenDeleteRequested() { + rpc.create(COMPLETE_PROJECT); + RESOURCE_MANAGER_HELPER.changeLifecycleState( + COMPLETE_PROJECT.getProjectId(), "DELETE_REQUESTED"); + try { + rpc.delete(COMPLETE_PROJECT.getProjectId()); + fail("Should fail because the project is not ACTIVE."); + } catch (ResourceManagerException e) { + assertEquals(400, e.code()); + assertTrue(e.getMessage().contains("the lifecycle state was not ACTIVE")); + } + } + + @Test + public void testGet() { + rpc.create(COMPLETE_PROJECT); + com.google.api.services.cloudresourcemanager.model.Project returnedProject = + rpc.get(COMPLETE_PROJECT.getProjectId(), EMPTY_RPC_OPTIONS); + compareReadWriteFields(COMPLETE_PROJECT, returnedProject); + RESOURCE_MANAGER_HELPER.removeProject(COMPLETE_PROJECT.getProjectId()); + try { + rpc.get(COMPLETE_PROJECT.getProjectId(), EMPTY_RPC_OPTIONS); + fail("Should fail because the project doesn't exist."); + } catch (ResourceManagerException e) { + assertEquals(403, e.code()); + assertTrue(e.getMessage().contains("not found")); + } + } + + @Test + public void testGetWithOptions() { + com.google.api.services.cloudresourcemanager.model.Project originalProject = + rpc.create(COMPLETE_PROJECT); + Map rpcOptions = new HashMap<>(); + rpcOptions.put(ResourceManagerRpc.Option.FIELDS, "projectId,name,createTime"); + com.google.api.services.cloudresourcemanager.model.Project returnedProject = + rpc.get(COMPLETE_PROJECT.getProjectId(), rpcOptions); + assertFalse(COMPLETE_PROJECT.equals(returnedProject)); + assertEquals(COMPLETE_PROJECT.getProjectId(), returnedProject.getProjectId()); + assertEquals(COMPLETE_PROJECT.getName(), returnedProject.getName()); + assertEquals(originalProject.getCreateTime(), returnedProject.getCreateTime()); + assertNull(returnedProject.getParent()); + assertNull(returnedProject.getProjectNumber()); + assertNull(returnedProject.getLifecycleState()); + assertNull(returnedProject.getLabels()); + } + + @Test + public void testList() { + Tuple> projects = + rpc.list(EMPTY_RPC_OPTIONS); + assertNull(projects.x()); // change this when #421 is resolved + assertFalse(projects.y().iterator().hasNext()); + rpc.create(COMPLETE_PROJECT); + RESOURCE_MANAGER_HELPER.changeLifecycleState( + COMPLETE_PROJECT.getProjectId(), "DELETE_REQUESTED"); + rpc.create(PROJECT_WITH_PARENT); + projects = rpc.list(EMPTY_RPC_OPTIONS); + Iterator it = + projects.y().iterator(); + compareReadWriteFields(COMPLETE_PROJECT, it.next()); + compareReadWriteFields(PROJECT_WITH_PARENT, it.next()); + } + + @Test + public void testListFieldOptions() { + Map rpcOptions = new HashMap<>(); + rpcOptions.put(ResourceManagerRpc.Option.FIELDS, "projectId,name,labels"); + rpcOptions.put(ResourceManagerRpc.Option.PAGE_TOKEN, "somePageToken"); + rpcOptions.put(ResourceManagerRpc.Option.PAGE_SIZE, 1); + rpc.create(PROJECT_WITH_PARENT); + Tuple> projects = + rpc.list(rpcOptions); + com.google.api.services.cloudresourcemanager.model.Project returnedProject = + projects.y().iterator().next(); + assertFalse(PROJECT_WITH_PARENT.equals(returnedProject)); + assertEquals(PROJECT_WITH_PARENT.getProjectId(), returnedProject.getProjectId()); + assertEquals(PROJECT_WITH_PARENT.getName(), returnedProject.getName()); + assertEquals(PROJECT_WITH_PARENT.getLabels(), returnedProject.getLabels()); + assertNull(returnedProject.getParent()); + assertNull(returnedProject.getProjectNumber()); + assertNull(returnedProject.getLifecycleState()); + assertNull(returnedProject.getCreateTime()); + } + + @Test + public void testListFilterOptions() { + Map rpcFilterOptions = new HashMap<>(); + rpcFilterOptions.put( + ResourceManagerRpc.Option.FILTER, "id:* name:myProject labels.color:blue LABELS.SIZE:*"); + com.google.api.services.cloudresourcemanager.model.Project matchingProject = + new com.google.api.services.cloudresourcemanager.model.Project() + .setProjectId("matching-project") + .setName("MyProject") + .setLabels(ImmutableMap.of("color", "blue", "size", "big")); + com.google.api.services.cloudresourcemanager.model.Project nonMatchingProject1 = + new com.google.api.services.cloudresourcemanager.model.Project() + .setProjectId("non-matching-project1") + .setName("myProject"); + nonMatchingProject1.setLabels(ImmutableMap.of("color", "blue")); + com.google.api.services.cloudresourcemanager.model.Project nonMatchingProject2 = + new com.google.api.services.cloudresourcemanager.model.Project() + .setProjectId("non-matching-project2") + .setName("myProj") + .setLabels(ImmutableMap.of("color", "blue", "size", "big")); + com.google.api.services.cloudresourcemanager.model.Project nonMatchingProject3 = + new com.google.api.services.cloudresourcemanager.model.Project().setProjectId( + "non-matching-project3"); + rpc.create(matchingProject); + rpc.create(nonMatchingProject1); + rpc.create(nonMatchingProject2); + rpc.create(nonMatchingProject3); + for (com.google.api.services.cloudresourcemanager.model.Project p : + rpc.list(rpcFilterOptions).y()) { + assertFalse(p.equals(nonMatchingProject1)); + assertFalse(p.equals(nonMatchingProject2)); + compareReadWriteFields(matchingProject, p); + } + } + + @Test + public void testReplace() { + com.google.api.services.cloudresourcemanager.model.Project createdProject = + rpc.create(COMPLETE_PROJECT); + String newName = "new name"; + Map newLabels = ImmutableMap.of("new k1", "new v1"); + com.google.api.services.cloudresourcemanager.model.Project anotherCompleteProject = + new com.google.api.services.cloudresourcemanager.model.Project() + .setProjectId(COMPLETE_PROJECT.getProjectId()) + .setName(newName) + .setLabels(newLabels) + .setProjectNumber(987654321L) + .setCreateTime("2000-01-01T00:00:00.001Z") + .setLifecycleState("DELETE_REQUESTED"); + com.google.api.services.cloudresourcemanager.model.Project returnedProject = + rpc.replace(anotherCompleteProject); + compareReadWriteFields(anotherCompleteProject, returnedProject); + assertEquals(createdProject.getProjectNumber(), returnedProject.getProjectNumber()); + assertEquals(createdProject.getCreateTime(), returnedProject.getCreateTime()); + assertEquals(createdProject.getLifecycleState(), returnedProject.getLifecycleState()); + com.google.api.services.cloudresourcemanager.model.Project nonexistantProject = + new com.google.api.services.cloudresourcemanager.model.Project(); + nonexistantProject.setProjectId("some-project-id-that-does-not-exist"); + try { + rpc.replace(nonexistantProject); + fail("Should fail because the project doesn't exist."); + } catch (ResourceManagerException e) { + assertEquals(403, e.code()); + assertTrue(e.getMessage().contains("the project was not found")); + } + } + + @Test + public void testReplaceWhenDeleteRequested() { + rpc.create(COMPLETE_PROJECT); + rpc.delete(COMPLETE_PROJECT.getProjectId()); + com.google.api.services.cloudresourcemanager.model.Project anotherProject = + new com.google.api.services.cloudresourcemanager.model.Project().setProjectId( + COMPLETE_PROJECT.getProjectId()); + try { + rpc.replace(anotherProject); + fail("Should fail because the project is not ACTIVE."); + } catch (ResourceManagerException e) { + assertEquals(400, e.code()); + assertTrue(e.getMessage().contains("the lifecycle state was not ACTIVE")); + } + } + + @Test + public void testReplaceWhenDeleteInProgress() { + rpc.create(COMPLETE_PROJECT); + RESOURCE_MANAGER_HELPER.changeLifecycleState( + COMPLETE_PROJECT.getProjectId(), "DELETE_IN_PROGRESS"); + com.google.api.services.cloudresourcemanager.model.Project anotherProject = + new com.google.api.services.cloudresourcemanager.model.Project().setProjectId( + COMPLETE_PROJECT.getProjectId()); + try { + rpc.replace(anotherProject); + fail("Should fail because the project is not ACTIVE."); + } catch (ResourceManagerException e) { + assertEquals(400, e.code()); + assertTrue(e.getMessage().contains("the lifecycle state was not ACTIVE")); + } + } + + @Test + public void testReplaceAddingParent() { + rpc.create(COMPLETE_PROJECT); + com.google.api.services.cloudresourcemanager.model.Project anotherProject = + new com.google.api.services.cloudresourcemanager.model.Project() + .setProjectId(COMPLETE_PROJECT.getProjectId()) + .setParent(PARENT); + try { + rpc.replace(anotherProject); + fail("Should fail because the project's parent was modified after creation."); + } catch (ResourceManagerException e) { + assertEquals(400, e.code()); + assertEquals( + "The server currently only supports setting the parent once " + + "and does not allow unsetting it.", + e.getMessage()); + } + } + + @Test + public void testReplaceRemovingParent() { + rpc.create(PROJECT_WITH_PARENT); + com.google.api.services.cloudresourcemanager.model.Project anotherProject = + new com.google.api.services.cloudresourcemanager.model.Project().setProjectId( + PROJECT_WITH_PARENT.getProjectId()); + try { + rpc.replace(anotherProject); + fail("Should fail because the project's parent was unset."); + } catch (ResourceManagerException e) { + assertEquals(400, e.code()); + assertEquals( + "The server currently only supports setting the parent once " + + "and does not allow unsetting it.", + e.getMessage()); + } + } + + @Test + public void testUndelete() { + rpc.create(COMPLETE_PROJECT); + rpc.delete(COMPLETE_PROJECT.getProjectId()); + assertEquals( + "DELETE_REQUESTED", + rpc.get(COMPLETE_PROJECT.getProjectId(), EMPTY_RPC_OPTIONS).getLifecycleState()); + rpc.undelete(COMPLETE_PROJECT.getProjectId()); + com.google.api.services.cloudresourcemanager.model.Project revivedProject = + rpc.get(COMPLETE_PROJECT.getProjectId(), EMPTY_RPC_OPTIONS); + compareReadWriteFields(COMPLETE_PROJECT, revivedProject); + assertEquals("ACTIVE", revivedProject.getLifecycleState()); + try { + rpc.undelete("invalid-project-id"); + fail("Should fail because the project doesn't exist."); + } catch (ResourceManagerException e) { + assertEquals(403, e.code()); + assertTrue(e.getMessage().contains("the project was not found")); + } + } + + @Test + public void testUndeleteWhenActive() { + rpc.create(COMPLETE_PROJECT); + try { + rpc.undelete(COMPLETE_PROJECT.getProjectId()); + fail("Should fail because the project is not deleted."); + } catch (ResourceManagerException e) { + assertEquals(400, e.code()); + assertTrue(e.getMessage().contains("lifecycle state was not DELETE_REQUESTED")); + } + } + + @Test + public void testUndeleteWhenDeleteInProgress() { + rpc.create(COMPLETE_PROJECT); + RESOURCE_MANAGER_HELPER.changeLifecycleState( + COMPLETE_PROJECT.getProjectId(), "DELETE_IN_PROGRESS"); + try { + rpc.undelete(COMPLETE_PROJECT.getProjectId()); + fail("Should fail because the project is in the process of being deleted."); + } catch (ResourceManagerException e) { + assertEquals(400, e.code()); + assertTrue(e.getMessage().contains("lifecycle state was not DELETE_REQUESTED")); + } + } + + @Test + public void testChangeLifecycleStatus() { + assertFalse(RESOURCE_MANAGER_HELPER.changeLifecycleState( + COMPLETE_PROJECT.getProjectId(), "DELETE_IN_PROGRESS")); + rpc.create(COMPLETE_PROJECT); + assertTrue(RESOURCE_MANAGER_HELPER.changeLifecycleState( + COMPLETE_PROJECT.getProjectId(), "DELETE_IN_PROGRESS")); + assertEquals( + "DELETE_IN_PROGRESS", + rpc.get(COMPLETE_PROJECT.getProjectId(), EMPTY_RPC_OPTIONS).getLifecycleState()); + try { + RESOURCE_MANAGER_HELPER.changeLifecycleState( + COMPLETE_PROJECT.getProjectId(), "INVALID_STATE"); + fail("Should fail because of an invalid lifecycle state"); + } catch (IllegalArgumentException e) { + // ignore + } + } + + @Test + public void testRemoveProject() { + assertFalse(RESOURCE_MANAGER_HELPER.removeProject(COMPLETE_PROJECT.getProjectId())); + rpc.create(COMPLETE_PROJECT); + assertTrue(RESOURCE_MANAGER_HELPER.removeProject(COMPLETE_PROJECT.getProjectId())); + try { + rpc.get(COMPLETE_PROJECT.getProjectId(), EMPTY_RPC_OPTIONS); + fail("Project shouldn't exist."); + } catch (ResourceManagerException e) { + assertEquals(403, e.code()); + assertTrue(e.getMessage().contains("not found.")); + } + } + + private void compareReadWriteFields( + com.google.api.services.cloudresourcemanager.model.Project expected, + com.google.api.services.cloudresourcemanager.model.Project actual) { + assertEquals(expected.getProjectId(), actual.getProjectId()); + assertEquals(expected.getName(), actual.getName()); + assertEquals(expected.getLabels(), actual.getLabels()); + assertEquals(expected.getParent(), actual.getParent()); + } +}