From a1ec7f46c9e4e53ad185039011f49113de48c53a Mon Sep 17 00:00:00 2001 From: Martin Derka Date: Fri, 19 Feb 2016 10:32:17 -0800 Subject: [PATCH 1/3] Added local implementation of the service and tests. --- .../google/gcloud/testing/LocalDnsHelper.java | 1426 +++++++++++++++++ .../testing/OptionParsersAndExtractors.java | 279 ++++ .../gcloud/testing/LocalDnsHelperTest.java | 955 +++++++++++ 3 files changed, 2660 insertions(+) create mode 100644 gcloud-java-dns/src/main/java/com/google/gcloud/testing/LocalDnsHelper.java create mode 100644 gcloud-java-dns/src/main/java/com/google/gcloud/testing/OptionParsersAndExtractors.java create mode 100644 gcloud-java-dns/src/test/java/com/google/gcloud/testing/LocalDnsHelperTest.java diff --git a/gcloud-java-dns/src/main/java/com/google/gcloud/testing/LocalDnsHelper.java b/gcloud-java-dns/src/main/java/com/google/gcloud/testing/LocalDnsHelper.java new file mode 100644 index 000000000000..2d30cf9e5d90 --- /dev/null +++ b/gcloud-java-dns/src/main/java/com/google/gcloud/testing/LocalDnsHelper.java @@ -0,0 +1,1426 @@ +/* + * Copyright 2016 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. + */ + +package com.google.gcloud.testing; + +import static com.google.common.base.Preconditions.checkNotNull; +import static java.net.HttpURLConnection.HTTP_NO_CONTENT; +import static java.net.HttpURLConnection.HTTP_OK; + +import com.google.api.client.json.JsonFactory; +import com.google.api.client.repackaged.com.google.common.base.Joiner; +import com.google.api.services.dns.model.Change; +import com.google.api.services.dns.model.ManagedZone; +import com.google.api.services.dns.model.Project; +import com.google.api.services.dns.model.Quota; +import com.google.api.services.dns.model.ResourceRecordSet; +import com.google.common.base.Function; +import com.google.common.base.MoreObjects; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSet; +import com.google.common.collect.Lists; +import com.google.common.collect.Sets; +import com.google.common.io.ByteStreams; +import com.google.gcloud.dns.DnsOptions; + +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.math.BigInteger; +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.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.NavigableSet; +import java.util.Objects; +import java.util.Random; +import java.util.Set; +import java.util.TreeMap; +import java.util.TreeSet; +import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.concurrent.ConcurrentSkipListMap; +import java.util.logging.Level; +import java.util.logging.Logger; +import java.util.zip.GZIPInputStream; + +import javax.annotation.Nullable; + +/** + * A utility to create local Google Cloud DNS mock. + * + *

The mock runs in a separate thread, listening for HTTP requests on the local machine at an + * ephemeral port. + * + *

While the mock attempts to simulate the service, there are some differences in the behaviour. + * The mock will accept any project ID and never returns a notFound or another error because of + * project ID. It assumes that all project IDs exists and that the user has all the necessary + * privileges to manipulate any project. Similarly, the local simulation does not work with any + * verification of domain name ownership. Any request for creating a managed zone will be approved. + * The mock does not track quota and will allow the user to exceed it. The mock provides only basic + * validation of the DNS data for records of type A and AAAA. It does not validate any other record + * types. + */ +public class LocalDnsHelper { + + private final ConcurrentSkipListMap projects + = new ConcurrentSkipListMap<>(); + private static final URI BASE_CONTEXT; + private static final Logger log = Logger.getLogger(LocalDnsHelper.class.getName()); + private static final JsonFactory jsonFactory = + new com.google.api.client.json.jackson.JacksonFactory(); + private static final Random ID_GENERATOR = new Random(); + private static final String VERSION = "v1"; + private static final String CONTEXT = "/" + VERSION + "/projects"; + private static final Set SUPPORTED_COMPRESSION_ENCODINGS = + ImmutableSet.of("gzip", "x-gzip"); + private static final List TYPES = ImmutableList.of("A", "AAAA", "CNAME", "MX", "NAPTR", + "NS", "PTR", "SOA", "SPF", "SRV", "TXT"); + + static { + try { + BASE_CONTEXT = new URI(CONTEXT); + } catch (URISyntaxException e) { + throw new RuntimeException( + "Could not initialize LocalDnsHelper due to URISyntaxException.", e); + } + } + + private long delayChange; + private final HttpServer server; + private final int port; + + /** + * For matching URLs to operations. + */ + private enum CallRegex { + CHANGE_CREATE("POST", "/[^/]+/managedZones/[^/]+/changes"), + CHANGE_GET("GET", "/[^/]+/managedZones/[^/]+/changes/[^/]+"), + CHANGE_LIST("GET", "/[^/]+/managedZones/[^/]+/changes"), + ZONE_CREATE("POST", "/[^/]+/managedZones"), + ZONE_DELETE("DELETE", "/[^/]+/managedZones/[^/]+"), + ZONE_GET("GET", "/[^/]+/managedZones/[^/]+"), + ZONE_LIST("GET", "/[^/]+/managedZones"), + PROJECT_GET("GET", "/[^/]+"), + RECORD_LIST("GET", "/[^/]+/managedZones/[^/]+/rrsets"); + + private String method; + private String pathRegex; + + CallRegex(String method, String pathRegex) { + this.pathRegex = pathRegex; + this.method = method; + } + } + + /** + * Wraps DNS data by adding a timestamp and id which is used for paging and listing. + */ + static class RrsetWrapper { + static final Function WRAP_FUNCTION = + new Function() { + @Nullable + @Override + public RrsetWrapper apply(@Nullable ResourceRecordSet input) { + return new RrsetWrapper(input); + } + }; + private final ResourceRecordSet rrset; + private final Long timestamp = System.currentTimeMillis(); + private String id; + + RrsetWrapper(ResourceRecordSet rrset) { + // The constructor creates a copy in order to prevent side effects. + this.rrset = new ResourceRecordSet(); + this.rrset.setName(rrset.getName()); + this.rrset.setTtl(rrset.getTtl()); + this.rrset.setRrdatas(ImmutableList.copyOf(rrset.getRrdatas())); + this.rrset.setType(rrset.getType()); + } + + void setId(String id) { + this.id = id; + } + + String id() { + return id; + } + + /** + * Equals does not care about the listing id and timestamp metadata, just the rrset. + */ + @Override + public boolean equals(Object other) { + return (other instanceof RrsetWrapper) && Objects.equals(rrset, ((RrsetWrapper) other).rrset); + } + + @Override + public int hashCode() { + return Objects.hash(rrset); + } + + ResourceRecordSet rrset() { + return rrset; + } + + @Override + public String toString() { + return MoreObjects.toStringHelper(this) + .add("rrset", rrset) + .add("timestamp", timestamp) + .add("id", id) + .toString(); + } + } + + /** + * Associates a project with a collection of ManagedZones. Thread safe. + */ + static class ProjectContainer { + private final Project project; + private final ConcurrentSkipListMap zones = + new ConcurrentSkipListMap<>(); + + ProjectContainer(Project project) { + this.project = project; + } + + Project project() { + return project; + } + + ConcurrentSkipListMap zones() { + return zones; + } + } + + /** + * Associates a zone with a collection of changes and dns record. Thread safe. + */ + static class ZoneContainer { + private final ManagedZone zone; + /** + * DNS records are held in a map to allow for atomic replacement of record sets when applying + * changes. The key for the map is always the zone name. The collection of records is immutable + * and must always exist, i.e., dnsRecords.get(zone.getName()) is never null. + */ + private final ConcurrentSkipListMap> + dnsRecords = new ConcurrentSkipListMap<>(); + private final ConcurrentLinkedQueue changes = new ConcurrentLinkedQueue<>(); + + ZoneContainer(ManagedZone zone) { + this.zone = zone; + this.dnsRecords.put(zone.getName(), ImmutableList.of()); + } + + ManagedZone zone() { + return zone; + } + + ConcurrentSkipListMap> dnsRecords() { + return dnsRecords; + } + + ConcurrentLinkedQueue changes() { + return changes; + } + + Change findChange(String changeId) { + for (Change current : changes) { + if (changeId.equals(current.getId())) { + return current; + } + } + return null; + } + } + + 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 { + REQUIRED(400, "global", "required", "REQUIRED"), + INTERNAL_ERROR(500, "global", "internalError", "INTERNAL_ERROR"), + BAD_REQUEST(400, "global", "badRequest", "BAD_REQUEST"), + INVALID(400, "global", "invalid", "INVALID"), + NOT_AVAILABLE(400, "global", "managedZoneDnsNameNotAvailable", "NOT_AVAILABLE"), + NOT_FOUND(404, "global", "notFound", "NOT_FOUND"), + ALREADY_EXISTS(409, "global", "alreadyExists", "ALREADY_EXISTS"), + CONDITION_NOT_MET(412, "global", "conditionNotMet", "CONDITION_NOT_MET"), + INVALID_ZONE_APEX(400, "global", "invalidZoneApex", "INVALID_ZONE_APEX"); + + 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 { + + /** + * Chooses the proper handler for a request. + */ + private Response pickHandler(HttpExchange exchange, CallRegex regex) { + switch (regex) { + case CHANGE_GET: + return handleChangeGet(exchange); + case CHANGE_LIST: + return handleChangeList(exchange); + case ZONE_GET: + return handleZoneGet(exchange); + case ZONE_DELETE: + return handleZoneDelete(exchange); + case ZONE_LIST: + return handleZoneList(exchange); + case PROJECT_GET: + return handleProjectGet(exchange); + case RECORD_LIST: + return handleDnsRecordList(exchange); + case ZONE_CREATE: + try { + return handleZoneCreate(exchange); + } catch (IOException ex) { + return Error.BAD_REQUEST.response(ex.getMessage()); + } + case CHANGE_CREATE: + try { + return handleChangeCreate(exchange); + } catch (IOException ex) { + return Error.BAD_REQUEST.response(ex.getMessage()); + } + default: + return Error.INTERNAL_ERROR.response("Operation without a handler."); + } + } + + @Override + public void handle(HttpExchange exchange) throws IOException { + String requestMethod = exchange.getRequestMethod(); + String rawPath = exchange.getRequestURI().getRawPath(); + for (CallRegex regex : CallRegex.values()) { + if (requestMethod.equals(regex.method) && rawPath.matches(regex.pathRegex)) { + // there is a match, pass the handling accordingly + Response response = pickHandler(exchange, regex); + writeResponse(exchange, response); + return; // only one match is possible + } + } + // could not be matched, the service returns 404 page not found here + writeResponse(exchange, Error.NOT_FOUND.response("The url does not match any API call.")); + } + + // todo(mderka) Test handlers using gcloud-java-dns. Issue #665. + private Response handleZoneDelete(HttpExchange exchange) { + String path = BASE_CONTEXT.relativize(exchange.getRequestURI()).getPath(); + String[] tokens = path.split("/"); + String projectId = tokens[1]; + String zoneName = tokens[3]; + return deleteZone(projectId, zoneName); + } + + private Response handleZoneGet(HttpExchange exchange) { + String path = BASE_CONTEXT.relativize(exchange.getRequestURI()).getPath(); + String[] tokens = path.split("/"); + String projectId = tokens[1]; + String zoneName = tokens[3]; + String query = BASE_CONTEXT.relativize(exchange.getRequestURI()).getQuery(); + String[] fields = OptionParsersAndExtractors.parseGetOptions(query); + return getZone(projectId, zoneName, fields); + } + + private Response handleZoneList(HttpExchange exchange) { + String path = BASE_CONTEXT.relativize(exchange.getRequestURI()).getPath(); + String[] tokens = path.split("/"); + String projectId = tokens[1]; + String query = BASE_CONTEXT.relativize(exchange.getRequestURI()).getQuery(); + Map options = OptionParsersAndExtractors.parseListZonesOptions(query); + return listZones(projectId, options); + } + + private Response handleProjectGet(HttpExchange exchange) { + String path = BASE_CONTEXT.relativize(exchange.getRequestURI()).getPath(); + String[] tokens = path.split("/"); + String projectId = tokens[1]; + String query = BASE_CONTEXT.relativize(exchange.getRequestURI()).getQuery(); + String[] fields = OptionParsersAndExtractors.parseGetOptions(query); + return getProject(projectId, fields); + } + + /** + * @throws IOException if the request cannot be parsed. + */ + private Response handleChangeCreate(HttpExchange exchange) throws IOException { + String path = BASE_CONTEXT.relativize(exchange.getRequestURI()).getPath(); + String[] tokens = path.split("/"); + String projectId = tokens[1]; + String zoneName = tokens[3]; + String query = BASE_CONTEXT.relativize(exchange.getRequestURI()).getQuery(); + String[] fields = OptionParsersAndExtractors.parseGetOptions(query); + String requestBody = decodeContent(exchange.getRequestHeaders(), exchange.getRequestBody()); + Change change = jsonFactory.fromString(requestBody, Change.class); + return createChange(projectId, zoneName, change, fields); + } + + private Response handleChangeGet(HttpExchange exchange) { + String path = BASE_CONTEXT.relativize(exchange.getRequestURI()).getPath(); + String[] tokens = path.split("/"); + String projectId = tokens[1]; + String zoneName = tokens[3]; + String changeId = tokens[5]; + String query = BASE_CONTEXT.relativize(exchange.getRequestURI()).getQuery(); + String[] fields = OptionParsersAndExtractors.parseGetOptions(query); + return getChange(projectId, zoneName, changeId, fields); + } + + private Response handleChangeList(HttpExchange exchange) { + String path = BASE_CONTEXT.relativize(exchange.getRequestURI()).getPath(); + String[] tokens = path.split("/"); + String projectId = tokens[1]; + String zoneName = tokens[3]; + String query = BASE_CONTEXT.relativize(exchange.getRequestURI()).getQuery(); + Map options = OptionParsersAndExtractors.parseListChangesOptions(query); + return listChanges(projectId, zoneName, options); + } + + private Response handleDnsRecordList(HttpExchange exchange) { + String path = BASE_CONTEXT.relativize(exchange.getRequestURI()).getPath(); + String[] tokens = path.split("/"); + String projectId = tokens[1]; + String zoneName = tokens[3]; + String query = BASE_CONTEXT.relativize(exchange.getRequestURI()).getQuery(); + Map options = OptionParsersAndExtractors.parseListDnsRecordsOptions(query); + return listDnsRecords(projectId, zoneName, options); + } + + /** + * @throws IOException if the request cannot be parsed. + */ + private Response handleZoneCreate(HttpExchange exchange) throws IOException { + String path = BASE_CONTEXT.relativize(exchange.getRequestURI()).getPath(); + String[] tokens = path.split("/"); + String projectId = tokens[1]; + String query = BASE_CONTEXT.relativize(exchange.getRequestURI()).getQuery(); + String[] options = OptionParsersAndExtractors.parseGetOptions(query); + String requestBody = decodeContent(exchange.getRequestHeaders(), exchange.getRequestBody()); + ManagedZone zone; + try { + // IllegalArgumentException if the request body is an empty string + zone = jsonFactory.fromString(requestBody, ManagedZone.class); + } catch (IllegalArgumentException ex) { + return Error.REQUIRED.response( + "The 'entity.managedZone' parameter is required but was missing."); + } + return createZone(projectId, zone, options); + } + } + + private LocalDnsHelper(long delay) { + this.delayChange = delay; // 0 makes this synchronous + 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 DNS server.", e); + } + } + + /** + * Accessor for testing purposes. + */ + ConcurrentSkipListMap projects() { + return projects; + } + + /** + * Creates new {@link LocalDnsHelper} instance that listens to requests on the local machine. This + * instance processes changes separate threads. The parameter determines how long a thread should + * wait before processing a change. If it is set to 0, the threading is turned off and the mock + * will behave synchronously. + * + * @param delay delay for processing changes in ms or 0 for synchronous processing + */ + public static LocalDnsHelper create(Long delay) { + return new LocalDnsHelper(delay); + } + + /** + * Returns a DnsOptions instance that sets the host to use the mock server. + */ + public DnsOptions options() { + return DnsOptions.builder().host("http://localhost:" + port).build(); + } + + /** + * Starts the thread that runs the local DNS server. + */ + public void start() { + server.start(); + } + + /** + * Stops the thread that runs the mock DNS server. + */ + public void stop() { + server.stop(1); + } + + private static void writeResponse(HttpExchange exchange, Response response) { + exchange.getResponseHeaders().set("Content-type", "application/json; charset=UTF-8"); + OutputStream outputStream = exchange.getResponseBody(); + try { + exchange.getResponseHeaders().add("Connection", "close"); + 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); + } + } + + /** + * Decodes content of the HttpRequest. + */ + 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); + } + } + + /** + * Generates a JSON response. Tested. + */ + static Response toListResponse(List serializedObjects, String pageToken, + boolean includePageToken) { + // start building response + StringBuilder responseBody = new StringBuilder(); + responseBody.append("{\"projects\": ["); + Joiner.on(",").appendTo(responseBody, serializedObjects); + responseBody.append("]"); + // add page token only if exists and is asked for + if (pageToken != null && includePageToken) { + responseBody.append(",\"pageToken\": \"").append(pageToken).append("\""); + } + responseBody.append("}"); + return new Response(HTTP_OK, responseBody.toString()); + } + + /** + * Prepares DNS records that are created by default for each zone. + */ + private static ImmutableList defaultRecords(ManagedZone zone) { + ResourceRecordSet soa = new ResourceRecordSet(); + soa.setTtl(21600); + soa.setName(zone.getDnsName()); + soa.setRrdatas(ImmutableList.of( + // taken from the service + "ns-cloud-c1.googledomains.com. cloud-dns-hostmaster.google.com. 0 21600 3600 1209600 312" + )); + soa.setType("SOA"); + ResourceRecordSet ns = new ResourceRecordSet(); + ns.setTtl(21600); + ns.setName(zone.getDnsName()); + ns.setRrdatas(zone.getNameServers()); + ns.setType("NS"); + RrsetWrapper nsWrapper = new RrsetWrapper(ns); + RrsetWrapper soaWrapper = new RrsetWrapper(soa); + ImmutableList results = ImmutableList.of(nsWrapper, soaWrapper); + nsWrapper.setId(getUniqueId(results)); + soaWrapper.setId(getUniqueId(results)); + return results; + } + + /** + * Returns a list of four nameservers randomly chosen from the predefined set. + */ + static List randomNameservers() { + ArrayList nameservers = Lists.newArrayList( + "dns1.googlecloud.com", "dns2.googlecloud.com", "dns3.googlecloud.com", + "dns4.googlecloud.com", "dns5.googlecloud.com", "dns6.googlecloud.com" + ); + while (nameservers.size() != 4) { + int index = ID_GENERATOR.nextInt(nameservers.size()); + nameservers.remove(index); + } + return nameservers; + } + + /** + * Returns a hex string id (used for a dns record) unique withing the set of wrappers. + */ + static String getUniqueId(List wrappers) { + TreeSet ids = Sets.newTreeSet(Lists.transform(wrappers, + new Function() { + @Nullable + @Override + public String apply(@Nullable RrsetWrapper input) { + return input.id() == null ? "null" : input.id(); + } + })); + String id; + do { + id = Long.toHexString(System.currentTimeMillis()) + + Long.toHexString(Math.abs(ID_GENERATOR.nextLong())); + if (!ids.contains(id)) { + return id; + } + } while (ids.contains(id)); + return id; + } + + /** + * Tests if a DNS record matches name and type (if provided). Used for filtering. + */ + static boolean matchesCriteria(ResourceRecordSet record, String name, String type) { + if (type != null && !record.getType().equals(type)) { + return false; + } + if (name != null && !record.getName().equals(name)) { + return false; + } + return true; + } + + /** + * Returns a project container. Never returns null because we assume that all projects exists. + */ + ProjectContainer findProject(String projectId) { + ProjectContainer defaultProject = createProject(projectId); + projects.putIfAbsent(projectId, defaultProject); + return projects.get(projectId); + } + + /** + * Returns a zone container. Returns null if zone does not exist within project. + */ + ZoneContainer findZone(String projectId, String zoneName) { + ProjectContainer projectContainer = findProject(projectId); // never null + return projectContainer.zones().get(zoneName); + } + + /** + * Returns a change found by its id. Returns null if such a change does not exist. + */ + Change findChange(String projectId, String zoneName, String changeId) { + ZoneContainer wrapper = findZone(projectId, zoneName); + return wrapper == null ? null : wrapper.findChange(changeId); + } + + /** + * Returns a response to get change call. + */ + Response getChange(String projectId, String zoneName, String changeId, String[] fields) { + Change change = findChange(projectId, zoneName, changeId); + if (change == null) { + ZoneContainer zone = findZone(projectId, zoneName); + if (zone == null) { + // zone does not exist + return Error.NOT_FOUND.response(String.format( + "The 'parameters.managedZone' resource named '%s' does not exist.", zoneName)); + } + // zone exists but change does not + return Error.NOT_FOUND.response(String.format( + "The 'parameters.changeId' resource named '%s' does not exist.", changeId)); + } + Change result = OptionParsersAndExtractors.extractFields(change, fields); + try { + return new Response(HTTP_OK, jsonFactory.toString(result)); + } catch (IOException e) { + return Error.INTERNAL_ERROR.response(String.format( + "Error when serializing change %s in managed zone %s in project %s.", + changeId, zoneName, projectId)); + } + // todo(mderka) Test field options within #665. + } + + /** + * Returns a response to get zone call. + */ + Response getZone(String projectId, String zoneName, String[] fields) { + ZoneContainer container = findZone(projectId, zoneName); + if (container == null) { + return Error.NOT_FOUND.response(String.format( + "The 'parameters.managedZone' resource named '%s' does not exist.", zoneName)); + } + ManagedZone result = OptionParsersAndExtractors.extractFields(container.zone(), fields); + try { + return new Response(HTTP_OK, jsonFactory.toString(result)); + } catch (IOException e) { + return Error.INTERNAL_ERROR.response(String.format( + "Error when serializing managed zone %s in project %s.", zoneName, projectId)); + } + // todo(mderka) Test field options within #665. + } + + /** + * We assume that every project exists. If we do not have it in the collection yet, we just create + * a new default project instance with default quota. + */ + Response getProject(String projectId, String[] fields) { + ProjectContainer defaultProject = createProject(projectId); + projects.putIfAbsent(projectId, defaultProject); + Project project = projects.get(projectId).project(); // project is now guaranteed to exist + Project result = OptionParsersAndExtractors.extractFields(project, fields); + try { + return new Response(HTTP_OK, jsonFactory.toString(result)); + } catch (IOException e) { + return Error.INTERNAL_ERROR.response( + String.format("Error when serializing project %s.", projectId)); + } + // todo(mderka) Test field options within #665. + } + + /** + * Creates a project. It generates a project number randomly (we do not have project numbers + * available). + */ + private ProjectContainer createProject(String projectId) { + Quota quota = new Quota(); + quota.setManagedZones(10000); + quota.setRrsetsPerManagedZone(10000); + quota.setRrsetAdditionsPerChange(100); + quota.setRrsetDeletionsPerChange(100); + quota.setTotalRrdataSizePerChange(10000); + quota.setResourceRecordsPerRrset(100); + Project project = new Project(); + project.setId(projectId); + project.setNumber(new BigInteger(String.valueOf( + Math.abs(ID_GENERATOR.nextLong() % Long.MAX_VALUE)))); + project.setQuota(quota); + return new ProjectContainer(project); + } + + /** + * Deletes a zone. + */ + Response deleteZone(String projectId, String zoneName) { + ProjectContainer projectContainer = projects.get(projectId); + ZoneContainer previous = projectContainer.zones.remove(zoneName); + return previous == null + // map was not in the collection + ? Error.NOT_FOUND.response(String.format( + "The 'parameters.managedZone' resource named '%s' does not exist.", zoneName)) + : new Response(HTTP_NO_CONTENT, "{}"); + } + + /** + * Creates new managed zone and stores it in the collection. Assumes that project exists. + */ + Response createZone(String projectId, ManagedZone zone, String[] fields) { + checkNotNull(zone, "Zone to create cannot be null"); + // check if the provided data is valid + Response errorResponse = checkZone(zone); + if (errorResponse != null) { + return errorResponse; + } + // create a copy of the managed zone in order to avoid side effects + ManagedZone completeZone = new ManagedZone(); + completeZone.setName(zone.getName()); + completeZone.setDnsName(zone.getDnsName()); + completeZone.setNameServerSet(zone.getNameServerSet()); + completeZone.setCreationTime(ISODateTimeFormat.dateTime().withZoneUTC() + .print(System.currentTimeMillis())); + completeZone.setId( + new BigInteger(String.valueOf(Math.abs(ID_GENERATOR.nextLong() % Long.MAX_VALUE)))); + completeZone.setNameServers(randomNameservers()); + ZoneContainer zoneContainer = new ZoneContainer(completeZone); + // create the default NS and SOA records + zoneContainer.dnsRecords().put(zone.getName(), defaultRecords(completeZone)); + // place the zone in the data collection + ProjectContainer projectContainer = findProject(projectId); + ZoneContainer oldValue = projectContainer.zones().putIfAbsent( + completeZone.getName(), zoneContainer); + if (oldValue != null) { + return Error.ALREADY_EXISTS.response(String.format( + "The resource 'entity.managedZone' named '%s' already exists", completeZone.getName())); + } + // now return the desired attributes + ManagedZone result = OptionParsersAndExtractors.extractFields(completeZone, fields); + try { + return new Response(HTTP_OK, jsonFactory.toString(result)); + } catch (IOException e) { + return Error.INTERNAL_ERROR.response( + String.format("Error when serializing managed zone %s.", result.getName())); + } + // todo(mderka) Test field options within #665. + } + + /** + * Creates a new change, stores it, and invokes processing in a new thread. + */ + Response createChange(String projectId, String zoneName, Change change, String[] fields) { + ZoneContainer zoneContainer = findZone(projectId, zoneName); + if (zoneContainer == null) { + return Error.NOT_FOUND.response(String.format( + "The 'parameters.managedZone' resource named %s does not exist.", zoneName)); + } + // check that the change to be applied is valid + Response response = checkChange(change, zoneContainer); + if (response != null) { + return response; + } + // start applying + Change completeChange = new Change(); // copy to avoid side effects + if (change.getAdditions() != null) { + completeChange.setAdditions(ImmutableList.copyOf(change.getAdditions())); + } + if (change.getDeletions() != null) { + completeChange.setDeletions(ImmutableList.copyOf(change.getDeletions())); + } + /* we need to get the proper ID in concurrent environment + the element fell on an index between 0 and maxId + we will reset all IDs in this range (all of them are valid) */ + ConcurrentLinkedQueue changeSequence = zoneContainer.changes(); + changeSequence.add(completeChange); + int maxId = changeSequence.size(); + int index = 0; + for (Change c : changeSequence) { + if (index == maxId) { + break; + } + c.setId(String.valueOf(++index)); // indexing from 1 + } + completeChange.setStatus("pending"); // not finished yet + completeChange.setStartTime(ISODateTimeFormat.dateTime().withZoneUTC() + .print(System.currentTimeMillis())); // accepted + invokeChange(projectId, zoneName, completeChange.getId()); + Change result = OptionParsersAndExtractors.extractFields(completeChange, fields); + try { + return new Response(HTTP_OK, jsonFactory.toString(result)); + } catch (IOException e) { + return Error.INTERNAL_ERROR.response( + String.format("Error when serializing change %s in managed zone %s in project %s.", + result.getId(), zoneName, projectId)); + } + // todo(mderka) Test field options within #665. + } + + /** + * Applies change. Uses a new thread which applies the change only if DELAY_CHANGE is > 0. + */ + private Thread invokeChange(final String projectId, final String zoneName, + final String changeId) { + if (delayChange > 0) { + Thread thread = new Thread() { + @Override + public void run() { + try { + Thread.sleep(delayChange); // simulate delayed execution + } catch (InterruptedException ex) { + log.log(Level.WARNING, "Thread was interrupted while sleeping.", ex); + } + // start applying the changes + applyExistingChange(projectId, zoneName, changeId); + } + }; + thread.start(); + return thread; + } else { + applyExistingChange(projectId, zoneName, changeId); + return null; + } + } + + /** + * Applies changes to a zone. Repeatedly tries until succeeds. Thread safe and deadlock safe. + */ + private void applyExistingChange(String projectId, String zoneName, String changeId) { + Change change = findChange(projectId, zoneName, changeId); + if (change == null) { + return; // no such change exists, nothing to do + } + ZoneContainer wrapper = findZone(projectId, zoneName); + ConcurrentSkipListMap> dnsRecords = wrapper.dnsRecords(); + while (true) { + // managed zone must have a set of records which is not null + ImmutableList original = dnsRecords.get(zoneName); + assert original != null; + List copy = Lists.newLinkedList(original); + // apply deletions first + List deletions = change.getDeletions(); + if (deletions != null) { + List transformedDeletions = Lists.transform(deletions, + RrsetWrapper.WRAP_FUNCTION); + copy.removeAll(transformedDeletions); + } + // apply additions + List additions = change.getAdditions(); + if (additions != null) { + for (ResourceRecordSet addition : additions) { + String id = getUniqueId(copy); + RrsetWrapper rrsetWrapper = new RrsetWrapper(addition); + rrsetWrapper.setId(id); + copy.add(rrsetWrapper); + } + } + // make it immutable and replace + boolean success = dnsRecords.replace(zoneName, original, ImmutableList.copyOf(copy)); + if (success) { + break; // success if no other thread modified the value in the meantime + } + } + // set status to done + change.setStatus("done"); + } + + /** + * Lists zones. Next page token is the last listed zone name and is returned only of there is more + * to list. + */ + Response listZones(String projectId, Map options) { + Response response = checkListOptions(options); + if (response != null) { + return response; + } + ConcurrentSkipListMap containers = findProject(projectId).zones(); + String[] fields = (String[]) options.get("fields"); + String dnsName = (String) options.get("dnsName"); + String pageToken = (String) options.get("pageToken"); + Integer maxResults = options.get("maxResults") == null + ? null : Integer.valueOf((String) options.get("maxResults")); + // matches will be included in the result if true + boolean listing = (pageToken == null || !containers.containsKey(pageToken)); + boolean sizeReached = false; // maximum result size was reached, we should not return more + boolean hasMorePages = false; // should next page token be included in the response? + LinkedList serializedZones = new LinkedList<>(); + String lastZoneName = null; + for (ZoneContainer zoneContainer : containers.values()) { + ManagedZone zone = zoneContainer.zone(); + if (listing) { + if (dnsName == null || zone.getDnsName().equals(dnsName)) { + lastZoneName = zone.getName(); + if (sizeReached) { + // we do not add this, just note that there would be more and there should be a token + hasMorePages = true; + break; + } else { + try { + serializedZones.addLast(jsonFactory.toString( + OptionParsersAndExtractors.extractFields(zone, fields))); + } catch (IOException e) { + return Error.INTERNAL_ERROR.response(String.format( + "Error when serializing managed zone %s in project %s", zone.getName(), + projectId)); + } + } + } + } + // either we are listing already, or we check if we should start in the next iteration + listing = zone.getName().equals(pageToken) || listing; + sizeReached = (maxResults != null) && maxResults.equals(serializedZones.size()); + } + boolean includePageToken = + hasMorePages && (fields == null || ImmutableList.copyOf(fields).contains("pageToken")); + return toListResponse(serializedZones, lastZoneName, includePageToken); + // todo(mderka) Test field and listing options within #665. + } + + /** + * Lists DNS records for a zone. Next page token is ID of the last record listed. + */ + Response listDnsRecords(String projectId, String zoneName, Map options) { + Response response = checkListOptions(options); + if (response != null) { + return response; + } + ZoneContainer zoneContainer = findZone(projectId, zoneName); + if (zoneContainer == null) { + return Error.NOT_FOUND.response(String.format( + "The 'parameters.managedZone' resource named '%s' does not exist.", zoneName)); + } + List dnsRecords = zoneContainer.dnsRecords().get(zoneName); + String[] fields = (String[]) options.get("fields"); + String name = (String) options.get("name"); + String type = (String) options.get("type"); + String pageToken = (String) options.get("pageToken"); + Integer maxResults = options.get("maxResults") == null + ? null : Integer.valueOf((String) options.get("maxResults")); + boolean listing = (pageToken == null); // matches will be included in the result if true + boolean sizeReached = false; // maximum result size was reached, we should not return more + boolean hasMorePages = false; // should next page token be included in the response? + LinkedList serializedRrsets = new LinkedList<>(); + String lastRecordId = null; + for (RrsetWrapper recordWrapper : dnsRecords) { + ResourceRecordSet record = recordWrapper.rrset(); + if (listing) { + if (matchesCriteria(record, name, type)) { + lastRecordId = recordWrapper.id(); + if (sizeReached) { + // we do not add this, just note that there would be more and there should be a token + hasMorePages = true; + break; + } else { + try { + serializedRrsets.addLast(jsonFactory.toString( + OptionParsersAndExtractors.extractFields(record, fields))); + } catch (IOException e) { + return Error.INTERNAL_ERROR.response(String.format( + "Error when serializing resource record set in managed zone %s in project %s", + zoneName, projectId)); + } + } + } + } + // either we are listing already, or we check if we should start in the next iteration + listing = recordWrapper.id().equals(pageToken) || listing; + sizeReached = (maxResults != null) && maxResults.equals(serializedRrsets.size()); + } + boolean includePageToken = + hasMorePages && (fields == null || ImmutableList.copyOf(fields).contains("pageToken")); + return toListResponse(serializedRrsets, lastRecordId, includePageToken); + // todo(mderka) Test field and listing options within #665. + } + + /** + * Lists changes. Next page token is ID of the last change listed. + */ + Response listChanges(String projectId, String zoneName, Map options) { + Response response = checkListOptions(options); + if (response != null) { + return response; + } + ZoneContainer zoneContainer = findZone(projectId, zoneName); + // take a sorted snapshot of the current change list + TreeMap changes = new TreeMap<>(); + for (Change c : zoneContainer.changes()) { + if (c.getId() != null) { + changes.put(Integer.valueOf(c.getId()), c); + } + } + String[] fields = (String[]) options.get("fields"); + String sortOrder = (String) options.get("sortOrder"); + String pageToken = (String) options.get("pageToken"); + Integer maxResults = options.get("maxResults") == null + ? null : Integer.valueOf((String) options.get("maxResults")); + // we are not reading sort by as it the only key is the change sequence + NavigableSet keys; + if ("descending".equals(sortOrder)) { + keys = changes.descendingKeySet(); + } else { + keys = changes.navigableKeySet(); + } + boolean listing = (pageToken == null); // matches will be included in the result if true + boolean sizeReached = false; // maximum result size was reached, we should not return more + boolean hasMorePages = false; // should next page token be included in the response? + LinkedList serializedResults = new LinkedList<>(); + String lastChangeId = null; + for (Integer key : keys) { + Change change = changes.get(key); + if (listing) { + lastChangeId = change.getId(); + if (sizeReached) { + // we do not add this, just note that there would be more and there should be a token + hasMorePages = true; + break; + } else { + try { + serializedResults.addLast(jsonFactory.toString( + OptionParsersAndExtractors.extractFields(change, fields))); + } catch (IOException e) { + return Error.INTERNAL_ERROR.response(String.format( + "Error when serializing change %s in managed zone %s in project %s", + change.getId(), zoneName, projectId)); + } + } + } + + // either we are listing already, or we check if we should start in the next iteration + listing = change.getId().equals(pageToken) || listing; + sizeReached = (maxResults != null) && maxResults.equals(serializedResults.size()); + } + boolean includePageToken = + hasMorePages && (fields == null || ImmutableList.copyOf(fields).contains("pageToken")); + return toListResponse(serializedResults, lastChangeId, includePageToken); + // todo(mderka) Test field and listing options within #665. + } + + /** + * Validates a zone to be created. + */ + static Response checkZone(ManagedZone zone) { + if (zone.getName() == null) { + return Error.REQUIRED.response( + "The 'entity.managedZone.name' parameter is required but was missing."); + } + if (zone.getDnsName() == null) { + return Error.REQUIRED.response( + "The 'entity.managedZone.dnsName' parameter is required but was missing."); + } + if (zone.getDescription() == null) { + return Error.REQUIRED.response( + "The 'entity.managedZone.description' parameter is required but was missing."); + } + try { + int number = Integer.valueOf(zone.getName()); + return Error.INVALID.response( + String.format("Invalid value for 'entity.managedZone.name': '%s'", number)); + } catch (NumberFormatException ex) { + // expected + } + if (zone.getName().isEmpty()) { + return Error.INVALID.response( + String.format("Invalid value for 'entity.managedZone.name': '%s'", zone.getName())); + } + if (zone.getDnsName().isEmpty() || !zone.getDnsName().endsWith(".")) { + return Error.INVALID.response( + String.format("Invalid value for 'entity.managedZone.dnsName': '%s'", zone.getDnsName())); + } + TreeSet forbidden = Sets.newTreeSet( + ImmutableList.of("google.com.", "com.", "example.com.", "net.", "org.")); + if (forbidden.contains(zone.getDnsName())) { + return Error.NOT_AVAILABLE.response(String.format( + "The '%s' managed zone is not available to be created.", zone.getDnsName())); + } + return null; + } + + /** + * Validates a change to be created. + */ + static Response checkChange(Change change, ZoneContainer zone) { + checkNotNull(zone); + if ((change.getDeletions() != null && change.getDeletions().size() > 0) + || (change.getAdditions() != null && change.getAdditions().size() > 0)) { + // ok, this is what we want + } else { + return Error.REQUIRED.response("The 'entity.change' parameter is required but was missing."); + } + if (change.getAdditions() != null) { + int counter = 0; + for (ResourceRecordSet addition : change.getAdditions()) { + Response response = checkRrset(addition, zone, counter, "additions"); + if (response != null) { + return response; + } + counter++; + } + } + if (change.getDeletions() != null) { + int counter = 0; + for (ResourceRecordSet deletion : change.getDeletions()) { + Response response = checkRrset(deletion, zone, counter, "deletions"); + if (response != null) { + return response; + } + counter++; + } + } + return additionsMeetDeletions(change.getAdditions(), change.getDeletions(), zone); + // null if everything is ok + } + + /** + * Checks a rrset within a change. + * + * @param type [additions|deletions] + * @param index the index or addition or deletion in the list + * @param zone the zone that this change is applied to + */ + static Response checkRrset(ResourceRecordSet rrset, ZoneContainer zone, int index, String type) { + if (rrset.getName() == null || !rrset.getName().endsWith(zone.zone().getDnsName())) { + return Error.INVALID.response(String.format( + "Invalid value for 'entity.change.%s[%s].name': '%s'", type, index, rrset.getName())); + } + if (rrset.getType() == null || !TYPES.contains(rrset.getType())) { + return Error.INVALID.response(String.format( + "Invalid value for 'entity.change.%s[%s].type': '%s'", type, index, rrset.getType())); + } + if (rrset.getTtl() != null && rrset.getTtl() != 0 && rrset.getTtl() < 0) { + return Error.INVALID.response(String.format( + "Invalid value for 'entity.change.%s[%s].ttl': '%s'", type, index, rrset.getTtl())); + } + if (rrset.getRrdatas() == null || rrset.isEmpty()) { + return Error.INVALID.response(String.format( + "Invalid value for 'entity.change.%s[%s].rrdata': '%s'", type, index, "")); + } + int counter = 0; + for (String record : rrset.getRrdatas()) { + if (!checkRrData(record, rrset.getType())) { + return Error.INVALID.response(String.format( + "Invalid value for 'entity.change.%s[%s].rrdata[%s]': '%s'", + type, index, counter, record)); + } + counter++; + } + if ("deletions".equals(type)) { + // check that deletion has a match by name and type + boolean found = false; + for (RrsetWrapper rrsetWrapper : zone.dnsRecords().get(zone.zone().getName())) { + ResourceRecordSet wrappedRrset = rrsetWrapper.rrset(); + if (rrset.getName().equals(wrappedRrset.getName()) + && rrset.getType().equals(wrappedRrset.getType())) { + found = true; + break; + } + } + if (!found) { + return Error.NOT_FOUND.response(String.format( + "The 'entity.change.deletions[%s]' resource named '%s (%s)' does not exist.", + index, rrset.getName(), rrset.getType())); + } + // if found, we still need an exact match + if ("deletions".equals(type) + && !zone.dnsRecords().get(zone.zone().getName()).contains(new RrsetWrapper(rrset))) { + // such a record does not exist + return Error.CONDITION_NOT_MET.response(String.format( + "Precondition not met for 'entity.change.deletions[%s]", index)); + } + } + return null; + } + + /** + * Checks that for each record that already exists, we have a matching deletion. Furthermore, + * check that mandatory SOA and NS records stay. + */ + static Response additionsMeetDeletions(List additions, + List deletions, ZoneContainer zone) { + if (additions != null) { + int index = 0; + for (ResourceRecordSet rrset : additions) { + for (RrsetWrapper wrapper : zone.dnsRecords().get(zone.zone().getName())) { + ResourceRecordSet wrappedRrset = wrapper.rrset(); + if (rrset.getName().equals(wrappedRrset.getName()) + && rrset.getType().equals(wrappedRrset.getType())) { + // such a record exist and we must have a deletion + if (deletions == null || !deletions.contains(wrappedRrset)) { + return Error.ALREADY_EXISTS.response(String.format( + "The 'entity.change.additions[%s]' resource named '%s (%s)' already exists.", + index, rrset.getName(), rrset.getType())); + } + } + } + if (rrset.getType().equals("SOA") && findByNameAndType(deletions, null, "SOA") == null) { + return Error.INVALID_ZONE_APEX.response(String.format("The resource record set 'entity" + + ".change.additions[%s]' is invalid because a zone must contain exactly one resource" + + " record set of type 'SOA' at the apex.", index)); + } + if (rrset.getType().equals("NS") && findByNameAndType(deletions, null, "NS") == null) { + return Error.INVALID_ZONE_APEX.response(String.format("The resource record set 'entity" + + ".change.additions[%s]' is invalid because a zone must contain exactly one resource" + + " record set of type 'NS' at the apex.", index)); + } + index++; + } + } + if (deletions != null) { + int index = 0; + for (ResourceRecordSet rrset : deletions) { + if (rrset.getType().equals("SOA") && findByNameAndType(additions, null, "SOA") == null) { + return Error.INVALID_ZONE_APEX.response(String.format("The resource record set 'entity" + + ".change.deletions[%s]' is invalid because a zone must contain exactly one resource" + + " record set of type 'SOA' at the apex.", index)); + } + if (rrset.getType().equals("NS") && findByNameAndType(additions, null, "NS") == null) { + return Error.INVALID_ZONE_APEX.response(String.format("The resource record set 'entity" + + ".change.deletions[%s]' is invalid because a zone must contain exactly one resource" + + " record set of type 'NS' at the apex.", index)); + } + index++; + } + } + return null; + } + + /** + * Helper for searching rrsets in a collection. + */ + private static ResourceRecordSet findByNameAndType(Iterable records, + String name, String type) { + if (records != null) { + for (ResourceRecordSet rrset : records) { + if ((name == null || name.equals(rrset.getName())) + && (type == null || type.equals(rrset.getType()))) { + return rrset; + } + } + } + return null; + } + + /** + * We only provide the most basic validation for A and AAAA records. + */ + static boolean checkRrData(String data, String type) { + // todo add validation for other records + String[] tokens; + switch (type) { + case "A": + tokens = data.split("\\."); + if (tokens.length != 4) { + return false; + } + for (String token : tokens) { + try { + Integer number = Integer.valueOf(token); + if (number < 0 || number > 255) { + return false; + } + } catch (NumberFormatException ex) { + return false; + } + } + return true; + case "AAAA": + tokens = data.split(":", -1); + if (tokens.length != 8) { + return false; + } + for (String token : tokens) { + try { + if (!token.isEmpty()) { + // empty is ok + Long number = Long.parseLong(token, 16); + if (number < 0 || number > 0xFFFF) { + return false; + } + } + } catch (NumberFormatException ex) { + return false; + } + } + return true; + default: + return true; + } + } + + /** + * Check supplied listing options. + */ + static Response checkListOptions(Map options) { + // for general listing + String maxResultsString = (String) options.get("maxResults"); + if (maxResultsString != null) { + Integer maxResults = null; + try { + maxResults = Integer.valueOf(maxResultsString); + } catch (NumberFormatException ex) { + return Error.INVALID.response(String.format( + "Invalid integer value': '%s'.", maxResultsString)); + } + if (maxResults <= 0) { + return Error.INVALID.response(String.format( + "Invalid value for 'parameters.maxResults': '%s'", maxResults)); + } + } + String dnsName = (String) options.get("dnsName"); + if (dnsName != null) { + if (!dnsName.endsWith(".")) { + return Error.INVALID.response(String.format( + "Invalid value for 'parameters.dnsName': '%s'", dnsName)); + } + } + // for listing dns records, name must be fully qualified + String name = (String) options.get("name"); + if (name != null) { + if (!name.endsWith(".")) { + return Error.INVALID.response(String.format( + "Invalid value for 'parameters.name': '%s'", name)); + } + } + String type = (String) options.get("type"); // must be provided with name + if (type != null) { + if (name == null) { + return Error.INVALID.response("Invalid value for 'parameters.name': ''"); + } + if (!TYPES.contains(type)) { + return Error.INVALID.response(String.format( + "Invalid value for 'parameters.type': '%s'", type)); + } + } + // for listing changes + String order = (String) options.get("sortOrder"); + if (order != null && !"ascending".equals(order) && !"descending".equals(order)) { + return Error.INVALID.response(String.format( + "Invalid value for 'parameters.sortOrder': '%s'", order)); + } + String sortBy = (String) options.get("sortBy"); // case insensitive + if (sortBy != null && !"changesequence".equals(sortBy.toLowerCase())) { + return Error.INVALID.response(String.format( + "Invalid string value: '%s'. Allowed values: [changesequence]", sortBy)); + } + return null; + } +} diff --git a/gcloud-java-dns/src/main/java/com/google/gcloud/testing/OptionParsersAndExtractors.java b/gcloud-java-dns/src/main/java/com/google/gcloud/testing/OptionParsersAndExtractors.java new file mode 100644 index 000000000000..f94cb15779ca --- /dev/null +++ b/gcloud-java-dns/src/main/java/com/google/gcloud/testing/OptionParsersAndExtractors.java @@ -0,0 +1,279 @@ +/* + * Copyright 2016 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. + */ + +package com.google.gcloud.testing; + +import com.google.api.services.dns.model.Change; +import com.google.api.services.dns.model.ManagedZone; +import com.google.api.services.dns.model.Project; +import com.google.api.services.dns.model.ResourceRecordSet; + +import java.util.HashMap; +import java.util.Map; + +/** + * Utility helpers for LocalDnsHelper. + */ +class OptionParsersAndExtractors { + + /** + * Makes a map of list options. Expects query to be only query part of the url (i.e., what follows + * the '?'). + */ + static Map parseListZonesOptions(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": + // List fields are in the form "managedZones(field1, field2, ...)" + options.put( + "fields", + argEntry[1].substring("managedZones(".length(), argEntry[1].length() - 1) + .split(",")); + break; + case "dnsName": + options.put("dnsName", argEntry[1]); + break; + case "pageToken": + options.put("pageToken", argEntry[1]); + break; + case "maxResults": + // parsing to int is done while handling + options.put("maxResults", argEntry[1]); + break; + default: + break; + } + } + } + return options; + } + + /** + * Makes a map of list options. Expects query to be only query part of the url (i.e., what follows + * the '?'). This format is common for all of zone, change and project. + */ + static String[] parseGetOptions(String query) { + if (query != null) { + String[] args = query.split("&"); + for (String arg : args) { + String[] argEntry = arg.split("="); + if (argEntry[0].equals("fields")) { + // List fields are in the form "fields=field1, field2,..." + return argEntry[1].split(","); + } + } + } + return null; + } + + /** + * Extracts only request fields. + */ + static ManagedZone extractFields(ManagedZone fullZone, String[] fields) { + if (fields == null) { + return fullZone; + } + ManagedZone managedZone = new ManagedZone(); + for (String field : fields) { + switch (field) { + case "creationTime": + managedZone.setCreationTime(fullZone.getCreationTime()); + break; + case "description": + managedZone.setDescription(fullZone.getDescription()); + break; + case "dnsName": + managedZone.setDnsName(fullZone.getDnsName()); + break; + case "id": + managedZone.setId(fullZone.getId()); + break; + case "name": + managedZone.setName(fullZone.getName()); + break; + case "nameServerSet": + managedZone.setNameServerSet(fullZone.getNameServerSet()); + break; + case "nameServers": + managedZone.setNameServers(fullZone.getNameServers()); + break; + default: + break; + } + } + return managedZone; + } + + /** + * Extracts only request fields. + */ + static Change extractFields(Change fullChange, String[] fields) { + if (fields == null) { + return fullChange; + } + Change change = new Change(); + for (String field : fields) { + switch (field) { + case "additions": + // todo the fragmentation is ignored here as our api does not support it + change.setAdditions(fullChange.getAdditions()); + break; + case "deletions": + // todo the fragmentation is ignored here as our api does not support it + change.setDeletions(fullChange.getDeletions()); + break; + case "id": + change.setId(fullChange.getId()); + break; + case "startTime": + change.setStartTime(fullChange.getStartTime()); + break; + case "status": + change.setStatus(fullChange.getStatus()); + break; + default: + break; + } + } + return change; + } + + /** + * Extracts only request fields. + */ + static Project extractFields(Project fullProject, String[] fields) { + if (fields == null) { + return fullProject; + } + Project project = new Project(); + for (String field : fields) { + switch (field) { + case "id": + project.setId(fullProject.getId()); + break; + case "number": + project.setNumber(fullProject.getNumber()); + break; + case "quota": + project.setQuota(fullProject.getQuota()); + break; + default: + break; + } + } + return project; + } + + /** + * Extracts only request fields. + */ + static ResourceRecordSet extractFields(ResourceRecordSet fullRecord, String[] fields) { + if (fields == null) { + return fullRecord; + } + ResourceRecordSet record = new ResourceRecordSet(); + for (String field : fields) { + switch (field) { + case "name": + record.setName(fullRecord.getName()); + break; + case "rrdatas": + record.setRrdatas(fullRecord.getRrdatas()); + break; + case "type": + record.setType(fullRecord.getType()); + break; + case "ttl": + record.setTtl(fullRecord.getTtl()); + break; + default: + break; + } + } + return record; + } + + static Map parseListChangesOptions(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": + // todo we do not support fragmentation in deletions and additions + options.put( + "fields", + argEntry[1].substring("changes(".length(), argEntry[1].length() - 1).split(",")); + break; + case "name": + options.put("name", argEntry[1]); + break; + case "pageToken": + options.put("pageToken", argEntry[1]); + break; + case "sortBy": + options.put("sortBy", argEntry[1]); + break; + case "sortOrder": + options.put("sortOrder", argEntry[1]); + break; + case "maxResults": + // parsing to int is done while handling + options.put("maxResults", argEntry[1]); + break; + default: + break; + } + } + } + return options; + } + + static Map parseListDnsRecordsOptions(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].substring("rrsets(".length(), argEntry[1].length() - 1).split(",")); + break; + case "name": + options.put("name", argEntry[1]); + break; + case "pageToken": + options.put("pageToken", argEntry[1]); + break; + case "maxResults": + // parsing to int is done while handling + options.put("maxResults", argEntry[1]); + break; + default: + break; + } + } + } + return options; + } +} diff --git a/gcloud-java-dns/src/test/java/com/google/gcloud/testing/LocalDnsHelperTest.java b/gcloud-java-dns/src/test/java/com/google/gcloud/testing/LocalDnsHelperTest.java new file mode 100644 index 000000000000..1f851045659c --- /dev/null +++ b/gcloud-java-dns/src/test/java/com/google/gcloud/testing/LocalDnsHelperTest.java @@ -0,0 +1,955 @@ +/* + * Copyright 2016 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. + */ + +package com.google.gcloud.testing; + +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.api.services.dns.model.Change; +import com.google.api.services.dns.model.ManagedZone; +import com.google.api.services.dns.model.ResourceRecordSet; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.Lists; + +import org.junit.After; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Test; + +import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; + +public class LocalDnsHelperTest { + + private static final String RRSET_TYPE = "A"; + private static final ResourceRecordSet RRSET1 = new ResourceRecordSet(); + private static final ResourceRecordSet RRSET2 = new ResourceRecordSet(); + private static final ResourceRecordSet RRSET_KEEP = new ResourceRecordSet(); + private static final String PROJECT_ID1 = "2135436541254"; + private static final String PROJECT_ID2 = "882248761325"; + private static final String ZONE_NAME1 = "my little zone"; + private static final String ZONE_NAME2 = "another zone name"; + private static final ManagedZone ZONE1 = new ManagedZone(); + private static final ManagedZone ZONE2 = new ManagedZone(); + private static final String DNS_NAME = "www.example.com."; + private static final Change CHANGE1 = new Change(); + private static final Change CHANGE2 = new Change(); + private static final Change CHANGE_KEEP = new Change(); + private Map optionsMap; + private LocalDnsHelper localDns; + private ManagedZone minimalZone = new ManagedZone(); // to be adjusted as needed + + @BeforeClass + public static void before() { + RRSET1.setName(DNS_NAME); + RRSET1.setType(RRSET_TYPE); + RRSET1.setRrdatas(ImmutableList.of("1.1.1.1")); + ZONE1.setName(ZONE_NAME1); + ZONE1.setDescription(""); + ZONE1.setDnsName(DNS_NAME); + ZONE2.setName(ZONE_NAME2); + ZONE2.setDescription(""); + ZONE2.setDnsName(DNS_NAME); + RRSET2.setName(DNS_NAME); + RRSET2.setType(RRSET_TYPE); + RRSET_KEEP.setName(DNS_NAME); + RRSET_KEEP.setType("MX"); + RRSET_KEEP.setRrdatas(ImmutableList.of("255.255.255.254")); + RRSET2.setRrdatas(ImmutableList.of("123.132.153.156")); + CHANGE1.setAdditions(ImmutableList.of(RRSET1, RRSET2)); + CHANGE2.setDeletions(ImmutableList.of(RRSET2)); + CHANGE_KEEP.setAdditions(ImmutableList.of(RRSET_KEEP)); + } + + @Before + public void setUp() { + localDns = LocalDnsHelper.create(0L); // synchronous + optionsMap = new HashMap<>(); + minimalZone = copyZone(ZONE1); + } + + @After + public void after() { + localDns = null; + } + + @Test + public void testMatchesCriteria() { + assertTrue(LocalDnsHelper.matchesCriteria(RRSET1, RRSET1.getName(), RRSET1.getType())); + assertFalse(LocalDnsHelper.matchesCriteria(RRSET1, RRSET1.getName(), "anothertype")); + assertTrue(LocalDnsHelper.matchesCriteria(RRSET1, null, RRSET1.getType())); + assertTrue(LocalDnsHelper.matchesCriteria(RRSET1, RRSET1.getName(), null)); + assertFalse(LocalDnsHelper.matchesCriteria(RRSET1, "anothername", RRSET1.getType())); + } + + @Test + public void testGetUniqueId() { + assertNotNull(LocalDnsHelper.getUniqueId(Lists.newLinkedList())); + } + + @Test + public void testFindProject() { + assertEquals(0, localDns.projects().size()); + LocalDnsHelper.ProjectContainer project = localDns.findProject(PROJECT_ID1); + assertNotNull(project); + assertTrue(localDns.projects().containsKey(PROJECT_ID1)); + assertNotNull(localDns.findProject(PROJECT_ID2)); + assertTrue(localDns.projects().containsKey(PROJECT_ID2)); + assertTrue(localDns.projects().containsKey(PROJECT_ID1)); + assertNotNull(project.zones()); + assertEquals(0, project.zones().size()); + assertNotNull(project.project()); + assertNotNull(project.project().getQuota()); + } + + @Test + public void testCreateAndFindZone() { + LocalDnsHelper.ZoneContainer zone1 = localDns.findZone(PROJECT_ID1, ZONE_NAME1); + assertTrue(localDns.projects().containsKey(PROJECT_ID1)); + assertNull(zone1); + localDns.createZone(PROJECT_ID1, ZONE1, null); // we do not care about options + zone1 = localDns.findZone(PROJECT_ID1, ZONE1.getName()); + assertNotNull(zone1); + // cannot call equals because id and timestamp got assigned + assertEquals(ZONE_NAME1, zone1.zone().getName()); + assertNotNull(zone1.changes()); + assertTrue(zone1.changes().isEmpty()); + assertNotNull(zone1.dnsRecords()); + assertEquals(2, zone1.dnsRecords().get(ZONE_NAME1).size()); // default SOA and NS + localDns.createZone(PROJECT_ID2, ZONE1, null); // project does not exits yet + assertEquals(ZONE1.getName(), localDns.findZone(PROJECT_ID2, ZONE_NAME1).zone().getName()); + } + + @Test + public void testDeleteZone() { + localDns.createZone(PROJECT_ID1, ZONE1, null); + LocalDnsHelper.Response response = localDns.deleteZone(PROJECT_ID1, ZONE1.getName()); + assertEquals(204, response.code()); + // deleting non-existent zone + response = localDns.deleteZone(PROJECT_ID1, ZONE1.getName()); + assertEquals(404, response.code()); + assertNull(localDns.findZone(PROJECT_ID1, ZONE1.getName())); + localDns.createZone(PROJECT_ID1, ZONE1, null); + localDns.createZone(PROJECT_ID1, ZONE2, null); + assertNotNull(localDns.findZone(PROJECT_ID1, ZONE1.getName())); + assertNotNull(localDns.findZone(PROJECT_ID1, ZONE2.getName())); + // delete in reverse order + response = localDns.deleteZone(PROJECT_ID1, ZONE1.getName()); + assertEquals(204, response.code()); + assertNull(localDns.findZone(PROJECT_ID1, ZONE1.getName())); + assertNotNull(localDns.findZone(PROJECT_ID1, ZONE2.getName())); + localDns.deleteZone(PROJECT_ID1, ZONE2.getName()); + assertEquals(204, response.code()); + assertNull(localDns.findZone(PROJECT_ID1, ZONE1.getName())); + assertNull(localDns.findZone(PROJECT_ID1, ZONE2.getName())); + } + + @Test + public void testCreateAndApplyChange() { + localDns = LocalDnsHelper.create(5 * 1000L); // we will be using threads here + localDns.createZone(PROJECT_ID1, ZONE1, null); + assertNull(localDns.findZone(PROJECT_ID1, ZONE_NAME1).findChange("1")); + LocalDnsHelper.Response response + = localDns.createChange(PROJECT_ID1, ZONE_NAME1, CHANGE1, null); // add + assertEquals(200, response.code()); + assertNotNull(localDns.findZone(PROJECT_ID1, ZONE_NAME1).findChange("1")); + assertNull(localDns.findZone(PROJECT_ID1, ZONE_NAME1).findChange("2")); + localDns.createChange(PROJECT_ID1, ZONE_NAME1, CHANGE1, null); // add + response = localDns.createChange(PROJECT_ID1, ZONE_NAME1, CHANGE1, null); // add + assertEquals(200, response.code()); + assertNotNull(localDns.findZone(PROJECT_ID1, ZONE_NAME1).findChange("1")); + assertNotNull(localDns.findZone(PROJECT_ID1, ZONE_NAME1).findChange("2")); + localDns.createChange(PROJECT_ID1, ZONE_NAME1, CHANGE2, null); // delete + assertNotNull(localDns.findZone(PROJECT_ID1, ZONE_NAME1).findChange("1")); + assertNotNull(localDns.findZone(PROJECT_ID1, ZONE_NAME1).findChange("2")); + assertNotNull(localDns.findZone(PROJECT_ID1, ZONE_NAME1).findChange("3")); + localDns.createChange(PROJECT_ID1, ZONE_NAME1, CHANGE_KEEP, null); // id is "4" + // check execution + Change change = localDns.findChange(PROJECT_ID1, ZONE_NAME1, "4"); + for (int i = 0; i < 10 && !change.getStatus().equals("done"); i++) { + // change has not been finished yet; wait at most 20 seconds + // it takes 5 seconds for the thread to kick in in the first place + try { + Thread.sleep(2 * 1000); + } catch (InterruptedException e) { + fail("Test was interrupted"); + } + } + assertEquals("done", change.getStatus()); + List list = + localDns.findZone(PROJECT_ID1, ZONE_NAME1).dnsRecords().get(ZONE_NAME1); + assertTrue(list.contains(new LocalDnsHelper.RrsetWrapper(RRSET_KEEP))); + } + + @Test + public void testFindChange() { + localDns.createZone(PROJECT_ID1, ZONE1, null); + Change change = localDns.findChange(PROJECT_ID1, ZONE1.getName(), "somerandomchange"); + assertNull(change); + localDns.createChange(PROJECT_ID1, ZONE1.getName(), CHANGE1, null); + // changes are sequential so we should find ID 1 + assertNotNull(localDns.findChange(PROJECT_ID1, ZONE1.getName(), "1")); + // add another + localDns.createChange(PROJECT_ID1, ZONE_NAME1, CHANGE2, null); + assertNotNull(localDns.findChange(PROJECT_ID1, ZONE1.getName(), "1")); + assertNotNull(localDns.findChange(PROJECT_ID1, ZONE1.getName(), "2")); + // try to find non-existent change + assertNull(localDns.findChange(PROJECT_ID1, ZONE1.getName(), "3")); + // try to find a change in yet non-existent project + assertNull(localDns.findChange(PROJECT_ID2, ZONE1.getName(), "3")); + } + + @Test + public void testRandomNameServers() { + assertEquals(4, LocalDnsHelper.randomNameservers().size()); + assertEquals(4, LocalDnsHelper.randomNameservers().size()); + assertEquals(4, LocalDnsHelper.randomNameservers().size()); + assertEquals(4, LocalDnsHelper.randomNameservers().size()); + } + + @Test + public void testGetProject() { + // only interested in no exceptions and non-null response here + assertNotNull(localDns.getProject(PROJECT_ID1, null)); + assertNotNull(localDns.getProject(PROJECT_ID2, null)); + } + + @Test + public void testGetZone() { + // non-existent + LocalDnsHelper.Response response = localDns.getZone(PROJECT_ID1, ZONE_NAME1, null); + assertEquals(404, response.code()); + assertTrue(response.body().contains("does not exist")); + // existent + localDns.createZone(PROJECT_ID1, ZONE1, null); + response = localDns.getZone(PROJECT_ID1, ZONE1.getName(), null); + assertEquals(200, response.code()); + } + + @Test + public void testCreateZone() { + // only interested in no exceptions and non-null response here + LocalDnsHelper.Response response = localDns.createZone(PROJECT_ID1, ZONE1, null); + assertEquals(200, response.code()); + assertEquals(1, localDns.projects().get(PROJECT_ID1).zones().size()); + try { + localDns.createZone(PROJECT_ID1, null, null); + fail("Zone cannot be null"); + } catch (NullPointerException ex) { + // expected + } + // create zone twice + response = localDns.createZone(PROJECT_ID1, ZONE1, null); + assertEquals(409, response.code()); + assertTrue(response.body().contains("already exists")); + } + + @Test + public void testCreateChange() { + // non-existent zone + LocalDnsHelper.Response response = + localDns.createChange(PROJECT_ID1, ZONE_NAME1, CHANGE1, null); + assertEquals(404, response.code()); + + // existent zone + assertNotNull(localDns.createZone(PROJECT_ID1, ZONE1, null)); + assertNull(localDns.findChange(PROJECT_ID1, ZONE_NAME1, "1")); + response = localDns.createChange(PROJECT_ID1, ZONE_NAME1, CHANGE1, null); + assertEquals(200, response.code()); + assertNotNull(localDns.findChange(PROJECT_ID1, ZONE_NAME1, "1")); + } + + @Test + public void testGetChange() { + // existent + assertEquals(200, localDns.createZone(PROJECT_ID1, ZONE1, null).code()); + assertEquals(200, localDns.createChange(PROJECT_ID1, ZONE_NAME1, CHANGE1, null).code()); + assertEquals(200, localDns.getChange(PROJECT_ID1, ZONE_NAME1, "1", null).code()); + // non-existent + LocalDnsHelper.Response response = localDns.getChange(PROJECT_ID1, ZONE_NAME1, "2", null); + assertEquals(404, response.code()); + assertTrue(response.body().contains("parameters.changeId")); + // non-existent zone + response = localDns.getChange(PROJECT_ID1, ZONE_NAME2, "1", null); + assertEquals(404, response.code()); + assertTrue(response.body().contains("parameters.managedZone")); + } + + @Test + public void testListZones() { + // only interested in no exceptions and non-null response here + optionsMap.put("dnsName", null); + optionsMap.put("fields", null); + optionsMap.put("pageToken", null); + optionsMap.put("maxResults", null); + LocalDnsHelper.Response response = localDns.listZones(PROJECT_ID1, optionsMap); + assertEquals(200, response.code()); + // some zones exists + localDns.createZone(PROJECT_ID1, ZONE1, null); + response = localDns.listZones(PROJECT_ID1, optionsMap); + assertEquals(200, response.code()); + localDns.createZone(PROJECT_ID1, ZONE2, null); + response = localDns.listZones(PROJECT_ID1, optionsMap); + assertEquals(200, response.code()); + // error in options + optionsMap.put("maxResults", "aaa"); + response = localDns.listZones(PROJECT_ID1, optionsMap); + assertEquals(400, response.code()); + optionsMap.put("maxResults", "0"); + response = localDns.listZones(PROJECT_ID1, optionsMap); + assertEquals(400, response.code()); + optionsMap.put("maxResults", "-1"); + response = localDns.listZones(PROJECT_ID1, optionsMap); + assertEquals(400, response.code()); + optionsMap.put("maxResults", "15"); + response = localDns.listZones(PROJECT_ID1, optionsMap); + assertEquals(200, response.code()); + optionsMap.put("dnsName", "aaa"); + response = localDns.listZones(PROJECT_ID1, optionsMap); + assertEquals(400, response.code()); + optionsMap.put("dnsName", "aaa."); + response = localDns.listZones(PROJECT_ID1, optionsMap); + assertEquals(200, response.code()); + } + + @Test + public void testListDnsRecords() { + // only interested in no exceptions and non-null response here + optionsMap.put("name", null); + optionsMap.put("fields", null); + optionsMap.put("type", null); + optionsMap.put("pageToken", null); + optionsMap.put("maxResults", null); + // no zone exists + LocalDnsHelper.Response response = localDns.listDnsRecords(PROJECT_ID1, ZONE_NAME1, + optionsMap); + assertEquals(404, response.code()); + // zone exists but has no records + localDns.createZone(PROJECT_ID1, ZONE1, null); + localDns.listDnsRecords(PROJECT_ID1, ZONE_NAME1, optionsMap); + // zone has records + localDns.createChange(PROJECT_ID1, ZONE_NAME1, CHANGE1, null); + response = localDns.listDnsRecords(PROJECT_ID1, ZONE_NAME1, optionsMap); + assertEquals(200, response.code()); + // error in options + optionsMap.put("maxResults", "aaa"); + response = localDns.listZones(PROJECT_ID1, optionsMap); + assertEquals(400, response.code()); + optionsMap.put("maxResults", "0"); + response = localDns.listZones(PROJECT_ID1, optionsMap); + assertEquals(400, response.code()); + optionsMap.put("maxResults", "-1"); + response = localDns.listZones(PROJECT_ID1, optionsMap); + assertEquals(400, response.code()); + optionsMap.put("maxResults", "15"); + response = localDns.listZones(PROJECT_ID1, optionsMap); + assertEquals(200, response.code()); + optionsMap.put("name", "aaa"); + response = localDns.listZones(PROJECT_ID1, optionsMap); + assertEquals(400, response.code()); + optionsMap.put("name", "aaa."); + response = localDns.listZones(PROJECT_ID1, optionsMap); + assertEquals(200, response.code()); + optionsMap.put("name", null); + optionsMap.put("type", "A"); + response = localDns.listZones(PROJECT_ID1, optionsMap); + assertEquals(400, response.code()); + optionsMap.put("name", "aaa."); + optionsMap.put("type", "a"); + response = localDns.listZones(PROJECT_ID1, optionsMap); + assertEquals(400, response.code()); + optionsMap.put("name", "aaaa."); + optionsMap.put("type", "A"); + response = localDns.listZones(PROJECT_ID1, optionsMap); + assertEquals(200, response.code()); + } + + @Test + public void testListChanges() { + optionsMap.put("sortBy", null); + optionsMap.put("sortOrder", null); + optionsMap.put("fields", null); + optionsMap.put("pageToken", null); + optionsMap.put("maxResults", null); + // no such zone exists + LocalDnsHelper.Response response = localDns.listDnsRecords(PROJECT_ID1, ZONE_NAME1, optionsMap); + assertEquals(404, response.code()); + assertTrue(response.body().contains("managedZone")); + // zone exists but has no changes + localDns.createZone(PROJECT_ID1, ZONE1, null); + assertNotNull(localDns.listChanges(PROJECT_ID1, ZONE_NAME1, optionsMap)); + // zone has changes + localDns.createChange(PROJECT_ID1, ZONE_NAME1, CHANGE1, null); + assertNotNull(localDns.listChanges(PROJECT_ID1, ZONE_NAME1, optionsMap)); + localDns.createChange(PROJECT_ID1, ZONE_NAME1, CHANGE1, null); + localDns.createChange(PROJECT_ID1, ZONE_NAME1, CHANGE2, null); + localDns.createChange(PROJECT_ID1, ZONE_NAME1, CHANGE2, null); + assertNotNull(localDns.listChanges(PROJECT_ID1, ZONE_NAME1, optionsMap)); + // error in options + optionsMap.put("maxResults", "aaa"); + response = localDns.listChanges(PROJECT_ID1, ZONE_NAME1, optionsMap); + assertEquals(400, response.code()); + optionsMap.put("maxResults", "0"); + response = localDns.listChanges(PROJECT_ID1, ZONE_NAME1, optionsMap); + assertEquals(400, response.code()); + optionsMap.put("maxResults", "-1"); + response = localDns.listChanges(PROJECT_ID1, ZONE_NAME1, optionsMap); + assertEquals(400, response.code()); + optionsMap.put("maxResults", "15"); + response = localDns.listChanges(PROJECT_ID1, ZONE_NAME1, optionsMap); + assertEquals(200, response.code()); + optionsMap.put("dnsName", "aaa"); + response = localDns.listChanges(PROJECT_ID1, ZONE_NAME1, optionsMap); + assertEquals(400, response.code()); + optionsMap.put("dnsName", "aaa."); + response = localDns.listChanges(PROJECT_ID1, ZONE_NAME1, optionsMap); + assertEquals(200, response.code()); + optionsMap.put("sortBy", "changeSequence"); + response = localDns.listChanges(PROJECT_ID1, ZONE_NAME1, optionsMap); + assertEquals(200, response.code()); + optionsMap.put("sortBy", "something else"); + response = localDns.listChanges(PROJECT_ID1, ZONE_NAME1, optionsMap); + assertEquals(400, response.code()); + assertTrue(response.body().contains("Allowed values: [changesequence]")); + optionsMap.put("sortBy", "ChAnGeSeQuEnCe"); // is not case sensitive + response = localDns.listChanges(PROJECT_ID1, ZONE_NAME1, optionsMap); + assertEquals(200, response.code()); + optionsMap.put("sortOrder", "ascending"); + response = localDns.listChanges(PROJECT_ID1, ZONE_NAME1, optionsMap); + assertEquals(200, response.code()); + optionsMap.put("sortBy", null); + optionsMap.put("sortOrder", "descending"); + response = localDns.listChanges(PROJECT_ID1, ZONE_NAME1, optionsMap); + assertEquals(200, response.code()); + optionsMap.put("sortOrder", "somethingelse"); + response = localDns.listChanges(PROJECT_ID1, ZONE_NAME1, optionsMap); + assertEquals(400, response.code()); + assertTrue(response.body().contains("parameters.sortOrder")); + } + + @Test + public void testToListResponse() { + LocalDnsHelper.Response response = LocalDnsHelper.toListResponse( + Lists.newArrayList("some", "multiple", "words"), "IncludeThisPageToken", true); + assertTrue(response.body().contains("IncludeThisPageToken")); + response = LocalDnsHelper.toListResponse( + Lists.newArrayList("some", "multiple", "words"), "IncludeThisPageToken", false); + assertFalse(response.body().contains("IncludeThisPageToken")); + response = LocalDnsHelper.toListResponse( + Lists.newArrayList("some", "multiple", "words"), null, true); + assertFalse(response.body().contains("pageToken")); + } + + @Test + public void testCheckZone() { + // no name + ManagedZone copy = copyZone(minimalZone); + copy.setName(null); + LocalDnsHelper.Response response = LocalDnsHelper.checkZone(copy); + assertEquals(400, response.code()); + assertTrue(response.body().contains("entity.managedZone.name")); + // no description + copy = copyZone(minimalZone); + copy.setDescription(null); + response = LocalDnsHelper.checkZone(copy); + assertEquals(400, response.code()); + assertTrue(response.body().contains("entity.managedZone.description")); + // no description + copy = copyZone(minimalZone); + copy.setDnsName(null); + response = LocalDnsHelper.checkZone(copy); + assertEquals(400, response.code()); + assertTrue(response.body().contains("entity.managedZone.dnsName")); + // zone name is a number + copy = copyZone(minimalZone); + copy.setName("123456"); + response = LocalDnsHelper.checkZone(copy); + assertEquals(400, response.code()); + assertTrue(response.body().contains("entity.managedZone.name")); + assertTrue(response.body().contains("Invalid")); + // dns name does not end with period + copy = copyZone(minimalZone); + copy.setDnsName("aaaaaa.com"); + response = LocalDnsHelper.checkZone(copy); + assertEquals(400, response.code()); + assertTrue(response.body().contains("entity.managedZone.dnsName")); + assertTrue(response.body().contains("Invalid")); + // dns name is reserved + copy = copyZone(minimalZone); + copy.setDnsName("com."); + response = LocalDnsHelper.checkZone(copy); + assertEquals(400, response.code()); + assertTrue(response.body().contains("not available to be created.")); + // empty description should pass + copy = copyZone(minimalZone); + copy.setDescription(""); + assertNull(LocalDnsHelper.checkZone(copy)); + } + + @Test + public void testCreateZoneValidatesZone() { + // no name + ManagedZone copy = copyZone(minimalZone); + copy.setName(null); + LocalDnsHelper.Response response = localDns.createZone(PROJECT_ID1, copy, null); + assertEquals(400, response.code()); + assertTrue(response.body().contains("entity.managedZone.name")); + // no description + copy = copyZone(minimalZone); + copy.setDescription(null); + response = localDns.createZone(PROJECT_ID1, copy, null); + assertEquals(400, response.code()); + assertTrue(response.body().contains("entity.managedZone.description")); + // no dns name + copy = copyZone(minimalZone); + copy.setDnsName(null); + response = localDns.createZone(PROJECT_ID1, copy, null); + assertEquals(400, response.code()); + assertTrue(response.body().contains("entity.managedZone.dnsName")); + // zone name is a number + copy = copyZone(minimalZone); + copy.setName("123456"); + response = localDns.createZone(PROJECT_ID1, copy, null); + assertEquals(400, response.code()); + assertTrue(response.body().contains("entity.managedZone.name")); + assertTrue(response.body().contains("Invalid")); + // dns name does not end with period + copy = copyZone(minimalZone); + copy.setDnsName("aaaaaa.com"); + response = localDns.createZone(PROJECT_ID1, copy, null); + assertEquals(400, response.code()); + assertTrue(response.body().contains("entity.managedZone.dnsName")); + assertTrue(response.body().contains("Invalid")); + // dns name is reserved + copy = copyZone(minimalZone); + copy.setDnsName("com."); + response = localDns.createZone(PROJECT_ID1, copy, null); + assertEquals(400, response.code()); + assertTrue(response.body().contains("not available to be created.")); + // empty description should pass + copy = copyZone(minimalZone); + copy.setDescription(""); + response = localDns.createZone(PROJECT_ID1, copy, null); + assertEquals(200, response.code()); + } + + @Test + public void testCheckListOptions() { + // listing zones + optionsMap.put("maxResults", "-1"); + LocalDnsHelper.Response response = LocalDnsHelper.checkListOptions(optionsMap); + assertEquals(400, response.code()); + assertTrue(response.body().contains("parameters.maxResults")); + optionsMap.put("maxResults", "0"); + response = LocalDnsHelper.checkListOptions(optionsMap); + assertEquals(400, response.code()); + assertTrue(response.body().contains("parameters.maxResults")); + optionsMap.put("maxResults", "aaaa"); + response = LocalDnsHelper.checkListOptions(optionsMap); + assertEquals(400, response.code()); + assertTrue(response.body().contains("integer")); + optionsMap.put("maxResults", "15"); + response = LocalDnsHelper.checkListOptions(optionsMap); + assertNull(response); + optionsMap.put("dnsName", "aaa"); + response = LocalDnsHelper.checkListOptions(optionsMap); + assertEquals(400, response.code()); + assertTrue(response.body().contains("parameters.dnsName")); + optionsMap.put("dnsName", "aaa."); + response = LocalDnsHelper.checkListOptions(optionsMap); + assertNull(response); + // listing dns records + optionsMap.put("name", "aaa"); + response = LocalDnsHelper.checkListOptions(optionsMap); + assertEquals(400, response.code()); + assertTrue(response.body().contains("parameters.name")); + optionsMap.put("name", "aaa."); + response = LocalDnsHelper.checkListOptions(optionsMap); + assertNull(response); + optionsMap.put("name", null); + optionsMap.put("type", "A"); + response = LocalDnsHelper.checkListOptions(optionsMap); + assertEquals(400, response.code()); + assertTrue(response.body().contains("parameters.name")); + optionsMap.put("name", "aaa."); + optionsMap.put("type", "a"); + response = LocalDnsHelper.checkListOptions(optionsMap); + assertEquals(400, response.code()); + assertTrue(response.body().contains("parameters.type")); + optionsMap.put("name", "aaaa."); + optionsMap.put("type", "A"); + response = LocalDnsHelper.checkListOptions(optionsMap); + assertNull(response); + // listing changes + optionsMap.put("sortBy", "changeSequence"); + response = LocalDnsHelper.checkListOptions(optionsMap); + assertNull(response); + optionsMap.put("sortBy", "something else"); + response = LocalDnsHelper.checkListOptions(optionsMap); + assertEquals(400, response.code()); + assertTrue(response.body().contains("Allowed values: [changesequence]")); + optionsMap.put("sortBy", "ChAnGeSeQuEnCe"); // is not case sensitive + response = LocalDnsHelper.checkListOptions(optionsMap); + assertNull(response); + optionsMap.put("sortOrder", "ascending"); + response = LocalDnsHelper.checkListOptions(optionsMap); + assertNull(response); + optionsMap.put("sortOrder", "descending"); + response = LocalDnsHelper.checkListOptions(optionsMap); + assertNull(response); + optionsMap.put("sortOrder", "somethingelse"); + response = LocalDnsHelper.checkListOptions(optionsMap); + assertEquals(400, response.code()); + assertTrue(response.body().contains("parameters.sortOrder")); + } + + @Test + public void testCheckRrset() { + ResourceRecordSet valid = new ResourceRecordSet(); + valid.setName(ZONE1.getDnsName()); + valid.setType("A"); + valid.setRrdatas(ImmutableList.of("0.255.1.5")); + valid.setTtl(500); + Change validChange = new Change(); + validChange.setAdditions(ImmutableList.of(valid)); + localDns.createZone(PROJECT_ID1, ZONE1, null); + localDns.createChange(PROJECT_ID1, ZONE_NAME1, validChange, null); + // delete with field mismatch + LocalDnsHelper.ZoneContainer zone = localDns.findZone(PROJECT_ID1, ZONE_NAME1); + valid.setTtl(valid.getTtl() + 20); + LocalDnsHelper.Response response = LocalDnsHelper.checkRrset(valid, zone, 0, "deletions"); + assertEquals(412, response.code()); + assertTrue(response.body().contains("entity.change.deletions[0]")); + } + + @Test + public void testCheckRrdata() { + // A + assertTrue(LocalDnsHelper.checkRrData("255.255.255.255", "A")); + assertTrue(LocalDnsHelper.checkRrData("13.15.145.165", "A")); + assertTrue(LocalDnsHelper.checkRrData("0.0.0.0", "A")); + assertFalse(LocalDnsHelper.checkRrData("255.255.255.256", "A")); + assertFalse(LocalDnsHelper.checkRrData("-1.255.255.255", "A")); + assertFalse(LocalDnsHelper.checkRrData(".255.255.254", "A")); + assertFalse(LocalDnsHelper.checkRrData("111.255.255.", "A")); + assertFalse(LocalDnsHelper.checkRrData("111.255..22", "A")); + assertFalse(LocalDnsHelper.checkRrData("111.255.aa.22", "A")); + assertFalse(LocalDnsHelper.checkRrData("", "A")); + assertFalse(LocalDnsHelper.checkRrData("...", "A")); + assertFalse(LocalDnsHelper.checkRrData("111.255.12", "A")); + assertFalse(LocalDnsHelper.checkRrData("111.255.12.11.11", "A")); + // AAAA + assertTrue(LocalDnsHelper.checkRrData(":::::::", "AAAA")); + assertTrue(LocalDnsHelper.checkRrData("1F:fa:09fd::343:aaaa:AAAA:", "AAAA")); + assertTrue(LocalDnsHelper.checkRrData("0000:FFFF:09fd::343:aaaa:AAAA:", "AAAA")); + assertFalse(LocalDnsHelper.checkRrData("-2:::::::", "AAAA")); + assertTrue(LocalDnsHelper.checkRrData("0:::::::", "AAAA")); + assertFalse(LocalDnsHelper.checkRrData("::1FFFF:::::", "AAAA")); + assertFalse(LocalDnsHelper.checkRrData("::aqaa:::::", "AAAA")); + assertFalse(LocalDnsHelper.checkRrData("::::::::", "AAAA")); // too long + assertFalse(LocalDnsHelper.checkRrData("::::::", "AAAA")); // too short + } + + @Test + public void testCheckChange() { + ResourceRecordSet validA = new ResourceRecordSet(); + validA.setName(ZONE1.getDnsName()); + validA.setType("A"); + validA.setRrdatas(ImmutableList.of("0.255.1.5")); + ResourceRecordSet invalidA = new ResourceRecordSet(); + invalidA.setName(ZONE1.getDnsName()); + invalidA.setType("A"); + invalidA.setRrdatas(ImmutableList.of("0.-255.1.5")); + Change validChange = new Change(); + validChange.setAdditions(ImmutableList.of(validA)); + Change invalidChange = new Change(); + invalidChange.setAdditions(ImmutableList.of(invalidA)); + LocalDnsHelper.ZoneContainer zoneContainer = new LocalDnsHelper.ZoneContainer(ZONE1); + LocalDnsHelper.Response response = LocalDnsHelper.checkChange(validChange, zoneContainer); + assertNull(response); + response = LocalDnsHelper.checkChange(invalidChange, zoneContainer); + assertEquals(400, response.code()); + assertTrue(response.body().contains("additions[0].rrdata[0]")); + // only empty additions/deletions + Change empty = new Change(); + empty.setAdditions(ImmutableList.of()); + empty.setDeletions(ImmutableList.of()); + response = LocalDnsHelper.checkChange(empty, zoneContainer); + assertEquals(400, response.code()); + assertTrue(response.body().contains( + "The 'entity.change' parameter is required but was missing.")); + // null additions/deletions + empty = new Change(); + response = LocalDnsHelper.checkChange(empty, zoneContainer); + assertEquals(400, response.code()); + assertTrue(response.body().contains( + "The 'entity.change' parameter is required but was missing.")); + // non-matching name + validA.setName(ZONE1.getDnsName() + ".aaa."); + response = LocalDnsHelper.checkChange(validChange, zoneContainer); + assertEquals(400, response.code()); + assertTrue(response.body().contains("additions[0].name")); + // wrong type + validA.setName(ZONE1.getDnsName()); // revert + validA.setType("ABCD"); + response = LocalDnsHelper.checkChange(validChange, zoneContainer); + assertEquals(400, response.code()); + assertTrue(response.body().contains("additions[0].type")); + // wrong ttl + validA.setType("A"); // revert + validA.setTtl(-1); + response = LocalDnsHelper.checkChange(validChange, zoneContainer); + assertEquals(400, response.code()); + assertTrue(response.body().contains("additions[0].ttl")); + validA.setTtl(null); + // null name + validA.setName(null); + response = LocalDnsHelper.checkChange(validChange, zoneContainer); + assertEquals(400, response.code()); + assertTrue(response.body().contains("additions[0].name")); + validA.setName(ZONE1.getDnsName()); + // null type + validA.setType(null); + response = LocalDnsHelper.checkChange(validChange, zoneContainer); + assertEquals(400, response.code()); + assertTrue(response.body().contains("additions[0].type")); + validA.setType("A"); + // null rrdata + List temp = validA.getRrdatas(); // preserve + validA.setRrdatas(null); + response = LocalDnsHelper.checkChange(validChange, zoneContainer); + assertEquals(400, response.code()); + assertTrue(response.body().contains("additions[0].rrdata")); + validA.setRrdatas(temp); + // delete non-existent + ResourceRecordSet nonExistent = new ResourceRecordSet(); + nonExistent.setName(ZONE1.getDnsName()); + nonExistent.setType("AAAA"); + nonExistent.setRrdatas(ImmutableList.of(":::::::")); + Change delete = new Change(); + delete.setDeletions(ImmutableList.of(nonExistent)); + response = LocalDnsHelper.checkChange(delete, zoneContainer); + assertEquals(404, response.code()); + assertTrue(response.body().contains("deletions[0]")); + + } + + @Test + public void testAdditionsMeetDeletions() { + ResourceRecordSet validA = new ResourceRecordSet(); + validA.setName(ZONE1.getDnsName()); + validA.setType("A"); + validA.setRrdatas(ImmutableList.of("0.255.1.5")); + Change validChange = new Change(); + validChange.setAdditions(ImmutableList.of(validA)); + localDns.createZone(PROJECT_ID1, ZONE1, null); + localDns.createChange(PROJECT_ID1, ZONE_NAME1, validChange, null); + LocalDnsHelper.ZoneContainer container = localDns.findZone(PROJECT_ID1, ZONE_NAME1); + LocalDnsHelper.Response response = + LocalDnsHelper.additionsMeetDeletions(ImmutableList.of(validA), null, container); + assertEquals(409, response.code()); + assertTrue(response.body().contains("already exists")); + + } + + @Test + public void testCreateChangeValidatesChangeContent() { + ResourceRecordSet validA = new ResourceRecordSet(); + validA.setName(ZONE1.getDnsName()); + validA.setType("A"); + validA.setRrdatas(ImmutableList.of("0.255.1.5")); + Change validChange = new Change(); + validChange.setAdditions(ImmutableList.of(validA)); + localDns.createZone(PROJECT_ID1, ZONE1, null); + localDns.createChange(PROJECT_ID1, ZONE_NAME1, validChange, null); + LocalDnsHelper.Response response = + localDns.createChange(PROJECT_ID1, ZONE_NAME1, validChange, null); + assertEquals(409, response.code()); + assertTrue(response.body().contains("already exists")); + // delete with field mismatch + Change delete = new Change(); + validA.setTtl(20); // mismatch + delete.setDeletions(ImmutableList.of(validA)); + response = localDns.createChange(PROJECT_ID1, ZONE_NAME1, delete, null); + assertEquals(412, response.code()); + assertTrue(response.body().contains("entity.change.deletions[0]")); + // delete and add SOA + Change addition = new Change(); + ImmutableList rrsetWrappers + = localDns.findZone(PROJECT_ID1, ZONE_NAME1).dnsRecords().get(ZONE_NAME1); + LinkedList deletions = new LinkedList<>(); + LinkedList additions = new LinkedList<>(); + for (LocalDnsHelper.RrsetWrapper wrapper : rrsetWrappers) { + ResourceRecordSet rrset = wrapper.rrset(); + if (rrset.getType().equals("SOA")) { + deletions.add(rrset); + ResourceRecordSet copy = copyRrset(rrset); + copy.setName("x." + copy.getName()); + additions.add(copy); + break; + } + } + delete.setDeletions(deletions); + addition.setAdditions(additions); + response = localDns.createChange(PROJECT_ID1, ZONE_NAME1, delete, null); + assertEquals(400, response.code()); + assertTrue(response.body().contains( + "zone must contain exactly one resource record set of type 'SOA' at the apex")); + assertTrue(response.body().contains("deletions[0]")); + response = localDns.createChange(PROJECT_ID1, ZONE_NAME1, addition, null); + assertEquals(400, response.code()); + assertTrue(response.body().contains( + "zone must contain exactly one resource record set of type 'SOA' at the apex")); + assertTrue(response.body().contains("additions[0]")); + // delete NS + deletions = new LinkedList<>(); + additions = new LinkedList<>(); + for (LocalDnsHelper.RrsetWrapper wrapper : rrsetWrappers) { + ResourceRecordSet rrset = wrapper.rrset(); + if (rrset.getType().equals("NS")) { + deletions.add(rrset); + ResourceRecordSet copy = copyRrset(rrset); + copy.setName("x." + copy.getName()); + additions.add(copy); + break; + } + } + delete.setDeletions(deletions); + addition.setAdditions(additions); + response = localDns.createChange(PROJECT_ID1, ZONE_NAME1, delete, null); + assertEquals(400, response.code()); + assertTrue(response.body().contains( + "zone must contain exactly one resource record set of type 'NS' at the apex")); + response = localDns.createChange(PROJECT_ID1, ZONE_NAME1, addition, null); + assertEquals(400, response.code()); + assertTrue(response.body().contains( + "zone must contain exactly one resource record set of type 'NS' at the apex")); + assertTrue(response.body().contains("additions[0]")); + // change (delete + add) + addition.setDeletions(deletions); + response = localDns.createChange(PROJECT_ID1, ZONE_NAME1, addition, null); + assertEquals(200, response.code()); + } + + @Test + public void testCreateChangeValidatesChange() { + localDns.createZone(PROJECT_ID1, ZONE1, null); + ResourceRecordSet validA = new ResourceRecordSet(); + validA.setName(ZONE1.getDnsName()); + validA.setType("A"); + validA.setRrdatas(ImmutableList.of("0.255.1.5")); + ResourceRecordSet invalidA = new ResourceRecordSet(); + invalidA.setName(ZONE1.getDnsName()); + invalidA.setType("A"); + invalidA.setRrdatas(ImmutableList.of("0.-255.1.5")); + Change validChange = new Change(); + validChange.setAdditions(ImmutableList.of(validA)); + Change invalidChange = new Change(); + invalidChange.setAdditions(ImmutableList.of(invalidA)); + LocalDnsHelper.Response response = + localDns.createChange(PROJECT_ID1, ZONE_NAME1, validChange, null); + assertEquals(200, response.code()); + response = localDns.createChange(PROJECT_ID1, ZONE_NAME1, invalidChange, null); + assertEquals(400, response.code()); + // only empty additions/deletions + Change empty = new Change(); + empty.setAdditions(ImmutableList.of()); + empty.setDeletions(ImmutableList.of()); + response = localDns.createChange(PROJECT_ID1, ZONE_NAME1, empty, null); + assertEquals(400, response.code()); + assertTrue(response.body().contains( + "The 'entity.change' parameter is required but was missing.")); + // non-matching name + validA.setName(ZONE1.getDnsName() + ".aaa."); + response = localDns.createChange(PROJECT_ID1, ZONE_NAME1, validChange, null); + assertEquals(400, response.code()); + assertTrue(response.body().contains("additions[0].name")); + // wrong type + validA.setName(ZONE1.getDnsName()); // revert + validA.setType("ABCD"); + response = localDns.createChange(PROJECT_ID1, ZONE_NAME1, validChange, null); + assertEquals(400, response.code()); + assertTrue(response.body().contains("additions[0].type")); + // wrong ttl + validA.setType("A"); // revert + validA.setTtl(-1); + response = localDns.createChange(PROJECT_ID1, ZONE_NAME1, validChange, null); + assertEquals(400, response.code()); + assertTrue(response.body().contains("additions[0].ttl")); + validA.setTtl(null); // revert + // null name + validA.setName(null); + response = localDns.createChange(PROJECT_ID1, ZONE_NAME1, validChange, null); + assertEquals(400, response.code()); + assertTrue(response.body().contains("additions[0].name")); + validA.setName(ZONE1.getDnsName()); + // null type + validA.setType(null); + response = localDns.createChange(PROJECT_ID1, ZONE_NAME1, validChange, null); + assertEquals(400, response.code()); + assertTrue(response.body().contains("additions[0].type")); + validA.setType("A"); + // null rrdata + List temp = validA.getRrdatas(); // preserve + validA.setRrdatas(null); + response = localDns.createChange(PROJECT_ID1, ZONE_NAME1, validChange, null); + assertEquals(400, response.code()); + assertTrue(response.body().contains("additions[0].rrdata")); + validA.setRrdatas(temp); + // delete non-existent + ResourceRecordSet nonExistent = new ResourceRecordSet(); + nonExistent.setName(ZONE1.getDnsName()); + nonExistent.setType("AAAA"); + nonExistent.setRrdatas(ImmutableList.of(":::::::")); + Change delete = new Change(); + delete.setDeletions(ImmutableList.of(nonExistent)); + response = localDns.createChange(PROJECT_ID1, ZONE_NAME1, delete, null); + assertEquals(404, response.code()); + assertTrue(response.body().contains("deletions[0]")); + } + + private static ManagedZone copyZone(ManagedZone original) { + ManagedZone copy = new ManagedZone(); + copy.setDescription(original.getDescription()); + copy.setName(original.getName()); + copy.setCreationTime(original.getCreationTime()); + copy.setId(original.getId()); + copy.setNameServerSet(original.getNameServerSet()); + copy.setDnsName(original.getDnsName()); + if (original.getNameServers() != null) { + copy.setNameServers(ImmutableList.copyOf(original.getNameServers())); + } + return copy; + } + + private static ResourceRecordSet copyRrset(ResourceRecordSet set) { + ResourceRecordSet copy = new ResourceRecordSet(); + if (set.getRrdatas() != null) { + copy.setRrdatas(ImmutableList.copyOf(set.getRrdatas())); + } + copy.setTtl(set.getTtl()); + copy.setName(set.getName()); + copy.setType(set.getType()); + return copy; + } +} From 04f354a16a8759af38d3e883b9bb9dcc467744f3 Mon Sep 17 00:00:00 2001 From: Martin Derka Date: Tue, 23 Feb 2016 11:38:51 -0800 Subject: [PATCH 2/3] Added RPC layer of tests for local helpers. Closes #665. Debugged parsing for options. Fixed context for the server URLs and regular expression parsing. Fixed paging. Improved error detection for missing zones vs. non-existent changes. --- .../com/google/gcloud/spi/DefaultDnsRpc.java | 4 +- .../google/gcloud/testing/LocalDnsHelper.java | 126 +- .../testing/OptionParsersAndExtractors.java | 31 +- .../gcloud/testing/LocalDnsHelperTest.java | 1176 ++++++++++++++--- 4 files changed, 1097 insertions(+), 240 deletions(-) diff --git a/gcloud-java-dns/src/main/java/com/google/gcloud/spi/DefaultDnsRpc.java b/gcloud-java-dns/src/main/java/com/google/gcloud/spi/DefaultDnsRpc.java index 6ed9c7e0f216..b72a21445a80 100644 --- a/gcloud-java-dns/src/main/java/com/google/gcloud/spi/DefaultDnsRpc.java +++ b/gcloud-java-dns/src/main/java/com/google/gcloud/spi/DefaultDnsRpc.java @@ -162,7 +162,9 @@ public Change getChangeRequest(String zoneName, String changeRequestId, MapWhile the mock attempts to simulate the service, there are some differences in the behaviour. * The mock will accept any project ID and never returns a notFound or another error because of - * project ID. It assumes that all project IDs exists and that the user has all the necessary + * project ID. It assumes that all project IDs exist and that the user has all the necessary * privileges to manipulate any project. Similarly, the local simulation does not work with any * verification of domain name ownership. Any request for creating a managed zone will be approved. * The mock does not track quota and will allow the user to exceed it. The mock provides only basic @@ -92,11 +93,10 @@ public class LocalDnsHelper { = new ConcurrentSkipListMap<>(); private static final URI BASE_CONTEXT; private static final Logger log = Logger.getLogger(LocalDnsHelper.class.getName()); - private static final JsonFactory jsonFactory = - new com.google.api.client.json.jackson.JacksonFactory(); + private static final JsonFactory jsonFactory = new JacksonFactory(); private static final Random ID_GENERATOR = new Random(); private static final String VERSION = "v1"; - private static final String CONTEXT = "/" + VERSION + "/projects"; + private static final String CONTEXT = "/dns/" + VERSION + "/projects"; private static final Set SUPPORTED_COMPRESSION_ENCODINGS = ImmutableSet.of("gzip", "x-gzip"); private static final List TYPES = ImmutableList.of("A", "AAAA", "CNAME", "MX", "NAPTR", @@ -119,15 +119,15 @@ public class LocalDnsHelper { * For matching URLs to operations. */ private enum CallRegex { - CHANGE_CREATE("POST", "/[^/]+/managedZones/[^/]+/changes"), - CHANGE_GET("GET", "/[^/]+/managedZones/[^/]+/changes/[^/]+"), - CHANGE_LIST("GET", "/[^/]+/managedZones/[^/]+/changes"), - ZONE_CREATE("POST", "/[^/]+/managedZones"), - ZONE_DELETE("DELETE", "/[^/]+/managedZones/[^/]+"), - ZONE_GET("GET", "/[^/]+/managedZones/[^/]+"), - ZONE_LIST("GET", "/[^/]+/managedZones"), - PROJECT_GET("GET", "/[^/]+"), - RECORD_LIST("GET", "/[^/]+/managedZones/[^/]+/rrsets"); + CHANGE_CREATE("POST", CONTEXT + "/[^/]+/managedZones/[^/]+/changes"), + CHANGE_GET("GET", CONTEXT + "/[^/]+/managedZones/[^/]+/changes/[^/]+"), + CHANGE_LIST("GET", CONTEXT + "/[^/]+/managedZones/[^/]+/changes"), + ZONE_CREATE("POST", CONTEXT + "/[^/]+/managedZones"), + ZONE_DELETE("DELETE", CONTEXT + "/[^/]+/managedZones/[^/]+"), + ZONE_GET("GET", CONTEXT + "/[^/]+/managedZones/[^/]+"), + ZONE_LIST("GET", CONTEXT + "/[^/]+/managedZones"), + PROJECT_GET("GET", CONTEXT + "/[^/]+"), + RECORD_LIST("GET", CONTEXT + "/[^/]+/managedZones/[^/]+/rrsets"); private String method; private String pathRegex; @@ -220,14 +220,14 @@ ConcurrentSkipListMap zones() { } /** - * Associates a zone with a collection of changes and dns record. Thread safe. + * Associates a zone with a collection of changes and dns records. Thread safe. */ static class ZoneContainer { private final ManagedZone zone; /** * DNS records are held in a map to allow for atomic replacement of record sets when applying * changes. The key for the map is always the zone name. The collection of records is immutable - * and must always exist, i.e., dnsRecords.get(zone.getName()) is never null. + * and must always exist, i.e., dnsRecords.get(zone.getName()) is never {@code null}. */ private final ConcurrentSkipListMap> dnsRecords = new ConcurrentSkipListMap<>(); @@ -377,20 +377,19 @@ public void handle(HttpExchange exchange) throws IOException { writeResponse(exchange, Error.NOT_FOUND.response("The url does not match any API call.")); } - // todo(mderka) Test handlers using gcloud-java-dns. Issue #665. private Response handleZoneDelete(HttpExchange exchange) { String path = BASE_CONTEXT.relativize(exchange.getRequestURI()).getPath(); String[] tokens = path.split("/"); - String projectId = tokens[1]; - String zoneName = tokens[3]; + String projectId = tokens[0]; + String zoneName = tokens[2]; return deleteZone(projectId, zoneName); } private Response handleZoneGet(HttpExchange exchange) { String path = BASE_CONTEXT.relativize(exchange.getRequestURI()).getPath(); String[] tokens = path.split("/"); - String projectId = tokens[1]; - String zoneName = tokens[3]; + String projectId = tokens[0]; + String zoneName = tokens[2]; String query = BASE_CONTEXT.relativize(exchange.getRequestURI()).getQuery(); String[] fields = OptionParsersAndExtractors.parseGetOptions(query); return getZone(projectId, zoneName, fields); @@ -399,7 +398,7 @@ private Response handleZoneGet(HttpExchange exchange) { private Response handleZoneList(HttpExchange exchange) { String path = BASE_CONTEXT.relativize(exchange.getRequestURI()).getPath(); String[] tokens = path.split("/"); - String projectId = tokens[1]; + String projectId = tokens[0]; String query = BASE_CONTEXT.relativize(exchange.getRequestURI()).getQuery(); Map options = OptionParsersAndExtractors.parseListZonesOptions(query); return listZones(projectId, options); @@ -408,7 +407,7 @@ private Response handleZoneList(HttpExchange exchange) { private Response handleProjectGet(HttpExchange exchange) { String path = BASE_CONTEXT.relativize(exchange.getRequestURI()).getPath(); String[] tokens = path.split("/"); - String projectId = tokens[1]; + String projectId = tokens[0]; String query = BASE_CONTEXT.relativize(exchange.getRequestURI()).getQuery(); String[] fields = OptionParsersAndExtractors.parseGetOptions(query); return getProject(projectId, fields); @@ -420,8 +419,8 @@ private Response handleProjectGet(HttpExchange exchange) { private Response handleChangeCreate(HttpExchange exchange) throws IOException { String path = BASE_CONTEXT.relativize(exchange.getRequestURI()).getPath(); String[] tokens = path.split("/"); - String projectId = tokens[1]; - String zoneName = tokens[3]; + String projectId = tokens[0]; + String zoneName = tokens[2]; String query = BASE_CONTEXT.relativize(exchange.getRequestURI()).getQuery(); String[] fields = OptionParsersAndExtractors.parseGetOptions(query); String requestBody = decodeContent(exchange.getRequestHeaders(), exchange.getRequestBody()); @@ -432,9 +431,9 @@ private Response handleChangeCreate(HttpExchange exchange) throws IOException { private Response handleChangeGet(HttpExchange exchange) { String path = BASE_CONTEXT.relativize(exchange.getRequestURI()).getPath(); String[] tokens = path.split("/"); - String projectId = tokens[1]; - String zoneName = tokens[3]; - String changeId = tokens[5]; + String projectId = tokens[0]; + String zoneName = tokens[2]; + String changeId = tokens[4]; String query = BASE_CONTEXT.relativize(exchange.getRequestURI()).getQuery(); String[] fields = OptionParsersAndExtractors.parseGetOptions(query); return getChange(projectId, zoneName, changeId, fields); @@ -443,8 +442,8 @@ private Response handleChangeGet(HttpExchange exchange) { private Response handleChangeList(HttpExchange exchange) { String path = BASE_CONTEXT.relativize(exchange.getRequestURI()).getPath(); String[] tokens = path.split("/"); - String projectId = tokens[1]; - String zoneName = tokens[3]; + String projectId = tokens[0]; + String zoneName = tokens[2]; String query = BASE_CONTEXT.relativize(exchange.getRequestURI()).getQuery(); Map options = OptionParsersAndExtractors.parseListChangesOptions(query); return listChanges(projectId, zoneName, options); @@ -453,8 +452,8 @@ private Response handleChangeList(HttpExchange exchange) { private Response handleDnsRecordList(HttpExchange exchange) { String path = BASE_CONTEXT.relativize(exchange.getRequestURI()).getPath(); String[] tokens = path.split("/"); - String projectId = tokens[1]; - String zoneName = tokens[3]; + String projectId = tokens[0]; + String zoneName = tokens[2]; String query = BASE_CONTEXT.relativize(exchange.getRequestURI()).getQuery(); Map options = OptionParsersAndExtractors.parseListDnsRecordsOptions(query); return listDnsRecords(projectId, zoneName, options); @@ -466,7 +465,7 @@ private Response handleDnsRecordList(HttpExchange exchange) { private Response handleZoneCreate(HttpExchange exchange) throws IOException { String path = BASE_CONTEXT.relativize(exchange.getRequestURI()).getPath(); String[] tokens = path.split("/"); - String projectId = tokens[1]; + String projectId = tokens[0]; String query = BASE_CONTEXT.relativize(exchange.getRequestURI()).getQuery(); String[] options = OptionParsersAndExtractors.parseGetOptions(query); String requestBody = decodeContent(exchange.getRequestHeaders(), exchange.getRequestBody()); @@ -539,7 +538,9 @@ private static void writeResponse(HttpExchange exchange, Response response) { try { exchange.getResponseHeaders().add("Connection", "close"); exchange.sendResponseHeaders(response.code(), response.body().length()); - outputStream.write(response.body().getBytes(StandardCharsets.UTF_8)); + if (response.code() != 204) { + outputStream.write(response.body().getBytes(StandardCharsets.UTF_8)); + } outputStream.close(); } catch (IOException e) { log.log(Level.WARNING, "IOException encountered when sending response.", e); @@ -569,18 +570,20 @@ private static String decodeContent(Headers headers, InputStream inputStream) th } /** - * Generates a JSON response. Tested. + * Generates a JSON response. + * + * @param context managedZones | projects | rrsets | changes */ - static Response toListResponse(List serializedObjects, String pageToken, + static Response toListResponse(List serializedObjects, String context, String pageToken, boolean includePageToken) { // start building response StringBuilder responseBody = new StringBuilder(); - responseBody.append("{\"projects\": ["); + responseBody.append("{\"").append(context).append("\": ["); Joiner.on(",").appendTo(responseBody, serializedObjects); responseBody.append("]"); // add page token only if exists and is asked for if (pageToken != null && includePageToken) { - responseBody.append(",\"pageToken\": \"").append(pageToken).append("\""); + responseBody.append(",\"nextPageToken\": \"").append(pageToken).append("\""); } responseBody.append("}"); return new Response(HTTP_OK, responseBody.toString()); @@ -627,14 +630,13 @@ static List randomNameservers() { } /** - * Returns a hex string id (used for a dns record) unique withing the set of wrappers. + * Returns a hex string id (used for a dns record) unique within the set of wrappers. */ static String getUniqueId(List wrappers) { TreeSet ids = Sets.newTreeSet(Lists.transform(wrappers, new Function() { - @Nullable @Override - public String apply(@Nullable RrsetWrapper input) { + public String apply(RrsetWrapper input) { return input.id() == null ? "null" : input.id(); } })); @@ -663,7 +665,8 @@ static boolean matchesCriteria(ResourceRecordSet record, String name, String typ } /** - * Returns a project container. Never returns null because we assume that all projects exists. + * Returns a project container. Never returns {@code null} because we assume that all projects + * exists. */ ProjectContainer findProject(String projectId) { ProjectContainer defaultProject = createProject(projectId); @@ -672,7 +675,7 @@ ProjectContainer findProject(String projectId) { } /** - * Returns a zone container. Returns null if zone does not exist within project. + * Returns a zone container. Returns {@code null} if zone does not exist within project. */ ZoneContainer findZone(String projectId, String zoneName) { ProjectContainer projectContainer = findProject(projectId); // never null @@ -680,7 +683,7 @@ ZoneContainer findZone(String projectId, String zoneName) { } /** - * Returns a change found by its id. Returns null if such a change does not exist. + * Returns a change found by its id. Returns {@code null} if such a change does not exist. */ Change findChange(String projectId, String zoneName, String changeId) { ZoneContainer wrapper = findZone(projectId, zoneName); @@ -688,7 +691,7 @@ Change findChange(String projectId, String zoneName, String changeId) { } /** - * Returns a response to get change call. + * Returns a response to getChange service call. */ Response getChange(String projectId, String zoneName, String changeId, String[] fields) { Change change = findChange(projectId, zoneName, changeId); @@ -711,11 +714,10 @@ Response getChange(String projectId, String zoneName, String changeId, String[] "Error when serializing change %s in managed zone %s in project %s.", changeId, zoneName, projectId)); } - // todo(mderka) Test field options within #665. } /** - * Returns a response to get zone call. + * Returns a response to getZone service call. */ Response getZone(String projectId, String zoneName, String[] fields) { ZoneContainer container = findZone(projectId, zoneName); @@ -730,7 +732,6 @@ Response getZone(String projectId, String zoneName, String[] fields) { return Error.INTERNAL_ERROR.response(String.format( "Error when serializing managed zone %s in project %s.", zoneName, projectId)); } - // todo(mderka) Test field options within #665. } /** @@ -748,7 +749,6 @@ Response getProject(String projectId, String[] fields) { return Error.INTERNAL_ERROR.response( String.format("Error when serializing project %s.", projectId)); } - // todo(mderka) Test field options within #665. } /** @@ -798,6 +798,7 @@ Response createZone(String projectId, ManagedZone zone, String[] fields) { ManagedZone completeZone = new ManagedZone(); completeZone.setName(zone.getName()); completeZone.setDnsName(zone.getDnsName()); + completeZone.setDescription(zone.getDescription()); completeZone.setNameServerSet(zone.getNameServerSet()); completeZone.setCreationTime(ISODateTimeFormat.dateTime().withZoneUTC() .print(System.currentTimeMillis())); @@ -823,7 +824,6 @@ Response createZone(String projectId, ManagedZone zone, String[] fields) { return Error.INTERNAL_ERROR.response( String.format("Error when serializing managed zone %s.", result.getName())); } - // todo(mderka) Test field options within #665. } /** @@ -873,7 +873,6 @@ we will reset all IDs in this range (all of them are valid) */ String.format("Error when serializing change %s in managed zone %s in project %s.", result.getId(), zoneName, projectId)); } - // todo(mderka) Test field options within #665. } /** @@ -969,13 +968,13 @@ Response listZones(String projectId, Map options) { ManagedZone zone = zoneContainer.zone(); if (listing) { if (dnsName == null || zone.getDnsName().equals(dnsName)) { - lastZoneName = zone.getName(); if (sizeReached) { // we do not add this, just note that there would be more and there should be a token hasMorePages = true; break; } else { try { + lastZoneName = zone.getName(); serializedZones.addLast(jsonFactory.toString( OptionParsersAndExtractors.extractFields(zone, fields))); } catch (IOException e) { @@ -991,9 +990,8 @@ Response listZones(String projectId, Map options) { sizeReached = (maxResults != null) && maxResults.equals(serializedZones.size()); } boolean includePageToken = - hasMorePages && (fields == null || ImmutableList.copyOf(fields).contains("pageToken")); - return toListResponse(serializedZones, lastZoneName, includePageToken); - // todo(mderka) Test field and listing options within #665. + hasMorePages && (fields == null || ImmutableList.copyOf(fields).contains("nextPageToken")); + return toListResponse(serializedZones, "managedZones", lastZoneName, includePageToken); } /** @@ -1025,12 +1023,12 @@ Response listDnsRecords(String projectId, String zoneName, Map o ResourceRecordSet record = recordWrapper.rrset(); if (listing) { if (matchesCriteria(record, name, type)) { - lastRecordId = recordWrapper.id(); if (sizeReached) { // we do not add this, just note that there would be more and there should be a token hasMorePages = true; break; } else { + lastRecordId = recordWrapper.id(); try { serializedRrsets.addLast(jsonFactory.toString( OptionParsersAndExtractors.extractFields(record, fields))); @@ -1047,9 +1045,8 @@ Response listDnsRecords(String projectId, String zoneName, Map o sizeReached = (maxResults != null) && maxResults.equals(serializedRrsets.size()); } boolean includePageToken = - hasMorePages && (fields == null || ImmutableList.copyOf(fields).contains("pageToken")); - return toListResponse(serializedRrsets, lastRecordId, includePageToken); - // todo(mderka) Test field and listing options within #665. + hasMorePages && (fields == null || ImmutableList.copyOf(fields).contains("nextPageToken")); + return toListResponse(serializedRrsets, "rrsets", lastRecordId, includePageToken); } /** @@ -1061,6 +1058,10 @@ Response listChanges(String projectId, String zoneName, Map opti return response; } ZoneContainer zoneContainer = findZone(projectId, zoneName); + if (zoneContainer == null) { + return Error.NOT_FOUND.response(String.format( + "The 'parameters.managedZone' resource named '%s' does not exist", zoneName)); + } // take a sorted snapshot of the current change list TreeMap changes = new TreeMap<>(); for (Change c : zoneContainer.changes()) { @@ -1088,12 +1089,12 @@ Response listChanges(String projectId, String zoneName, Map opti for (Integer key : keys) { Change change = changes.get(key); if (listing) { - lastChangeId = change.getId(); if (sizeReached) { // we do not add this, just note that there would be more and there should be a token hasMorePages = true; break; } else { + lastChangeId = change.getId(); try { serializedResults.addLast(jsonFactory.toString( OptionParsersAndExtractors.extractFields(change, fields))); @@ -1110,9 +1111,8 @@ Response listChanges(String projectId, String zoneName, Map opti sizeReached = (maxResults != null) && maxResults.equals(serializedResults.size()); } boolean includePageToken = - hasMorePages && (fields == null || ImmutableList.copyOf(fields).contains("pageToken")); - return toListResponse(serializedResults, lastChangeId, includePageToken); - // todo(mderka) Test field and listing options within #665. + hasMorePages && (fields == null || ImmutableList.copyOf(fields).contains("nextPageToken")); + return toListResponse(serializedResults, "changes", lastChangeId, includePageToken); } /** diff --git a/gcloud-java-dns/src/main/java/com/google/gcloud/testing/OptionParsersAndExtractors.java b/gcloud-java-dns/src/main/java/com/google/gcloud/testing/OptionParsersAndExtractors.java index f94cb15779ca..26759f7e3ccc 100644 --- a/gcloud-java-dns/src/main/java/com/google/gcloud/testing/OptionParsersAndExtractors.java +++ b/gcloud-java-dns/src/main/java/com/google/gcloud/testing/OptionParsersAndExtractors.java @@ -42,14 +42,19 @@ static Map parseListZonesOptions(String query) { switch (argEntry[0]) { case "fields": // List fields are in the form "managedZones(field1, field2, ...)" + String replaced = argEntry[1].replace("managedZones(", ","); + replaced = replaced.replace(")", ","); + // we will get empty strings, but it does not matter, they will be ignored options.put( "fields", - argEntry[1].substring("managedZones(".length(), argEntry[1].length() - 1) - .split(",")); + replaced.split(",")); break; case "dnsName": options.put("dnsName", argEntry[1]); break; + case "nextPageToken": + options.put("nextPageToken", argEntry[1]); + break; case "pageToken": options.put("pageToken", argEntry[1]); break; @@ -218,14 +223,16 @@ static Map parseListChangesOptions(String query) { String[] argEntry = arg.split("="); switch (argEntry[0]) { case "fields": - // todo we do not support fragmentation in deletions and additions - options.put( - "fields", - argEntry[1].substring("changes(".length(), argEntry[1].length() - 1).split(",")); + // todo we do not support fragmentation in deletions and additions in the library + String replaced = argEntry[1].replace("changes(", ",").replace(")", ","); + options.put("fields", replaced.split(",")); // empty strings will be ignored break; case "name": options.put("name", argEntry[1]); break; + case "nextPageToken": + options.put("nextPageToken", argEntry[1]); + break; case "pageToken": options.put("pageToken", argEntry[1]); break; @@ -255,16 +262,22 @@ static Map parseListDnsRecordsOptions(String query) { String[] argEntry = arg.split("="); switch (argEntry[0]) { case "fields": - options.put( - "fields", - argEntry[1].substring("rrsets(".length(), argEntry[1].length() - 1).split(",")); + String replace = argEntry[1].replace("rrsets(", ","); + replace = replace.replace(")", ","); + options.put("fields", replace.split(",")); // empty strings do not matter break; case "name": options.put("name", argEntry[1]); break; + case "type": + options.put("type", argEntry[1]); + break; case "pageToken": options.put("pageToken", argEntry[1]); break; + case "nextPageToken": + options.put("nextPageToken", argEntry[1]); + break; case "maxResults": // parsing to int is done while handling options.put("maxResults", argEntry[1]); diff --git a/gcloud-java-dns/src/test/java/com/google/gcloud/testing/LocalDnsHelperTest.java b/gcloud-java-dns/src/test/java/com/google/gcloud/testing/LocalDnsHelperTest.java index 1f851045659c..7a97b69846ef 100644 --- a/gcloud-java-dns/src/test/java/com/google/gcloud/testing/LocalDnsHelperTest.java +++ b/gcloud-java-dns/src/test/java/com/google/gcloud/testing/LocalDnsHelperTest.java @@ -18,6 +18,7 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; @@ -25,11 +26,16 @@ import com.google.api.services.dns.model.Change; import com.google.api.services.dns.model.ManagedZone; +import com.google.api.services.dns.model.Project; import com.google.api.services.dns.model.ResourceRecordSet; import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; import com.google.common.collect.Lists; +import com.google.gcloud.dns.DnsException; +import com.google.gcloud.spi.DefaultDnsRpc; +import com.google.gcloud.spi.DnsRpc; -import org.junit.After; +import org.junit.AfterClass; import org.junit.Before; import org.junit.BeforeClass; import org.junit.Test; @@ -55,8 +61,14 @@ public class LocalDnsHelperTest { private static final Change CHANGE1 = new Change(); private static final Change CHANGE2 = new Change(); private static final Change CHANGE_KEEP = new Change(); + private static final Change CHANGE_COMPLEX = new Change(); + private static final LocalDnsHelper LOCAL_DNS_HELPER = LocalDnsHelper.create(0L); // synchronous + private static final Map EMPTY_RPC_OPTIONS = ImmutableMap.of(); + private static final DnsRpc RPC = + new DefaultDnsRpc(LOCAL_DNS_HELPER.options()); + private static final String REAL_PROJECT_ID = LOCAL_DNS_HELPER.options().projectId(); private Map optionsMap; - private LocalDnsHelper localDns; + private ManagedZone minimalZone = new ManagedZone(); // to be adjusted as needed @BeforeClass @@ -67,9 +79,11 @@ public static void before() { ZONE1.setName(ZONE_NAME1); ZONE1.setDescription(""); ZONE1.setDnsName(DNS_NAME); + ZONE1.setNameServerSet("somenameserveset"); ZONE2.setName(ZONE_NAME2); ZONE2.setDescription(""); ZONE2.setDnsName(DNS_NAME); + ZONE2.setNameServerSet("somenameserveset"); RRSET2.setName(DNS_NAME); RRSET2.setType(RRSET_TYPE); RRSET_KEEP.setName(DNS_NAME); @@ -79,18 +93,27 @@ public static void before() { CHANGE1.setAdditions(ImmutableList.of(RRSET1, RRSET2)); CHANGE2.setDeletions(ImmutableList.of(RRSET2)); CHANGE_KEEP.setAdditions(ImmutableList.of(RRSET_KEEP)); + CHANGE_COMPLEX.setAdditions(ImmutableList.of(RRSET_KEEP)); + CHANGE_COMPLEX.setDeletions(ImmutableList.of(RRSET_KEEP)); + LOCAL_DNS_HELPER.start(); } @Before public void setUp() { - localDns = LocalDnsHelper.create(0L); // synchronous + resetProjects(); optionsMap = new HashMap<>(); minimalZone = copyZone(ZONE1); } - @After - public void after() { - localDns = null; + private static void resetProjects() { + for (String project : LOCAL_DNS_HELPER.projects().keySet()) { + LOCAL_DNS_HELPER.projects().remove(project); + } + } + + @AfterClass + public static void after() { + LOCAL_DNS_HELPER.stop(); } @Test @@ -109,13 +132,13 @@ public void testGetUniqueId() { @Test public void testFindProject() { - assertEquals(0, localDns.projects().size()); - LocalDnsHelper.ProjectContainer project = localDns.findProject(PROJECT_ID1); + assertEquals(0, LOCAL_DNS_HELPER.projects().size()); + LocalDnsHelper.ProjectContainer project = LOCAL_DNS_HELPER.findProject(PROJECT_ID1); assertNotNull(project); - assertTrue(localDns.projects().containsKey(PROJECT_ID1)); - assertNotNull(localDns.findProject(PROJECT_ID2)); - assertTrue(localDns.projects().containsKey(PROJECT_ID2)); - assertTrue(localDns.projects().containsKey(PROJECT_ID1)); + assertTrue(LOCAL_DNS_HELPER.projects().containsKey(PROJECT_ID1)); + assertNotNull(LOCAL_DNS_HELPER.findProject(PROJECT_ID2)); + assertTrue(LOCAL_DNS_HELPER.projects().containsKey(PROJECT_ID2)); + assertTrue(LOCAL_DNS_HELPER.projects().containsKey(PROJECT_ID1)); assertNotNull(project.zones()); assertEquals(0, project.zones().size()); assertNotNull(project.project()); @@ -124,11 +147,11 @@ public void testFindProject() { @Test public void testCreateAndFindZone() { - LocalDnsHelper.ZoneContainer zone1 = localDns.findZone(PROJECT_ID1, ZONE_NAME1); - assertTrue(localDns.projects().containsKey(PROJECT_ID1)); + LocalDnsHelper.ZoneContainer zone1 = LOCAL_DNS_HELPER.findZone(PROJECT_ID1, ZONE_NAME1); + assertTrue(LOCAL_DNS_HELPER.projects().containsKey(PROJECT_ID1)); assertNull(zone1); - localDns.createZone(PROJECT_ID1, ZONE1, null); // we do not care about options - zone1 = localDns.findZone(PROJECT_ID1, ZONE1.getName()); + LOCAL_DNS_HELPER.createZone(PROJECT_ID1, ZONE1, null); // we do not care about options + zone1 = LOCAL_DNS_HELPER.findZone(PROJECT_ID1, ZONE1.getName()); assertNotNull(zone1); // cannot call equals because id and timestamp got assigned assertEquals(ZONE_NAME1, zone1.zone().getName()); @@ -136,56 +159,99 @@ public void testCreateAndFindZone() { assertTrue(zone1.changes().isEmpty()); assertNotNull(zone1.dnsRecords()); assertEquals(2, zone1.dnsRecords().get(ZONE_NAME1).size()); // default SOA and NS - localDns.createZone(PROJECT_ID2, ZONE1, null); // project does not exits yet - assertEquals(ZONE1.getName(), localDns.findZone(PROJECT_ID2, ZONE_NAME1).zone().getName()); + LOCAL_DNS_HELPER.createZone(PROJECT_ID2, ZONE1, null); // project does not exits yet + assertEquals(ZONE1.getName(), + LOCAL_DNS_HELPER.findZone(PROJECT_ID2, ZONE_NAME1).zone().getName()); + } + + @Test + public void testCreateAndFindZoneUsingRpc() { + // zone does not exist yet + ManagedZone zone1 = RPC.getZone(ZONE_NAME1, EMPTY_RPC_OPTIONS); + assertTrue(LOCAL_DNS_HELPER.projects().containsKey(REAL_PROJECT_ID)); // check internal state + assertNull(zone1); + // create zone + ManagedZone createdZone = RPC.create(ZONE1, EMPTY_RPC_OPTIONS); + assertEquals(ZONE1.getName(), createdZone.getName()); + assertEquals(ZONE1.getDescription(), createdZone.getDescription()); + assertEquals(ZONE1.getDnsName(), createdZone.getDnsName()); + assertEquals(4, createdZone.getNameServers().size()); + // get the same zone zone + ManagedZone zone = RPC.getZone(ZONE1.getName(), EMPTY_RPC_OPTIONS); + assertEquals(createdZone, zone); + // check that default records were created + DnsRpc.ListResult resourceRecordSetListResult + = RPC.listDnsRecords(ZONE1.getName(), EMPTY_RPC_OPTIONS); + assertEquals(2, Lists.newLinkedList(resourceRecordSetListResult.results()).size()); } @Test public void testDeleteZone() { - localDns.createZone(PROJECT_ID1, ZONE1, null); - LocalDnsHelper.Response response = localDns.deleteZone(PROJECT_ID1, ZONE1.getName()); + LOCAL_DNS_HELPER.createZone(PROJECT_ID1, ZONE1, null); + LocalDnsHelper.Response response = LOCAL_DNS_HELPER.deleteZone(PROJECT_ID1, ZONE1.getName()); assertEquals(204, response.code()); // deleting non-existent zone - response = localDns.deleteZone(PROJECT_ID1, ZONE1.getName()); + response = LOCAL_DNS_HELPER.deleteZone(PROJECT_ID1, ZONE1.getName()); assertEquals(404, response.code()); - assertNull(localDns.findZone(PROJECT_ID1, ZONE1.getName())); - localDns.createZone(PROJECT_ID1, ZONE1, null); - localDns.createZone(PROJECT_ID1, ZONE2, null); - assertNotNull(localDns.findZone(PROJECT_ID1, ZONE1.getName())); - assertNotNull(localDns.findZone(PROJECT_ID1, ZONE2.getName())); + assertNull(LOCAL_DNS_HELPER.findZone(PROJECT_ID1, ZONE1.getName())); + LOCAL_DNS_HELPER.createZone(PROJECT_ID1, ZONE1, null); + LOCAL_DNS_HELPER.createZone(PROJECT_ID1, ZONE2, null); + assertNotNull(LOCAL_DNS_HELPER.findZone(PROJECT_ID1, ZONE1.getName())); + assertNotNull(LOCAL_DNS_HELPER.findZone(PROJECT_ID1, ZONE2.getName())); // delete in reverse order - response = localDns.deleteZone(PROJECT_ID1, ZONE1.getName()); + response = LOCAL_DNS_HELPER.deleteZone(PROJECT_ID1, ZONE1.getName()); assertEquals(204, response.code()); - assertNull(localDns.findZone(PROJECT_ID1, ZONE1.getName())); - assertNotNull(localDns.findZone(PROJECT_ID1, ZONE2.getName())); - localDns.deleteZone(PROJECT_ID1, ZONE2.getName()); + assertNull(LOCAL_DNS_HELPER.findZone(PROJECT_ID1, ZONE1.getName())); + assertNotNull(LOCAL_DNS_HELPER.findZone(PROJECT_ID1, ZONE2.getName())); + LOCAL_DNS_HELPER.deleteZone(PROJECT_ID1, ZONE2.getName()); assertEquals(204, response.code()); - assertNull(localDns.findZone(PROJECT_ID1, ZONE1.getName())); - assertNull(localDns.findZone(PROJECT_ID1, ZONE2.getName())); + assertNull(LOCAL_DNS_HELPER.findZone(PROJECT_ID1, ZONE1.getName())); + assertNull(LOCAL_DNS_HELPER.findZone(PROJECT_ID1, ZONE2.getName())); + } + + @Test + public void testDeleteZoneUsingRpc() { + RPC.create(ZONE1, EMPTY_RPC_OPTIONS); + assertTrue(RPC.deleteZone(ZONE1.getName())); + assertNull(RPC.getZone(ZONE1.getName(), EMPTY_RPC_OPTIONS)); + // deleting non-existent zone + assertFalse(RPC.deleteZone(ZONE1.getName())); + assertNull(RPC.getZone(ZONE1.getName(), EMPTY_RPC_OPTIONS)); + RPC.create(ZONE1, EMPTY_RPC_OPTIONS); + RPC.create(ZONE2, EMPTY_RPC_OPTIONS); + assertNotNull(RPC.getZone(ZONE1.getName(), EMPTY_RPC_OPTIONS)); + assertNotNull(RPC.getZone(ZONE2.getName(), EMPTY_RPC_OPTIONS)); + // delete in reverse order + assertTrue(RPC.deleteZone(ZONE1.getName())); + assertNull(RPC.getZone(ZONE1.getName(), EMPTY_RPC_OPTIONS)); + assertNotNull(RPC.getZone(ZONE2.getName(), EMPTY_RPC_OPTIONS)); + assertTrue(RPC.deleteZone(ZONE2.getName())); + assertNull(RPC.getZone(ZONE1.getName(), EMPTY_RPC_OPTIONS)); + assertNull(RPC.getZone(ZONE2.getName(), EMPTY_RPC_OPTIONS)); } @Test public void testCreateAndApplyChange() { - localDns = LocalDnsHelper.create(5 * 1000L); // we will be using threads here - localDns.createZone(PROJECT_ID1, ZONE1, null); - assertNull(localDns.findZone(PROJECT_ID1, ZONE_NAME1).findChange("1")); + LocalDnsHelper localDnsThreaded = LocalDnsHelper.create(5 * 1000L); // using threads here + localDnsThreaded.createZone(PROJECT_ID1, ZONE1, null); + assertNull(localDnsThreaded.findZone(PROJECT_ID1, ZONE_NAME1).findChange("1")); LocalDnsHelper.Response response - = localDns.createChange(PROJECT_ID1, ZONE_NAME1, CHANGE1, null); // add + = localDnsThreaded.createChange(PROJECT_ID1, ZONE_NAME1, CHANGE1, null); // add assertEquals(200, response.code()); - assertNotNull(localDns.findZone(PROJECT_ID1, ZONE_NAME1).findChange("1")); - assertNull(localDns.findZone(PROJECT_ID1, ZONE_NAME1).findChange("2")); - localDns.createChange(PROJECT_ID1, ZONE_NAME1, CHANGE1, null); // add - response = localDns.createChange(PROJECT_ID1, ZONE_NAME1, CHANGE1, null); // add + assertNotNull(localDnsThreaded.findZone(PROJECT_ID1, ZONE_NAME1).findChange("1")); + assertNull(localDnsThreaded.findZone(PROJECT_ID1, ZONE_NAME1).findChange("2")); + localDnsThreaded.createChange(PROJECT_ID1, ZONE_NAME1, CHANGE1, null); // add + response = localDnsThreaded.createChange(PROJECT_ID1, ZONE_NAME1, CHANGE1, null); // add assertEquals(200, response.code()); - assertNotNull(localDns.findZone(PROJECT_ID1, ZONE_NAME1).findChange("1")); - assertNotNull(localDns.findZone(PROJECT_ID1, ZONE_NAME1).findChange("2")); - localDns.createChange(PROJECT_ID1, ZONE_NAME1, CHANGE2, null); // delete - assertNotNull(localDns.findZone(PROJECT_ID1, ZONE_NAME1).findChange("1")); - assertNotNull(localDns.findZone(PROJECT_ID1, ZONE_NAME1).findChange("2")); - assertNotNull(localDns.findZone(PROJECT_ID1, ZONE_NAME1).findChange("3")); - localDns.createChange(PROJECT_ID1, ZONE_NAME1, CHANGE_KEEP, null); // id is "4" + assertNotNull(localDnsThreaded.findZone(PROJECT_ID1, ZONE_NAME1).findChange("1")); + assertNotNull(localDnsThreaded.findZone(PROJECT_ID1, ZONE_NAME1).findChange("2")); + localDnsThreaded.createChange(PROJECT_ID1, ZONE_NAME1, CHANGE2, null); // delete + assertNotNull(localDnsThreaded.findZone(PROJECT_ID1, ZONE_NAME1).findChange("1")); + assertNotNull(localDnsThreaded.findZone(PROJECT_ID1, ZONE_NAME1).findChange("2")); + assertNotNull(localDnsThreaded.findZone(PROJECT_ID1, ZONE_NAME1).findChange("3")); + localDnsThreaded.createChange(PROJECT_ID1, ZONE_NAME1, CHANGE_KEEP, null); // id is "4" // check execution - Change change = localDns.findChange(PROJECT_ID1, ZONE_NAME1, "4"); + Change change = localDnsThreaded.findChange(PROJECT_ID1, ZONE_NAME1, "4"); for (int i = 0; i < 10 && !change.getStatus().equals("done"); i++) { // change has not been finished yet; wait at most 20 seconds // it takes 5 seconds for the thread to kick in in the first place @@ -197,26 +263,70 @@ public void testCreateAndApplyChange() { } assertEquals("done", change.getStatus()); List list = - localDns.findZone(PROJECT_ID1, ZONE_NAME1).dnsRecords().get(ZONE_NAME1); + localDnsThreaded.findZone(PROJECT_ID1, ZONE_NAME1).dnsRecords().get(ZONE_NAME1); assertTrue(list.contains(new LocalDnsHelper.RrsetWrapper(RRSET_KEEP))); + localDnsThreaded.stop(); + } + + @Test + public void testCreateAndApplyChangeUsingRpc() { + // not using threads + RPC.create(ZONE1, EMPTY_RPC_OPTIONS); + assertNull(RPC.getChangeRequest(ZONE1.getName(), "1", EMPTY_RPC_OPTIONS)); + //add + Change createdChange = RPC.applyChangeRequest(ZONE1.getName(), CHANGE1, EMPTY_RPC_OPTIONS); + assertEquals(createdChange.getAdditions(), CHANGE1.getAdditions()); + assertEquals(createdChange.getDeletions(), CHANGE1.getDeletions()); + assertNotNull(createdChange.getStartTime()); + assertEquals("1", createdChange.getId()); + Change retrievedChange = RPC.getChangeRequest(ZONE1.getName(), "1", EMPTY_RPC_OPTIONS); + assertEquals(createdChange, retrievedChange); + assertNull(RPC.getChangeRequest(ZONE1.getName(), "2", EMPTY_RPC_OPTIONS)); + try { + Change anotherChange = RPC.applyChangeRequest(ZONE1.getName(), CHANGE1, EMPTY_RPC_OPTIONS); + } catch (DnsException ex) { + assertEquals(409, ex.code()); + } + assertNotNull(RPC.getChangeRequest(ZONE1.getName(), "1", EMPTY_RPC_OPTIONS)); + assertNull(RPC.getChangeRequest(ZONE1.getName(), "2", EMPTY_RPC_OPTIONS)); + // delete + RPC.applyChangeRequest(ZONE1.getName(), CHANGE2, EMPTY_RPC_OPTIONS); + assertNotNull(RPC.getChangeRequest(ZONE1.getName(), "1", EMPTY_RPC_OPTIONS)); + assertNotNull(RPC.getChangeRequest(ZONE1.getName(), "2", EMPTY_RPC_OPTIONS)); + Change last = RPC.applyChangeRequest(ZONE1.getName(), CHANGE_KEEP, EMPTY_RPC_OPTIONS); + assertEquals("done", last.getStatus()); + // todo(mderka) replace with real call + List list = + LOCAL_DNS_HELPER.findZone(REAL_PROJECT_ID, ZONE_NAME1).dnsRecords().get(ZONE_NAME1); + assertTrue(list.contains(new LocalDnsHelper.RrsetWrapper(RRSET_KEEP))); + Iterable results = + RPC.listDnsRecords(ZONE1.getName(), EMPTY_RPC_OPTIONS).results(); + boolean ok = false; + for (ResourceRecordSet dnsRecord : results) { + if (dnsRecord.getName().equals(RRSET_KEEP.getName()) + && dnsRecord.getType().equals(RRSET_KEEP.getType())) { + ok = true; + } + } + assertTrue(ok); } @Test public void testFindChange() { - localDns.createZone(PROJECT_ID1, ZONE1, null); - Change change = localDns.findChange(PROJECT_ID1, ZONE1.getName(), "somerandomchange"); + LOCAL_DNS_HELPER.createZone(PROJECT_ID1, ZONE1, null); + Change change = LOCAL_DNS_HELPER.findChange(PROJECT_ID1, ZONE1.getName(), "somerandomchange"); assertNull(change); - localDns.createChange(PROJECT_ID1, ZONE1.getName(), CHANGE1, null); + LOCAL_DNS_HELPER.createChange(PROJECT_ID1, ZONE1.getName(), CHANGE1, null); // changes are sequential so we should find ID 1 - assertNotNull(localDns.findChange(PROJECT_ID1, ZONE1.getName(), "1")); + assertNotNull(LOCAL_DNS_HELPER.findChange(PROJECT_ID1, ZONE1.getName(), "1")); // add another - localDns.createChange(PROJECT_ID1, ZONE_NAME1, CHANGE2, null); - assertNotNull(localDns.findChange(PROJECT_ID1, ZONE1.getName(), "1")); - assertNotNull(localDns.findChange(PROJECT_ID1, ZONE1.getName(), "2")); + LOCAL_DNS_HELPER.createChange(PROJECT_ID1, ZONE_NAME1, CHANGE2, null); + assertNotNull(LOCAL_DNS_HELPER.findChange(PROJECT_ID1, ZONE1.getName(), "1")); + assertNotNull(LOCAL_DNS_HELPER.findChange(PROJECT_ID1, ZONE1.getName(), "2")); // try to find non-existent change - assertNull(localDns.findChange(PROJECT_ID1, ZONE1.getName(), "3")); + assertNull(LOCAL_DNS_HELPER.findChange(PROJECT_ID1, ZONE1.getName(), "3")); // try to find a change in yet non-existent project - assertNull(localDns.findChange(PROJECT_ID2, ZONE1.getName(), "3")); + assertNull(LOCAL_DNS_HELPER.findChange(PROJECT_ID2, ZONE1.getName(), "3")); } @Test @@ -230,71 +340,375 @@ public void testRandomNameServers() { @Test public void testGetProject() { // only interested in no exceptions and non-null response here - assertNotNull(localDns.getProject(PROJECT_ID1, null)); - assertNotNull(localDns.getProject(PROJECT_ID2, null)); + assertNotNull(LOCAL_DNS_HELPER.getProject(PROJECT_ID1, null)); + assertNotNull(LOCAL_DNS_HELPER.getProject(PROJECT_ID2, null)); + Project project = RPC.getProject(EMPTY_RPC_OPTIONS); + assertNotNull(project.getQuota()); + assertEquals(REAL_PROJECT_ID, project.getId()); + // fields options + Map options = new HashMap<>(); + options.put(DnsRpc.Option.FIELDS, "number"); + project = RPC.getProject(options); + assertNull(project.getId()); + assertNotNull(project.getNumber()); + assertNull(project.getQuota()); + options.put(DnsRpc.Option.FIELDS, "id"); + project = RPC.getProject(options); + assertNotNull(project.getId()); + assertNull(project.getNumber()); + assertNull(project.getQuota()); + options.put(DnsRpc.Option.FIELDS, "quota"); + project = RPC.getProject(options); + assertNull(project.getId()); + assertNull(project.getNumber()); + assertNotNull(project.getQuota()); } @Test public void testGetZone() { // non-existent - LocalDnsHelper.Response response = localDns.getZone(PROJECT_ID1, ZONE_NAME1, null); + LocalDnsHelper.Response response = LOCAL_DNS_HELPER.getZone(PROJECT_ID1, ZONE_NAME1, null); assertEquals(404, response.code()); assertTrue(response.body().contains("does not exist")); // existent - localDns.createZone(PROJECT_ID1, ZONE1, null); - response = localDns.getZone(PROJECT_ID1, ZONE1.getName(), null); + LOCAL_DNS_HELPER.createZone(PROJECT_ID1, ZONE1, null); + response = LOCAL_DNS_HELPER.getZone(PROJECT_ID1, ZONE1.getName(), null); assertEquals(200, response.code()); } + @Test + public void testGetZoneUsingRpc() { + // non-existent + assertNull(RPC.getZone(ZONE_NAME1, EMPTY_RPC_OPTIONS)); + // existent + ManagedZone created = RPC.create(ZONE1, EMPTY_RPC_OPTIONS); + ManagedZone zone = RPC.getZone(ZONE_NAME1, EMPTY_RPC_OPTIONS); + assertEquals(created, zone); + assertEquals(ZONE1.getName(), zone.getName()); + // field options + Map options = new HashMap<>(); + options.put(DnsRpc.Option.FIELDS, "id"); + zone = RPC.getZone(ZONE1.getName(), options); + assertNull(zone.getCreationTime()); + assertNull(zone.getName()); + assertNull(zone.getDnsName()); + assertNull(zone.getDescription()); + assertNull(zone.getNameServers()); + assertNull(zone.getNameServerSet()); + assertNotNull(zone.getId()); + options.put(DnsRpc.Option.FIELDS, "creationTime"); + zone = RPC.getZone(ZONE1.getName(), options); + assertNotNull(zone.getCreationTime()); + assertNull(zone.getName()); + assertNull(zone.getDnsName()); + assertNull(zone.getDescription()); + assertNull(zone.getNameServers()); + assertNull(zone.getNameServerSet()); + assertNull(zone.getId()); + options.put(DnsRpc.Option.FIELDS, "dnsName"); + zone = RPC.getZone(ZONE1.getName(), options); + assertNull(zone.getCreationTime()); + assertNull(zone.getName()); + assertNotNull(zone.getDnsName()); + assertNull(zone.getDescription()); + assertNull(zone.getNameServers()); + assertNull(zone.getNameServerSet()); + assertNull(zone.getId()); + options.put(DnsRpc.Option.FIELDS, "description"); + zone = RPC.getZone(ZONE1.getName(), options); + assertNull(zone.getCreationTime()); + assertNull(zone.getName()); + assertNull(zone.getDnsName()); + assertNotNull(zone.getDescription()); + assertNull(zone.getNameServers()); + assertNull(zone.getNameServerSet()); + assertNull(zone.getId()); + options.put(DnsRpc.Option.FIELDS, "nameServers"); + zone = RPC.getZone(ZONE1.getName(), options); + assertNull(zone.getCreationTime()); + assertNull(zone.getName()); + assertNull(zone.getDnsName()); + assertNull(zone.getDescription()); + assertNotNull(zone.getNameServers()); + assertNull(zone.getNameServerSet()); + assertNull(zone.getId()); + options.put(DnsRpc.Option.FIELDS, "nameServerSet"); + zone = RPC.getZone(ZONE1.getName(), options); + assertNull(zone.getCreationTime()); + assertNull(zone.getName()); + assertNull(zone.getDnsName()); + assertNull(zone.getDescription()); + assertNull(zone.getNameServers()); + assertNotNull(zone.getNameServerSet()); + assertNull(zone.getId()); + // several combined + options.put(DnsRpc.Option.FIELDS, "nameServerSet,description,id,name"); + zone = RPC.getZone(ZONE1.getName(), options); + assertNull(zone.getCreationTime()); + assertNotNull(zone.getName()); + assertNull(zone.getDnsName()); + assertNotNull(zone.getDescription()); + assertNull(zone.getNameServers()); + assertNotNull(zone.getNameServerSet()); + assertNotNull(zone.getId()); + } + @Test public void testCreateZone() { // only interested in no exceptions and non-null response here - LocalDnsHelper.Response response = localDns.createZone(PROJECT_ID1, ZONE1, null); + LocalDnsHelper.Response response = LOCAL_DNS_HELPER.createZone(PROJECT_ID1, ZONE1, null); assertEquals(200, response.code()); - assertEquals(1, localDns.projects().get(PROJECT_ID1).zones().size()); + assertEquals(1, LOCAL_DNS_HELPER.projects().get(PROJECT_ID1).zones().size()); try { - localDns.createZone(PROJECT_ID1, null, null); + LOCAL_DNS_HELPER.createZone(PROJECT_ID1, null, null); fail("Zone cannot be null"); } catch (NullPointerException ex) { // expected } // create zone twice - response = localDns.createZone(PROJECT_ID1, ZONE1, null); + response = LOCAL_DNS_HELPER.createZone(PROJECT_ID1, ZONE1, null); assertEquals(409, response.code()); assertTrue(response.body().contains("already exists")); } + @Test + public void testCreateZoneUsingRpc() { + ManagedZone created = RPC.create(ZONE1, EMPTY_RPC_OPTIONS); + assertEquals(created, LOCAL_DNS_HELPER.findZone(REAL_PROJECT_ID, ZONE1.getName()).zone()); + ManagedZone zone = RPC.getZone(ZONE_NAME1, EMPTY_RPC_OPTIONS); + assertEquals(created, zone); + try { + RPC.create(null, EMPTY_RPC_OPTIONS); + fail("Zone cannot be null"); + } catch (DnsException ex) { + // expected + assertEquals(400, ex.code()); + assertTrue(ex.getMessage().contains("entity.managedZone")); + } + // create zone twice + try { + RPC.create(ZONE1, EMPTY_RPC_OPTIONS); + } catch (DnsException ex) { + // expected + assertEquals(409, ex.code()); + assertTrue(ex.getMessage().contains("already exists")); + } + // field options + resetProjects(); + Map options = new HashMap<>(); + options.put(DnsRpc.Option.FIELDS, "id"); + zone = RPC.create(ZONE1, options); + assertNull(zone.getCreationTime()); + assertNull(zone.getName()); + assertNull(zone.getDnsName()); + assertNull(zone.getDescription()); + assertNull(zone.getNameServers()); + assertNull(zone.getNameServerSet()); + assertNotNull(zone.getId()); + resetProjects(); + options.put(DnsRpc.Option.FIELDS, "creationTime"); + zone = RPC.create(ZONE1, options); + assertNotNull(zone.getCreationTime()); + assertNull(zone.getName()); + assertNull(zone.getDnsName()); + assertNull(zone.getDescription()); + assertNull(zone.getNameServers()); + assertNull(zone.getNameServerSet()); + assertNull(zone.getId()); + options.put(DnsRpc.Option.FIELDS, "dnsName"); + resetProjects(); + zone = RPC.create(ZONE1, options); + assertNull(zone.getCreationTime()); + assertNull(zone.getName()); + assertNotNull(zone.getDnsName()); + assertNull(zone.getDescription()); + assertNull(zone.getNameServers()); + assertNull(zone.getNameServerSet()); + assertNull(zone.getId()); + options.put(DnsRpc.Option.FIELDS, "description"); + resetProjects(); + zone = RPC.create(ZONE1, options); + assertNull(zone.getCreationTime()); + assertNull(zone.getName()); + assertNull(zone.getDnsName()); + assertNotNull(zone.getDescription()); + assertNull(zone.getNameServers()); + assertNull(zone.getNameServerSet()); + assertNull(zone.getId()); + options.put(DnsRpc.Option.FIELDS, "nameServers"); + resetProjects(); + zone = RPC.create(ZONE1, options); + assertNull(zone.getCreationTime()); + assertNull(zone.getName()); + assertNull(zone.getDnsName()); + assertNull(zone.getDescription()); + assertNotNull(zone.getNameServers()); + assertNull(zone.getNameServerSet()); + assertNull(zone.getId()); + options.put(DnsRpc.Option.FIELDS, "nameServerSet"); + resetProjects(); + zone = RPC.create(ZONE1, options); + assertNull(zone.getCreationTime()); + assertNull(zone.getName()); + assertNull(zone.getDnsName()); + assertNull(zone.getDescription()); + assertNull(zone.getNameServers()); + assertNotNull(zone.getNameServerSet()); + assertNull(zone.getId()); + // several combined + options.put(DnsRpc.Option.FIELDS, "nameServerSet,description,id,name"); + resetProjects(); + zone = RPC.create(ZONE1, options); + assertNull(zone.getCreationTime()); + assertNotNull(zone.getName()); + assertNull(zone.getDnsName()); + assertNotNull(zone.getDescription()); + assertNull(zone.getNameServers()); + assertNotNull(zone.getNameServerSet()); + assertNotNull(zone.getId()); + } + @Test public void testCreateChange() { // non-existent zone LocalDnsHelper.Response response = - localDns.createChange(PROJECT_ID1, ZONE_NAME1, CHANGE1, null); + LOCAL_DNS_HELPER.createChange(PROJECT_ID1, ZONE_NAME1, CHANGE1, null); assertEquals(404, response.code()); - // existent zone - assertNotNull(localDns.createZone(PROJECT_ID1, ZONE1, null)); - assertNull(localDns.findChange(PROJECT_ID1, ZONE_NAME1, "1")); - response = localDns.createChange(PROJECT_ID1, ZONE_NAME1, CHANGE1, null); + assertNotNull(LOCAL_DNS_HELPER.createZone(PROJECT_ID1, ZONE1, null)); + assertNull(LOCAL_DNS_HELPER.findChange(PROJECT_ID1, ZONE_NAME1, "1")); + response = LOCAL_DNS_HELPER.createChange(PROJECT_ID1, ZONE_NAME1, CHANGE1, null); assertEquals(200, response.code()); - assertNotNull(localDns.findChange(PROJECT_ID1, ZONE_NAME1, "1")); + assertNotNull(LOCAL_DNS_HELPER.findChange(PROJECT_ID1, ZONE_NAME1, "1")); + } + + @Test + public void testCreateChangeUsingRpc() { + // non-existent zone + try { + RPC.applyChangeRequest(ZONE_NAME1, CHANGE1, EMPTY_RPC_OPTIONS); + } catch (DnsException ex) { + assertEquals(404, ex.code()); + } + // existent zone + RPC.create(ZONE1, EMPTY_RPC_OPTIONS); + assertNull(RPC.getChangeRequest(ZONE_NAME1, "1", EMPTY_RPC_OPTIONS)); + Change created = RPC.applyChangeRequest(ZONE1.getName(), CHANGE1, EMPTY_RPC_OPTIONS); + assertEquals(created, RPC.getChangeRequest(ZONE_NAME1, "1", EMPTY_RPC_OPTIONS)); + // field options + RPC.applyChangeRequest(ZONE1.getName(), CHANGE_KEEP, EMPTY_RPC_OPTIONS); + Map options = new HashMap<>(); + options.put(DnsRpc.Option.FIELDS, "additions"); + Change complex = RPC.applyChangeRequest(ZONE1.getName(), CHANGE_COMPLEX, options); + assertNotNull(complex.getAdditions()); + assertNull(complex.getDeletions()); + assertNull(complex.getId()); + assertNull(complex.getStartTime()); + assertNull(complex.getStatus()); + options.put(DnsRpc.Option.FIELDS, "deletions"); + complex = RPC.applyChangeRequest(ZONE1.getName(), CHANGE_COMPLEX, options); + assertNull(complex.getAdditions()); + assertNotNull(complex.getDeletions()); + assertNull(complex.getId()); + assertNull(complex.getStartTime()); + assertNull(complex.getStatus()); + options.put(DnsRpc.Option.FIELDS, "id"); + complex = RPC.applyChangeRequest(ZONE1.getName(), CHANGE_COMPLEX, options); + assertNull(complex.getAdditions()); + assertNull(complex.getDeletions()); + assertNotNull(complex.getId()); + assertNull(complex.getStartTime()); + assertNull(complex.getStatus()); + options.put(DnsRpc.Option.FIELDS, "startTime"); + complex = RPC.applyChangeRequest(ZONE1.getName(), CHANGE_COMPLEX, options); + assertNull(complex.getAdditions()); + assertNull(complex.getDeletions()); + assertNull(complex.getId()); + assertNotNull(complex.getStartTime()); + assertNull(complex.getStatus()); + options.put(DnsRpc.Option.FIELDS, "status"); + complex = RPC.applyChangeRequest(ZONE1.getName(), CHANGE_COMPLEX, options); + assertNull(complex.getAdditions()); + assertNull(complex.getDeletions()); + assertNull(complex.getId()); + assertNull(complex.getStartTime()); + assertNotNull(complex.getStatus()); } @Test public void testGetChange() { // existent - assertEquals(200, localDns.createZone(PROJECT_ID1, ZONE1, null).code()); - assertEquals(200, localDns.createChange(PROJECT_ID1, ZONE_NAME1, CHANGE1, null).code()); - assertEquals(200, localDns.getChange(PROJECT_ID1, ZONE_NAME1, "1", null).code()); + assertEquals(200, LOCAL_DNS_HELPER.createZone(PROJECT_ID1, ZONE1, null).code()); + assertEquals(200, LOCAL_DNS_HELPER.createChange(PROJECT_ID1, ZONE_NAME1, CHANGE1, null).code()); + assertEquals(200, LOCAL_DNS_HELPER.getChange(PROJECT_ID1, ZONE_NAME1, "1", null).code()); // non-existent - LocalDnsHelper.Response response = localDns.getChange(PROJECT_ID1, ZONE_NAME1, "2", null); + LocalDnsHelper.Response response = + LOCAL_DNS_HELPER.getChange(PROJECT_ID1, ZONE_NAME1, "2", null); assertEquals(404, response.code()); assertTrue(response.body().contains("parameters.changeId")); // non-existent zone - response = localDns.getChange(PROJECT_ID1, ZONE_NAME2, "1", null); + response = LOCAL_DNS_HELPER.getChange(PROJECT_ID1, ZONE_NAME2, "1", null); assertEquals(404, response.code()); assertTrue(response.body().contains("parameters.managedZone")); } + @Test + public void testGetChangeUsingRpc() { + // existent + RPC.create(ZONE1, EMPTY_RPC_OPTIONS); + Change created = RPC.applyChangeRequest(ZONE1.getName(), CHANGE1, EMPTY_RPC_OPTIONS); + Change retrieved = RPC.getChangeRequest(ZONE1.getName(), "1", EMPTY_RPC_OPTIONS); + assertEquals(created, retrieved); + // non-existent + assertNull(RPC.getChangeRequest(ZONE1.getName(), "2", EMPTY_RPC_OPTIONS)); + // non-existent zone + try { + RPC.getChangeRequest(ZONE_NAME2, "1", EMPTY_RPC_OPTIONS); + } catch (DnsException ex) { + // expected + assertEquals(404, ex.code()); + } + // field options + RPC.applyChangeRequest(ZONE1.getName(), CHANGE_KEEP, EMPTY_RPC_OPTIONS); + Change change = RPC.applyChangeRequest(ZONE1.getName(), CHANGE_COMPLEX, EMPTY_RPC_OPTIONS); + Map options = new HashMap<>(); + options.put(DnsRpc.Option.FIELDS, "additions"); + Change complex = RPC.getChangeRequest(ZONE1.getName(), change.getId(), options); + assertNotNull(complex.getAdditions()); + assertNull(complex.getDeletions()); + assertNull(complex.getId()); + assertNull(complex.getStartTime()); + assertNull(complex.getStatus()); + options.put(DnsRpc.Option.FIELDS, "deletions"); + complex = RPC.getChangeRequest(ZONE1.getName(), change.getId(), options); + assertNull(complex.getAdditions()); + assertNotNull(complex.getDeletions()); + assertNull(complex.getId()); + assertNull(complex.getStartTime()); + assertNull(complex.getStatus()); + options.put(DnsRpc.Option.FIELDS, "id"); + complex = RPC.getChangeRequest(ZONE1.getName(), change.getId(), options); + assertNull(complex.getAdditions()); + assertNull(complex.getDeletions()); + assertNotNull(complex.getId()); + assertNull(complex.getStartTime()); + assertNull(complex.getStatus()); + options.put(DnsRpc.Option.FIELDS, "startTime"); + complex = RPC.getChangeRequest(ZONE1.getName(), change.getId(), options); + assertNull(complex.getAdditions()); + assertNull(complex.getDeletions()); + assertNull(complex.getId()); + assertNotNull(complex.getStartTime()); + assertNull(complex.getStatus()); + options.put(DnsRpc.Option.FIELDS, "status"); + complex = RPC.getChangeRequest(ZONE1.getName(), change.getId(), options); + assertNull(complex.getAdditions()); + assertNull(complex.getDeletions()); + assertNull(complex.getId()); + assertNull(complex.getStartTime()); + assertNotNull(complex.getStatus()); + } + @Test public void testListZones() { // only interested in no exceptions and non-null response here @@ -302,36 +716,175 @@ public void testListZones() { optionsMap.put("fields", null); optionsMap.put("pageToken", null); optionsMap.put("maxResults", null); - LocalDnsHelper.Response response = localDns.listZones(PROJECT_ID1, optionsMap); + LocalDnsHelper.Response response = LOCAL_DNS_HELPER.listZones(PROJECT_ID1, optionsMap); assertEquals(200, response.code()); // some zones exists - localDns.createZone(PROJECT_ID1, ZONE1, null); - response = localDns.listZones(PROJECT_ID1, optionsMap); + LOCAL_DNS_HELPER.createZone(PROJECT_ID1, ZONE1, null); + response = LOCAL_DNS_HELPER.listZones(PROJECT_ID1, optionsMap); assertEquals(200, response.code()); - localDns.createZone(PROJECT_ID1, ZONE2, null); - response = localDns.listZones(PROJECT_ID1, optionsMap); + LOCAL_DNS_HELPER.createZone(PROJECT_ID1, ZONE2, null); + response = LOCAL_DNS_HELPER.listZones(PROJECT_ID1, optionsMap); assertEquals(200, response.code()); // error in options optionsMap.put("maxResults", "aaa"); - response = localDns.listZones(PROJECT_ID1, optionsMap); + response = LOCAL_DNS_HELPER.listZones(PROJECT_ID1, optionsMap); assertEquals(400, response.code()); optionsMap.put("maxResults", "0"); - response = localDns.listZones(PROJECT_ID1, optionsMap); + response = LOCAL_DNS_HELPER.listZones(PROJECT_ID1, optionsMap); assertEquals(400, response.code()); optionsMap.put("maxResults", "-1"); - response = localDns.listZones(PROJECT_ID1, optionsMap); + response = LOCAL_DNS_HELPER.listZones(PROJECT_ID1, optionsMap); assertEquals(400, response.code()); optionsMap.put("maxResults", "15"); - response = localDns.listZones(PROJECT_ID1, optionsMap); + response = LOCAL_DNS_HELPER.listZones(PROJECT_ID1, optionsMap); assertEquals(200, response.code()); optionsMap.put("dnsName", "aaa"); - response = localDns.listZones(PROJECT_ID1, optionsMap); + response = LOCAL_DNS_HELPER.listZones(PROJECT_ID1, optionsMap); assertEquals(400, response.code()); optionsMap.put("dnsName", "aaa."); - response = localDns.listZones(PROJECT_ID1, optionsMap); + response = LOCAL_DNS_HELPER.listZones(PROJECT_ID1, optionsMap); assertEquals(200, response.code()); } + @Test + public void testListZonesUsingRpc() { + Iterable results = RPC.listZones(EMPTY_RPC_OPTIONS).results(); + ImmutableList zones = ImmutableList.copyOf(results); + assertEquals(0, zones.size()); + // some zones exists + ManagedZone created = RPC.create(ZONE1, EMPTY_RPC_OPTIONS); + results = RPC.listZones(EMPTY_RPC_OPTIONS).results(); + zones = ImmutableList.copyOf(results); + assertEquals(created, zones.get(0)); + assertEquals(1, zones.size()); + created = RPC.create(ZONE2, EMPTY_RPC_OPTIONS); + results = RPC.listZones(EMPTY_RPC_OPTIONS).results(); + zones = ImmutableList.copyOf(results); + assertEquals(2, zones.size()); + assertTrue(zones.contains(created)); + // error in options + Map options = new HashMap<>(); + options.put(DnsRpc.Option.PAGE_SIZE, 0); + try { + RPC.listZones(options); + } catch (DnsException ex) { + // expected + assertEquals(400, ex.code()); + } + options = new HashMap<>(); + options.put(DnsRpc.Option.PAGE_SIZE, -1); + try { + RPC.listZones(options); + } catch (DnsException ex) { + // expected + assertEquals(400, ex.code()); + } + // ok size + options = new HashMap<>(); + options.put(DnsRpc.Option.PAGE_SIZE, 1); + results = RPC.listZones(options).results(); + zones = ImmutableList.copyOf(results); + assertEquals(1, zones.size()); + // dns name problems + options = new HashMap<>(); + options.put(DnsRpc.Option.DNS_NAME, "aaa"); + try { + RPC.listZones(options); + } catch (DnsException ex) { + // expected + assertEquals(400, ex.code()); + } + // ok name + options = new HashMap<>(); + options.put(DnsRpc.Option.DNS_NAME, "aaaa."); + results = RPC.listZones(options).results(); + zones = ImmutableList.copyOf(results); + assertEquals(0, zones.size()); + // field options + options = new HashMap<>(); + options.put(DnsRpc.Option.FIELDS, "managedZones(id)"); + ManagedZone zone = RPC.listZones(options).results().iterator().next(); + assertNull(zone.getCreationTime()); + assertNull(zone.getName()); + assertNull(zone.getDnsName()); + assertNull(zone.getDescription()); + assertNull(zone.getNameServers()); + assertNull(zone.getNameServerSet()); + assertNotNull(zone.getId()); + options.put(DnsRpc.Option.FIELDS, "managedZones(creationTime)"); + zone = RPC.listZones(options).results().iterator().next(); + assertNotNull(zone.getCreationTime()); + assertNull(zone.getName()); + assertNull(zone.getDnsName()); + assertNull(zone.getDescription()); + assertNull(zone.getNameServers()); + assertNull(zone.getNameServerSet()); + assertNull(zone.getId()); + options.put(DnsRpc.Option.FIELDS, "managedZones(dnsName)"); + zone = RPC.listZones(options).results().iterator().next(); + assertNull(zone.getCreationTime()); + assertNull(zone.getName()); + assertNotNull(zone.getDnsName()); + assertNull(zone.getDescription()); + assertNull(zone.getNameServers()); + assertNull(zone.getNameServerSet()); + assertNull(zone.getId()); + options.put(DnsRpc.Option.FIELDS, "managedZones(description)"); + zone = RPC.listZones(options).results().iterator().next(); + assertNull(zone.getCreationTime()); + assertNull(zone.getName()); + assertNull(zone.getDnsName()); + assertNotNull(zone.getDescription()); + assertNull(zone.getNameServers()); + assertNull(zone.getNameServerSet()); + assertNull(zone.getId()); + options.put(DnsRpc.Option.FIELDS, "managedZones(nameServers)"); + zone = RPC.listZones(options).results().iterator().next(); + assertNull(zone.getCreationTime()); + assertNull(zone.getName()); + assertNull(zone.getDnsName()); + assertNull(zone.getDescription()); + assertNotNull(zone.getNameServers()); + assertNull(zone.getNameServerSet()); + assertNull(zone.getId()); + options.put(DnsRpc.Option.FIELDS, "managedZones(nameServerSet)"); + DnsRpc.ListResult managedZoneListResult = RPC.listZones(options); + zone = managedZoneListResult.results().iterator().next(); + assertNull(managedZoneListResult.pageToken()); + assertNull(zone.getCreationTime()); + assertNull(zone.getName()); + assertNull(zone.getDnsName()); + assertNull(zone.getDescription()); + assertNull(zone.getNameServers()); + assertNotNull(zone.getNameServerSet()); + assertNull(zone.getId()); + // several combined + options.put(DnsRpc.Option.FIELDS, + "managedZones(nameServerSet,description,id,name),nextPageToken"); + options.put(DnsRpc.Option.PAGE_SIZE, 1); + managedZoneListResult = RPC.listZones(options); + zone = managedZoneListResult.results().iterator().next(); + assertNull(zone.getCreationTime()); + assertNotNull(zone.getName()); + assertNull(zone.getDnsName()); + assertNotNull(zone.getDescription()); + assertNull(zone.getNameServers()); + assertNotNull(zone.getNameServerSet()); + assertNotNull(zone.getId()); + assertEquals(zone.getName(), managedZoneListResult.pageToken()); + // paging + options = new HashMap<>(); + options.put(DnsRpc.Option.PAGE_SIZE, 1); + managedZoneListResult = RPC.listZones(options); + ImmutableList page1 = ImmutableList.copyOf(managedZoneListResult.results()); + assertEquals(1, page1.size()); + options.put(DnsRpc.Option.PAGE_TOKEN, managedZoneListResult.pageToken()); + managedZoneListResult = RPC.listZones(options); + ImmutableList page2 = ImmutableList.copyOf(managedZoneListResult.results()); + assertEquals(1, page2.size()); + assertNotEquals(page1.get(0), page2.get(0)); + } + @Test public void testListDnsRecords() { // only interested in no exceptions and non-null response here @@ -341,49 +894,196 @@ public void testListDnsRecords() { optionsMap.put("pageToken", null); optionsMap.put("maxResults", null); // no zone exists - LocalDnsHelper.Response response = localDns.listDnsRecords(PROJECT_ID1, ZONE_NAME1, + LocalDnsHelper.Response response = LOCAL_DNS_HELPER.listDnsRecords(PROJECT_ID1, ZONE_NAME1, optionsMap); assertEquals(404, response.code()); // zone exists but has no records - localDns.createZone(PROJECT_ID1, ZONE1, null); - localDns.listDnsRecords(PROJECT_ID1, ZONE_NAME1, optionsMap); + LOCAL_DNS_HELPER.createZone(PROJECT_ID1, ZONE1, null); + LOCAL_DNS_HELPER.listDnsRecords(PROJECT_ID1, ZONE_NAME1, optionsMap); // zone has records - localDns.createChange(PROJECT_ID1, ZONE_NAME1, CHANGE1, null); - response = localDns.listDnsRecords(PROJECT_ID1, ZONE_NAME1, optionsMap); + LOCAL_DNS_HELPER.createChange(PROJECT_ID1, ZONE_NAME1, CHANGE1, null); + response = LOCAL_DNS_HELPER.listDnsRecords(PROJECT_ID1, ZONE_NAME1, optionsMap); assertEquals(200, response.code()); // error in options optionsMap.put("maxResults", "aaa"); - response = localDns.listZones(PROJECT_ID1, optionsMap); + response = LOCAL_DNS_HELPER.listZones(PROJECT_ID1, optionsMap); assertEquals(400, response.code()); optionsMap.put("maxResults", "0"); - response = localDns.listZones(PROJECT_ID1, optionsMap); + response = LOCAL_DNS_HELPER.listZones(PROJECT_ID1, optionsMap); assertEquals(400, response.code()); optionsMap.put("maxResults", "-1"); - response = localDns.listZones(PROJECT_ID1, optionsMap); + response = LOCAL_DNS_HELPER.listZones(PROJECT_ID1, optionsMap); assertEquals(400, response.code()); optionsMap.put("maxResults", "15"); - response = localDns.listZones(PROJECT_ID1, optionsMap); + response = LOCAL_DNS_HELPER.listZones(PROJECT_ID1, optionsMap); assertEquals(200, response.code()); optionsMap.put("name", "aaa"); - response = localDns.listZones(PROJECT_ID1, optionsMap); + response = LOCAL_DNS_HELPER.listZones(PROJECT_ID1, optionsMap); assertEquals(400, response.code()); optionsMap.put("name", "aaa."); - response = localDns.listZones(PROJECT_ID1, optionsMap); + response = LOCAL_DNS_HELPER.listZones(PROJECT_ID1, optionsMap); assertEquals(200, response.code()); optionsMap.put("name", null); optionsMap.put("type", "A"); - response = localDns.listZones(PROJECT_ID1, optionsMap); + response = LOCAL_DNS_HELPER.listZones(PROJECT_ID1, optionsMap); assertEquals(400, response.code()); optionsMap.put("name", "aaa."); optionsMap.put("type", "a"); - response = localDns.listZones(PROJECT_ID1, optionsMap); + response = LOCAL_DNS_HELPER.listZones(PROJECT_ID1, optionsMap); assertEquals(400, response.code()); optionsMap.put("name", "aaaa."); optionsMap.put("type", "A"); - response = localDns.listZones(PROJECT_ID1, optionsMap); + response = LOCAL_DNS_HELPER.listZones(PROJECT_ID1, optionsMap); assertEquals(200, response.code()); } + @Test + public void testListDnsRecordsUsingRpc() { + // no zone exists + try { + RPC.listDnsRecords(ZONE_NAME1, EMPTY_RPC_OPTIONS); + } catch (DnsException ex) { + // expected + assertEquals(404, ex.code()); + } + // zone exists but has no records + RPC.create(ZONE1, EMPTY_RPC_OPTIONS); + Iterable results = + RPC.listDnsRecords(ZONE_NAME1, EMPTY_RPC_OPTIONS).results(); + ImmutableList records = ImmutableList.copyOf(results); + assertEquals(2, records.size()); // contains default NS and SOA + // zone has records + RPC.applyChangeRequest(ZONE_NAME1, CHANGE_KEEP, EMPTY_RPC_OPTIONS); + results = RPC.listDnsRecords(ZONE_NAME1, EMPTY_RPC_OPTIONS).results(); + records = ImmutableList.copyOf(results); + assertEquals(3, records.size()); + // error in options + Map options = new HashMap<>(); + options.put(DnsRpc.Option.PAGE_SIZE, 0); + try { + RPC.listDnsRecords(ZONE1.getName(), options); + } catch (DnsException ex) { + // expected + assertEquals(400, ex.code()); + } + options.put(DnsRpc.Option.PAGE_SIZE, -1); + try { + RPC.listDnsRecords(ZONE1.getName(), options); + } catch (DnsException ex) { + // expected + assertEquals(400, ex.code()); + } + options.put(DnsRpc.Option.PAGE_SIZE, 1); + results = RPC.listDnsRecords(ZONE1.getName(), options).results(); + records = ImmutableList.copyOf(results); + assertEquals(1, records.size()); + options.put(DnsRpc.Option.PAGE_SIZE, 15); + results = RPC.listDnsRecords(ZONE1.getName(), options).results(); + records = ImmutableList.copyOf(results); + assertEquals(3, records.size()); + + // dnsName filter + options = new HashMap<>(); + options.put(DnsRpc.Option.NAME, "aaa"); + try { + RPC.listDnsRecords(ZONE1.getName(), options); + } catch (DnsException ex) { + // expected + assertEquals(400, ex.code()); + } + options.put(DnsRpc.Option.NAME, "aaa."); + results = RPC.listDnsRecords(ZONE1.getName(), options).results(); + records = ImmutableList.copyOf(results); + assertEquals(0, records.size()); + options.put(DnsRpc.Option.NAME, null); + options.put(DnsRpc.Option.DNS_TYPE, "A"); + try { + RPC.listDnsRecords(ZONE1.getName(), options); + } catch (DnsException ex) { + // expected + assertEquals(400, ex.code()); + } + options.put(DnsRpc.Option.NAME, "aaa."); + options.put(DnsRpc.Option.DNS_TYPE, "a"); + try { + RPC.listDnsRecords(ZONE1.getName(), options); + } catch (DnsException ex) { + // expected + assertEquals(400, ex.code()); + } + options.put(DnsRpc.Option.NAME, DNS_NAME); + options.put(DnsRpc.Option.DNS_TYPE, "SOA"); + results = RPC.listDnsRecords(ZONE1.getName(), options).results(); + records = ImmutableList.copyOf(results); + assertEquals(1, records.size()); + // field options + options = new HashMap<>(); + options.put(DnsRpc.Option.FIELDS, "rrsets(name)"); + DnsRpc.ListResult resourceRecordSetListResult = + RPC.listDnsRecords(ZONE1.getName(), options); + records = ImmutableList.copyOf(resourceRecordSetListResult.results()); + ResourceRecordSet record = records.get(0); + assertNotNull(record.getName()); + assertNull(record.getRrdatas()); + assertNull(record.getType()); + assertNull(record.getTtl()); + assertNull(resourceRecordSetListResult.pageToken()); + options.put(DnsRpc.Option.FIELDS, "rrsets(rrdatas)"); + resourceRecordSetListResult = RPC.listDnsRecords(ZONE1.getName(), options); + records = ImmutableList.copyOf(resourceRecordSetListResult.results()); + record = records.get(0); + assertNull(record.getName()); + assertNotNull(record.getRrdatas()); + assertNull(record.getType()); + assertNull(record.getTtl()); + assertNull(resourceRecordSetListResult.pageToken()); + options.put(DnsRpc.Option.FIELDS, "rrsets(ttl)"); + resourceRecordSetListResult = RPC.listDnsRecords(ZONE1.getName(), options); + records = ImmutableList.copyOf(resourceRecordSetListResult.results()); + record = records.get(0); + assertNull(record.getName()); + assertNull(record.getRrdatas()); + assertNull(record.getType()); + assertNotNull(record.getTtl()); + assertNull(resourceRecordSetListResult.pageToken()); + options.put(DnsRpc.Option.FIELDS, "rrsets(type)"); + resourceRecordSetListResult = RPC.listDnsRecords(ZONE1.getName(), options); + records = ImmutableList.copyOf(resourceRecordSetListResult.results()); + record = records.get(0); + assertNull(record.getName()); + assertNull(record.getRrdatas()); + assertNotNull(record.getType()); + assertNull(record.getTtl()); + assertNull(resourceRecordSetListResult.pageToken()); + options.put(DnsRpc.Option.FIELDS, "nextPageToken"); + resourceRecordSetListResult = RPC.listDnsRecords(ZONE1.getName(), options); + records = ImmutableList.copyOf(resourceRecordSetListResult.results()); + record = records.get(0); + assertNull(record.getName()); + assertNull(record.getRrdatas()); + assertNull(record.getType()); + assertNull(record.getTtl()); + assertNull(resourceRecordSetListResult.pageToken()); + options.put(DnsRpc.Option.FIELDS, "nextPageToken,rrsets(name,rrdatas)"); + options.put(DnsRpc.Option.PAGE_SIZE, 1); + resourceRecordSetListResult = RPC.listDnsRecords(ZONE1.getName(), options); + records = ImmutableList.copyOf(resourceRecordSetListResult.results()); + assertEquals(1, records.size()); + record = records.get(0); + assertNotNull(record.getName()); + assertNotNull(record.getRrdatas()); + assertNull(record.getType()); + assertNull(record.getTtl()); + assertNotNull(resourceRecordSetListResult.pageToken()); + // paging + options.put(DnsRpc.Option.PAGE_TOKEN, resourceRecordSetListResult.pageToken()); + resourceRecordSetListResult = RPC.listDnsRecords(ZONE1.getName(), options); + records = ImmutableList.copyOf(resourceRecordSetListResult.results()); + assertEquals(1, records.size()); + ResourceRecordSet nextRecord = records.get(0); + assertNotEquals(record, nextRecord); + } + @Test public void testListChanges() { optionsMap.put("sortBy", null); @@ -392,72 +1092,214 @@ public void testListChanges() { optionsMap.put("pageToken", null); optionsMap.put("maxResults", null); // no such zone exists - LocalDnsHelper.Response response = localDns.listDnsRecords(PROJECT_ID1, ZONE_NAME1, optionsMap); + LocalDnsHelper.Response response = + LOCAL_DNS_HELPER.listChanges(PROJECT_ID1, ZONE_NAME1, optionsMap); assertEquals(404, response.code()); assertTrue(response.body().contains("managedZone")); // zone exists but has no changes - localDns.createZone(PROJECT_ID1, ZONE1, null); - assertNotNull(localDns.listChanges(PROJECT_ID1, ZONE_NAME1, optionsMap)); + LOCAL_DNS_HELPER.createZone(PROJECT_ID1, ZONE1, null); + assertNotNull(LOCAL_DNS_HELPER.listChanges(PROJECT_ID1, ZONE_NAME1, optionsMap)); // zone has changes - localDns.createChange(PROJECT_ID1, ZONE_NAME1, CHANGE1, null); - assertNotNull(localDns.listChanges(PROJECT_ID1, ZONE_NAME1, optionsMap)); - localDns.createChange(PROJECT_ID1, ZONE_NAME1, CHANGE1, null); - localDns.createChange(PROJECT_ID1, ZONE_NAME1, CHANGE2, null); - localDns.createChange(PROJECT_ID1, ZONE_NAME1, CHANGE2, null); - assertNotNull(localDns.listChanges(PROJECT_ID1, ZONE_NAME1, optionsMap)); + LOCAL_DNS_HELPER.createChange(PROJECT_ID1, ZONE_NAME1, CHANGE1, null); + assertNotNull(LOCAL_DNS_HELPER.listChanges(PROJECT_ID1, ZONE_NAME1, optionsMap)); + LOCAL_DNS_HELPER.createChange(PROJECT_ID1, ZONE_NAME1, CHANGE1, null); + LOCAL_DNS_HELPER.createChange(PROJECT_ID1, ZONE_NAME1, CHANGE2, null); + LOCAL_DNS_HELPER.createChange(PROJECT_ID1, ZONE_NAME1, CHANGE2, null); + assertNotNull(LOCAL_DNS_HELPER.listChanges(PROJECT_ID1, ZONE_NAME1, optionsMap)); // error in options optionsMap.put("maxResults", "aaa"); - response = localDns.listChanges(PROJECT_ID1, ZONE_NAME1, optionsMap); + response = LOCAL_DNS_HELPER.listChanges(PROJECT_ID1, ZONE_NAME1, optionsMap); assertEquals(400, response.code()); optionsMap.put("maxResults", "0"); - response = localDns.listChanges(PROJECT_ID1, ZONE_NAME1, optionsMap); + response = LOCAL_DNS_HELPER.listChanges(PROJECT_ID1, ZONE_NAME1, optionsMap); assertEquals(400, response.code()); optionsMap.put("maxResults", "-1"); - response = localDns.listChanges(PROJECT_ID1, ZONE_NAME1, optionsMap); + response = LOCAL_DNS_HELPER.listChanges(PROJECT_ID1, ZONE_NAME1, optionsMap); assertEquals(400, response.code()); optionsMap.put("maxResults", "15"); - response = localDns.listChanges(PROJECT_ID1, ZONE_NAME1, optionsMap); - assertEquals(200, response.code()); - optionsMap.put("dnsName", "aaa"); - response = localDns.listChanges(PROJECT_ID1, ZONE_NAME1, optionsMap); - assertEquals(400, response.code()); - optionsMap.put("dnsName", "aaa."); - response = localDns.listChanges(PROJECT_ID1, ZONE_NAME1, optionsMap); + response = LOCAL_DNS_HELPER.listChanges(PROJECT_ID1, ZONE_NAME1, optionsMap); assertEquals(200, response.code()); optionsMap.put("sortBy", "changeSequence"); - response = localDns.listChanges(PROJECT_ID1, ZONE_NAME1, optionsMap); + response = LOCAL_DNS_HELPER.listChanges(PROJECT_ID1, ZONE_NAME1, optionsMap); assertEquals(200, response.code()); optionsMap.put("sortBy", "something else"); - response = localDns.listChanges(PROJECT_ID1, ZONE_NAME1, optionsMap); + response = LOCAL_DNS_HELPER.listChanges(PROJECT_ID1, ZONE_NAME1, optionsMap); assertEquals(400, response.code()); assertTrue(response.body().contains("Allowed values: [changesequence]")); optionsMap.put("sortBy", "ChAnGeSeQuEnCe"); // is not case sensitive - response = localDns.listChanges(PROJECT_ID1, ZONE_NAME1, optionsMap); + response = LOCAL_DNS_HELPER.listChanges(PROJECT_ID1, ZONE_NAME1, optionsMap); assertEquals(200, response.code()); optionsMap.put("sortOrder", "ascending"); - response = localDns.listChanges(PROJECT_ID1, ZONE_NAME1, optionsMap); + response = LOCAL_DNS_HELPER.listChanges(PROJECT_ID1, ZONE_NAME1, optionsMap); assertEquals(200, response.code()); optionsMap.put("sortBy", null); optionsMap.put("sortOrder", "descending"); - response = localDns.listChanges(PROJECT_ID1, ZONE_NAME1, optionsMap); + response = LOCAL_DNS_HELPER.listChanges(PROJECT_ID1, ZONE_NAME1, optionsMap); assertEquals(200, response.code()); optionsMap.put("sortOrder", "somethingelse"); - response = localDns.listChanges(PROJECT_ID1, ZONE_NAME1, optionsMap); + response = LOCAL_DNS_HELPER.listChanges(PROJECT_ID1, ZONE_NAME1, optionsMap); assertEquals(400, response.code()); assertTrue(response.body().contains("parameters.sortOrder")); } + @Test + public void testListChangesUsingRpc() { + // no such zone exists + try { + RPC.listChangeRequests(ZONE_NAME1, EMPTY_RPC_OPTIONS); + } catch (DnsException ex) { + // expected + assertEquals(404, ex.code()); + } + // zone exists but has no changes + RPC.create(ZONE1, EMPTY_RPC_OPTIONS); + Iterable results = RPC.listChangeRequests(ZONE1.getName(), EMPTY_RPC_OPTIONS).results(); + ImmutableList changes = ImmutableList.copyOf(results); + assertEquals(0, changes.size()); + // zone has changes + RPC.applyChangeRequest(ZONE1.getName(), CHANGE1, EMPTY_RPC_OPTIONS); + RPC.applyChangeRequest(ZONE1.getName(), CHANGE2, EMPTY_RPC_OPTIONS); + RPC.applyChangeRequest(ZONE1.getName(), CHANGE_KEEP, EMPTY_RPC_OPTIONS); + results = RPC.listChangeRequests(ZONE1.getName(), EMPTY_RPC_OPTIONS).results(); + changes = ImmutableList.copyOf(results); + assertEquals(3, changes.size()); + // error in options + Map options = new HashMap<>(); + options.put(DnsRpc.Option.PAGE_SIZE, 0); + try { + RPC.listChangeRequests(ZONE1.getName(), options); + } catch (DnsException ex) { + // expected + assertEquals(400, ex.code()); + } + options.put(DnsRpc.Option.PAGE_SIZE, -1); + try { + RPC.listChangeRequests(ZONE1.getName(), options); + } catch (DnsException ex) { + // expected + assertEquals(400, ex.code()); + } + options.put(DnsRpc.Option.PAGE_SIZE, 15); + try { + RPC.listChangeRequests(ZONE1.getName(), options); + } catch (DnsException ex) { + // expected + assertEquals(400, ex.code()); + } + options = new HashMap<>(); + options.put(DnsRpc.Option.SORTING_ORDER, "descending"); + results = RPC.listChangeRequests(ZONE1.getName(), options).results(); + ImmutableList descending = ImmutableList.copyOf(results); + results = RPC.listChangeRequests(ZONE1.getName(), EMPTY_RPC_OPTIONS).results(); + ImmutableList ascending = ImmutableList.copyOf(results); + int size = 3; + assertEquals(size, descending.size()); + for (int i = 0; i < size; i++) { + assertEquals(descending.get(i), ascending.get(size - i - 1)); + } + options.put(DnsRpc.Option.SORTING_ORDER, "something else"); + try { + RPC.listChangeRequests(ZONE1.getName(), options); + } catch (DnsException ex) { + // expected + assertEquals(400, ex.code()); + } + // field options + RPC.applyChangeRequest(ZONE1.getName(), CHANGE_COMPLEX, EMPTY_RPC_OPTIONS); + options = new HashMap<>(); + options.put(DnsRpc.Option.SORTING_ORDER, "descending"); + options.put(DnsRpc.Option.FIELDS, "changes(additions)"); + DnsRpc.ListResult changeListResult = + RPC.listChangeRequests(ZONE1.getName(), options); + changes = ImmutableList.copyOf(changeListResult.results()); + Change complex = changes.get(0); + assertNotNull(complex.getAdditions()); + assertNull(complex.getDeletions()); + assertNull(complex.getId()); + assertNull(complex.getStartTime()); + assertNull(complex.getStatus()); + assertNull(changeListResult.pageToken()); + options.put(DnsRpc.Option.FIELDS, "changes(deletions)"); + changeListResult = RPC.listChangeRequests(ZONE1.getName(), options); + changes = ImmutableList.copyOf(changeListResult.results()); + complex = changes.get(0); + assertNull(complex.getAdditions()); + assertNotNull(complex.getDeletions()); + assertNull(complex.getId()); + assertNull(complex.getStartTime()); + assertNull(complex.getStatus()); + assertNull(changeListResult.pageToken()); + options.put(DnsRpc.Option.FIELDS, "changes(id)"); + changeListResult = RPC.listChangeRequests(ZONE1.getName(), options); + changes = ImmutableList.copyOf(changeListResult.results()); + complex = changes.get(0); + assertNull(complex.getAdditions()); + assertNull(complex.getDeletions()); + assertNotNull(complex.getId()); + assertNull(complex.getStartTime()); + assertNull(complex.getStatus()); + assertNull(changeListResult.pageToken()); + options.put(DnsRpc.Option.FIELDS, "changes(startTime)"); + changeListResult = RPC.listChangeRequests(ZONE1.getName(), options); + changes = ImmutableList.copyOf(changeListResult.results()); + complex = changes.get(0); + assertNull(complex.getAdditions()); + assertNull(complex.getDeletions()); + assertNull(complex.getId()); + assertNotNull(complex.getStartTime()); + assertNull(complex.getStatus()); + assertNull(changeListResult.pageToken()); + options.put(DnsRpc.Option.FIELDS, "changes(status)"); + changeListResult = RPC.listChangeRequests(ZONE1.getName(), options); + changes = ImmutableList.copyOf(changeListResult.results()); + complex = changes.get(0); + assertNull(complex.getAdditions()); + assertNull(complex.getDeletions()); + assertNull(complex.getId()); + assertNull(complex.getStartTime()); + assertNotNull(complex.getStatus()); + assertNull(changeListResult.pageToken()); + options.put(DnsRpc.Option.FIELDS, "nextPageToken"); + options.put(DnsRpc.Option.PAGE_SIZE, 1); + changeListResult = RPC.listChangeRequests(ZONE1.getName(), options); + changes = ImmutableList.copyOf(changeListResult.results()); + complex = changes.get(0); + assertNull(complex.getAdditions()); + assertNull(complex.getDeletions()); + assertNull(complex.getId()); + assertNull(complex.getStartTime()); + assertNull(complex.getStatus()); + assertNotNull(changeListResult.pageToken()); + // paging + options.put(DnsRpc.Option.FIELDS, "nextPageToken,changes(id)"); + options.put(DnsRpc.Option.PAGE_SIZE, 1); + changeListResult = RPC.listChangeRequests(ZONE1.getName(), options); + changes = ImmutableList.copyOf(changeListResult.results()); + assertEquals(1, changes.size()); + final Change first = changes.get(0); + assertNotNull(changeListResult.pageToken()); + options.put(DnsRpc.Option.PAGE_TOKEN, changeListResult.pageToken()); + changeListResult = RPC.listChangeRequests(ZONE1.getName(), options); + changes = ImmutableList.copyOf(changeListResult.results()); + assertEquals(1, changes.size()); + Change second = changes.get(0); + assertNotEquals(first, second); + } + @Test public void testToListResponse() { LocalDnsHelper.Response response = LocalDnsHelper.toListResponse( - Lists.newArrayList("some", "multiple", "words"), "IncludeThisPageToken", true); + Lists.newArrayList("some", "multiple", "words"), "contextA", "IncludeThisPageToken", true); assertTrue(response.body().contains("IncludeThisPageToken")); + assertTrue(response.body().contains("contextA")); response = LocalDnsHelper.toListResponse( - Lists.newArrayList("some", "multiple", "words"), "IncludeThisPageToken", false); + Lists.newArrayList("some", "multiple", "words"), "contextB", "IncludeThisPageToken", false); assertFalse(response.body().contains("IncludeThisPageToken")); + assertTrue(response.body().contains("contextB")); response = LocalDnsHelper.toListResponse( - Lists.newArrayList("some", "multiple", "words"), null, true); + Lists.newArrayList("some", "multiple", "words"), "contextC", null, true); assertFalse(response.body().contains("pageToken")); + assertTrue(response.body().contains("contextC")); } @Test @@ -511,45 +1353,45 @@ public void testCreateZoneValidatesZone() { // no name ManagedZone copy = copyZone(minimalZone); copy.setName(null); - LocalDnsHelper.Response response = localDns.createZone(PROJECT_ID1, copy, null); + LocalDnsHelper.Response response = LOCAL_DNS_HELPER.createZone(PROJECT_ID1, copy, null); assertEquals(400, response.code()); assertTrue(response.body().contains("entity.managedZone.name")); // no description copy = copyZone(minimalZone); copy.setDescription(null); - response = localDns.createZone(PROJECT_ID1, copy, null); + response = LOCAL_DNS_HELPER.createZone(PROJECT_ID1, copy, null); assertEquals(400, response.code()); assertTrue(response.body().contains("entity.managedZone.description")); // no dns name copy = copyZone(minimalZone); copy.setDnsName(null); - response = localDns.createZone(PROJECT_ID1, copy, null); + response = LOCAL_DNS_HELPER.createZone(PROJECT_ID1, copy, null); assertEquals(400, response.code()); assertTrue(response.body().contains("entity.managedZone.dnsName")); // zone name is a number copy = copyZone(minimalZone); copy.setName("123456"); - response = localDns.createZone(PROJECT_ID1, copy, null); + response = LOCAL_DNS_HELPER.createZone(PROJECT_ID1, copy, null); assertEquals(400, response.code()); assertTrue(response.body().contains("entity.managedZone.name")); assertTrue(response.body().contains("Invalid")); // dns name does not end with period copy = copyZone(minimalZone); copy.setDnsName("aaaaaa.com"); - response = localDns.createZone(PROJECT_ID1, copy, null); + response = LOCAL_DNS_HELPER.createZone(PROJECT_ID1, copy, null); assertEquals(400, response.code()); assertTrue(response.body().contains("entity.managedZone.dnsName")); assertTrue(response.body().contains("Invalid")); // dns name is reserved copy = copyZone(minimalZone); copy.setDnsName("com."); - response = localDns.createZone(PROJECT_ID1, copy, null); + response = LOCAL_DNS_HELPER.createZone(PROJECT_ID1, copy, null); assertEquals(400, response.code()); assertTrue(response.body().contains("not available to be created.")); // empty description should pass copy = copyZone(minimalZone); copy.setDescription(""); - response = localDns.createZone(PROJECT_ID1, copy, null); + response = LOCAL_DNS_HELPER.createZone(PROJECT_ID1, copy, null); assertEquals(200, response.code()); } @@ -632,10 +1474,10 @@ public void testCheckRrset() { valid.setTtl(500); Change validChange = new Change(); validChange.setAdditions(ImmutableList.of(valid)); - localDns.createZone(PROJECT_ID1, ZONE1, null); - localDns.createChange(PROJECT_ID1, ZONE_NAME1, validChange, null); + LOCAL_DNS_HELPER.createZone(PROJECT_ID1, ZONE1, null); + LOCAL_DNS_HELPER.createChange(PROJECT_ID1, ZONE_NAME1, validChange, null); // delete with field mismatch - LocalDnsHelper.ZoneContainer zone = localDns.findZone(PROJECT_ID1, ZONE_NAME1); + LocalDnsHelper.ZoneContainer zone = LOCAL_DNS_HELPER.findZone(PROJECT_ID1, ZONE_NAME1); valid.setTtl(valid.getTtl() + 20); LocalDnsHelper.Response response = LocalDnsHelper.checkRrset(valid, zone, 0, "deletions"); assertEquals(412, response.code()); @@ -735,7 +1577,7 @@ public void testCheckChange() { assertTrue(response.body().contains("additions[0].type")); validA.setType("A"); // null rrdata - List temp = validA.getRrdatas(); // preserve + final List temp = validA.getRrdatas(); // preserve validA.setRrdatas(null); response = LocalDnsHelper.checkChange(validChange, zoneContainer); assertEquals(400, response.code()); @@ -762,9 +1604,9 @@ public void testAdditionsMeetDeletions() { validA.setRrdatas(ImmutableList.of("0.255.1.5")); Change validChange = new Change(); validChange.setAdditions(ImmutableList.of(validA)); - localDns.createZone(PROJECT_ID1, ZONE1, null); - localDns.createChange(PROJECT_ID1, ZONE_NAME1, validChange, null); - LocalDnsHelper.ZoneContainer container = localDns.findZone(PROJECT_ID1, ZONE_NAME1); + LOCAL_DNS_HELPER.createZone(PROJECT_ID1, ZONE1, null); + LOCAL_DNS_HELPER.createChange(PROJECT_ID1, ZONE_NAME1, validChange, null); + LocalDnsHelper.ZoneContainer container = LOCAL_DNS_HELPER.findZone(PROJECT_ID1, ZONE_NAME1); LocalDnsHelper.Response response = LocalDnsHelper.additionsMeetDeletions(ImmutableList.of(validA), null, container); assertEquals(409, response.code()); @@ -780,23 +1622,23 @@ public void testCreateChangeValidatesChangeContent() { validA.setRrdatas(ImmutableList.of("0.255.1.5")); Change validChange = new Change(); validChange.setAdditions(ImmutableList.of(validA)); - localDns.createZone(PROJECT_ID1, ZONE1, null); - localDns.createChange(PROJECT_ID1, ZONE_NAME1, validChange, null); + LOCAL_DNS_HELPER.createZone(PROJECT_ID1, ZONE1, null); + LOCAL_DNS_HELPER.createChange(PROJECT_ID1, ZONE_NAME1, validChange, null); LocalDnsHelper.Response response = - localDns.createChange(PROJECT_ID1, ZONE_NAME1, validChange, null); + LOCAL_DNS_HELPER.createChange(PROJECT_ID1, ZONE_NAME1, validChange, null); assertEquals(409, response.code()); assertTrue(response.body().contains("already exists")); // delete with field mismatch Change delete = new Change(); validA.setTtl(20); // mismatch delete.setDeletions(ImmutableList.of(validA)); - response = localDns.createChange(PROJECT_ID1, ZONE_NAME1, delete, null); + response = LOCAL_DNS_HELPER.createChange(PROJECT_ID1, ZONE_NAME1, delete, null); assertEquals(412, response.code()); assertTrue(response.body().contains("entity.change.deletions[0]")); // delete and add SOA Change addition = new Change(); ImmutableList rrsetWrappers - = localDns.findZone(PROJECT_ID1, ZONE_NAME1).dnsRecords().get(ZONE_NAME1); + = LOCAL_DNS_HELPER.findZone(PROJECT_ID1, ZONE_NAME1).dnsRecords().get(ZONE_NAME1); LinkedList deletions = new LinkedList<>(); LinkedList additions = new LinkedList<>(); for (LocalDnsHelper.RrsetWrapper wrapper : rrsetWrappers) { @@ -811,12 +1653,12 @@ public void testCreateChangeValidatesChangeContent() { } delete.setDeletions(deletions); addition.setAdditions(additions); - response = localDns.createChange(PROJECT_ID1, ZONE_NAME1, delete, null); + response = LOCAL_DNS_HELPER.createChange(PROJECT_ID1, ZONE_NAME1, delete, null); assertEquals(400, response.code()); assertTrue(response.body().contains( "zone must contain exactly one resource record set of type 'SOA' at the apex")); assertTrue(response.body().contains("deletions[0]")); - response = localDns.createChange(PROJECT_ID1, ZONE_NAME1, addition, null); + response = LOCAL_DNS_HELPER.createChange(PROJECT_ID1, ZONE_NAME1, addition, null); assertEquals(400, response.code()); assertTrue(response.body().contains( "zone must contain exactly one resource record set of type 'SOA' at the apex")); @@ -836,24 +1678,24 @@ public void testCreateChangeValidatesChangeContent() { } delete.setDeletions(deletions); addition.setAdditions(additions); - response = localDns.createChange(PROJECT_ID1, ZONE_NAME1, delete, null); + response = LOCAL_DNS_HELPER.createChange(PROJECT_ID1, ZONE_NAME1, delete, null); assertEquals(400, response.code()); assertTrue(response.body().contains( "zone must contain exactly one resource record set of type 'NS' at the apex")); - response = localDns.createChange(PROJECT_ID1, ZONE_NAME1, addition, null); + response = LOCAL_DNS_HELPER.createChange(PROJECT_ID1, ZONE_NAME1, addition, null); assertEquals(400, response.code()); assertTrue(response.body().contains( "zone must contain exactly one resource record set of type 'NS' at the apex")); assertTrue(response.body().contains("additions[0]")); // change (delete + add) addition.setDeletions(deletions); - response = localDns.createChange(PROJECT_ID1, ZONE_NAME1, addition, null); + response = LOCAL_DNS_HELPER.createChange(PROJECT_ID1, ZONE_NAME1, addition, null); assertEquals(200, response.code()); } @Test public void testCreateChangeValidatesChange() { - localDns.createZone(PROJECT_ID1, ZONE1, null); + LOCAL_DNS_HELPER.createZone(PROJECT_ID1, ZONE1, null); ResourceRecordSet validA = new ResourceRecordSet(); validA.setName(ZONE1.getDnsName()); validA.setType("A"); @@ -867,52 +1709,52 @@ public void testCreateChangeValidatesChange() { Change invalidChange = new Change(); invalidChange.setAdditions(ImmutableList.of(invalidA)); LocalDnsHelper.Response response = - localDns.createChange(PROJECT_ID1, ZONE_NAME1, validChange, null); + LOCAL_DNS_HELPER.createChange(PROJECT_ID1, ZONE_NAME1, validChange, null); assertEquals(200, response.code()); - response = localDns.createChange(PROJECT_ID1, ZONE_NAME1, invalidChange, null); + response = LOCAL_DNS_HELPER.createChange(PROJECT_ID1, ZONE_NAME1, invalidChange, null); assertEquals(400, response.code()); // only empty additions/deletions Change empty = new Change(); empty.setAdditions(ImmutableList.of()); empty.setDeletions(ImmutableList.of()); - response = localDns.createChange(PROJECT_ID1, ZONE_NAME1, empty, null); + response = LOCAL_DNS_HELPER.createChange(PROJECT_ID1, ZONE_NAME1, empty, null); assertEquals(400, response.code()); assertTrue(response.body().contains( "The 'entity.change' parameter is required but was missing.")); // non-matching name validA.setName(ZONE1.getDnsName() + ".aaa."); - response = localDns.createChange(PROJECT_ID1, ZONE_NAME1, validChange, null); + response = LOCAL_DNS_HELPER.createChange(PROJECT_ID1, ZONE_NAME1, validChange, null); assertEquals(400, response.code()); assertTrue(response.body().contains("additions[0].name")); // wrong type validA.setName(ZONE1.getDnsName()); // revert validA.setType("ABCD"); - response = localDns.createChange(PROJECT_ID1, ZONE_NAME1, validChange, null); + response = LOCAL_DNS_HELPER.createChange(PROJECT_ID1, ZONE_NAME1, validChange, null); assertEquals(400, response.code()); assertTrue(response.body().contains("additions[0].type")); // wrong ttl validA.setType("A"); // revert validA.setTtl(-1); - response = localDns.createChange(PROJECT_ID1, ZONE_NAME1, validChange, null); + response = LOCAL_DNS_HELPER.createChange(PROJECT_ID1, ZONE_NAME1, validChange, null); assertEquals(400, response.code()); assertTrue(response.body().contains("additions[0].ttl")); validA.setTtl(null); // revert // null name validA.setName(null); - response = localDns.createChange(PROJECT_ID1, ZONE_NAME1, validChange, null); + response = LOCAL_DNS_HELPER.createChange(PROJECT_ID1, ZONE_NAME1, validChange, null); assertEquals(400, response.code()); assertTrue(response.body().contains("additions[0].name")); validA.setName(ZONE1.getDnsName()); // null type validA.setType(null); - response = localDns.createChange(PROJECT_ID1, ZONE_NAME1, validChange, null); + response = LOCAL_DNS_HELPER.createChange(PROJECT_ID1, ZONE_NAME1, validChange, null); assertEquals(400, response.code()); assertTrue(response.body().contains("additions[0].type")); validA.setType("A"); // null rrdata - List temp = validA.getRrdatas(); // preserve + final List temp = validA.getRrdatas(); // preserve validA.setRrdatas(null); - response = localDns.createChange(PROJECT_ID1, ZONE_NAME1, validChange, null); + response = LOCAL_DNS_HELPER.createChange(PROJECT_ID1, ZONE_NAME1, validChange, null); assertEquals(400, response.code()); assertTrue(response.body().contains("additions[0].rrdata")); validA.setRrdatas(temp); @@ -923,7 +1765,7 @@ public void testCreateChangeValidatesChange() { nonExistent.setRrdatas(ImmutableList.of(":::::::")); Change delete = new Change(); delete.setDeletions(ImmutableList.of(nonExistent)); - response = localDns.createChange(PROJECT_ID1, ZONE_NAME1, delete, null); + response = LOCAL_DNS_HELPER.createChange(PROJECT_ID1, ZONE_NAME1, delete, null); assertEquals(404, response.code()); assertTrue(response.body().contains("deletions[0]")); } From 1f06caf21d73921280ce6e724cbaebb6f22169d4 Mon Sep 17 00:00:00 2001 From: Martin Derka Date: Wed, 24 Feb 2016 10:11:02 -0800 Subject: [PATCH 3/3] The following has been done within multiple iterations: Refactored URL parsing and removed unnecessary doc in local helper. Refactored to use AtomicReference instead of singleton Map. Adjusted tests of paging to test that pages contain expected objects. Added check and test for deleting non-empy zone. Removed RrsetWapper and used tailMap in listing. Added @VisibleForTesting annotations. Added fails to expected exceptions. Added error message checks. Addressed codacy suggestions. Assigned project ID to the helper and test. Added pool of executors instead of spawning one threads. Reduced number of threads. --- .../{ => dns}/testing/LocalDnsHelper.java | 719 ++++------- .../testing/OptionParsers.java} | 63 +- .../com/google/gcloud/spi/DefaultDnsRpc.java | 6 +- .../{ => dns}/testing/LocalDnsHelperTest.java | 1116 ++++++----------- 4 files changed, 678 insertions(+), 1226 deletions(-) rename gcloud-java-dns/src/main/java/com/google/gcloud/{ => dns}/testing/LocalDnsHelper.java (63%) rename gcloud-java-dns/src/main/java/com/google/gcloud/{testing/OptionParsersAndExtractors.java => dns/testing/OptionParsers.java} (80%) rename gcloud-java-dns/src/test/java/com/google/gcloud/{ => dns}/testing/LocalDnsHelperTest.java (63%) diff --git a/gcloud-java-dns/src/main/java/com/google/gcloud/testing/LocalDnsHelper.java b/gcloud-java-dns/src/main/java/com/google/gcloud/dns/testing/LocalDnsHelper.java similarity index 63% rename from gcloud-java-dns/src/main/java/com/google/gcloud/testing/LocalDnsHelper.java rename to gcloud-java-dns/src/main/java/com/google/gcloud/dns/testing/LocalDnsHelper.java index b8483bc4fcd7..f9cd1a11281e 100644 --- a/gcloud-java-dns/src/main/java/com/google/gcloud/testing/LocalDnsHelper.java +++ b/gcloud-java-dns/src/main/java/com/google/gcloud/dns/testing/LocalDnsHelper.java @@ -14,9 +14,9 @@ * limitations under the License. */ -package com.google.gcloud.testing; +package com.google.gcloud.dns.testing; -import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.net.InetAddresses.isInetAddress; import static java.net.HttpURLConnection.HTTP_NO_CONTENT; import static java.net.HttpURLConnection.HTTP_OK; @@ -27,12 +27,12 @@ import com.google.api.services.dns.model.Project; import com.google.api.services.dns.model.Quota; import com.google.api.services.dns.model.ResourceRecordSet; -import com.google.common.base.Function; +import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Joiner; -import com.google.common.base.MoreObjects; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; +import com.google.common.collect.ImmutableSortedMap; import com.google.common.collect.Lists; import com.google.common.collect.Sets; import com.google.common.io.ByteStreams; @@ -54,26 +54,32 @@ import java.net.URISyntaxException; import java.nio.charset.StandardCharsets; import java.util.ArrayList; +import java.util.Arrays; import java.util.HashMap; import java.util.LinkedList; import java.util.List; import java.util.Map; +import java.util.NavigableMap; import java.util.NavigableSet; -import java.util.Objects; import java.util.Random; import java.util.Set; +import java.util.SortedMap; import java.util.TreeMap; import java.util.TreeSet; import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.concurrent.ConcurrentNavigableMap; import java.util.concurrent.ConcurrentSkipListMap; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; import java.util.logging.Level; import java.util.logging.Logger; +import java.util.regex.Pattern; import java.util.zip.GZIPInputStream; -import javax.annotation.Nullable; - /** - * A utility to create local Google Cloud DNS mock. + * A local Google Cloud DNS mock. * *

The mock runs in a separate thread, listening for HTTP requests on the local machine at an * ephemeral port. @@ -81,7 +87,7 @@ *

While the mock attempts to simulate the service, there are some differences in the behaviour. * The mock will accept any project ID and never returns a notFound or another error because of * project ID. It assumes that all project IDs exist and that the user has all the necessary - * privileges to manipulate any project. Similarly, the local simulation does not work with any + * privileges to manipulate any project. Similarly, the local simulation does not require * verification of domain name ownership. Any request for creating a managed zone will be approved. * The mock does not track quota and will allow the user to exceed it. The mock provides only basic * validation of the DNS data for records of type A and AAAA. It does not validate any other record @@ -97,16 +103,21 @@ public class LocalDnsHelper { private static final Random ID_GENERATOR = new Random(); private static final String VERSION = "v1"; private static final String CONTEXT = "/dns/" + VERSION + "/projects"; - private static final Set SUPPORTED_COMPRESSION_ENCODINGS = - ImmutableSet.of("gzip", "x-gzip"); + private static final Set ENCODINGS = ImmutableSet.of("gzip", "x-gzip"); private static final List TYPES = ImmutableList.of("A", "AAAA", "CNAME", "MX", "NAPTR", "NS", "PTR", "SOA", "SPF", "SRV", "TXT"); + private static final TreeSet FORBIDDEN = Sets.newTreeSet( + ImmutableList.of("google.com.", "com.", "example.com.", "net.", "org.")); + private static final Pattern ZONE_NAME_RE = Pattern.compile("[a-z][a-z0-9-]*"); + private static final ScheduledExecutorService EXECUTORS = + Executors.newScheduledThreadPool(2, Executors.defaultThreadFactory()); + private static final String PROJECT_ID = "dummyprojectid"; static { try { BASE_CONTEXT = new URI(CONTEXT); } catch (URISyntaxException e) { - throw new RuntimeException( + throw new IllegalArgumentException( "Could not initialize LocalDnsHelper due to URISyntaxException.", e); } } @@ -139,67 +150,7 @@ private enum CallRegex { } /** - * Wraps DNS data by adding a timestamp and id which is used for paging and listing. - */ - static class RrsetWrapper { - static final Function WRAP_FUNCTION = - new Function() { - @Nullable - @Override - public RrsetWrapper apply(@Nullable ResourceRecordSet input) { - return new RrsetWrapper(input); - } - }; - private final ResourceRecordSet rrset; - private final Long timestamp = System.currentTimeMillis(); - private String id; - - RrsetWrapper(ResourceRecordSet rrset) { - // The constructor creates a copy in order to prevent side effects. - this.rrset = new ResourceRecordSet(); - this.rrset.setName(rrset.getName()); - this.rrset.setTtl(rrset.getTtl()); - this.rrset.setRrdatas(ImmutableList.copyOf(rrset.getRrdatas())); - this.rrset.setType(rrset.getType()); - } - - void setId(String id) { - this.id = id; - } - - String id() { - return id; - } - - /** - * Equals does not care about the listing id and timestamp metadata, just the rrset. - */ - @Override - public boolean equals(Object other) { - return (other instanceof RrsetWrapper) && Objects.equals(rrset, ((RrsetWrapper) other).rrset); - } - - @Override - public int hashCode() { - return Objects.hash(rrset); - } - - ResourceRecordSet rrset() { - return rrset; - } - - @Override - public String toString() { - return MoreObjects.toStringHelper(this) - .add("rrset", rrset) - .add("timestamp", timestamp) - .add("id", id) - .toString(); - } - } - - /** - * Associates a project with a collection of ManagedZones. Thread safe. + * Associates a project with a collection of ManagedZones. */ static class ProjectContainer { private final Project project; @@ -220,29 +171,24 @@ ConcurrentSkipListMap zones() { } /** - * Associates a zone with a collection of changes and dns records. Thread safe. + * Associates a zone with a collection of changes and dns records. */ static class ZoneContainer { private final ManagedZone zone; - /** - * DNS records are held in a map to allow for atomic replacement of record sets when applying - * changes. The key for the map is always the zone name. The collection of records is immutable - * and must always exist, i.e., dnsRecords.get(zone.getName()) is never {@code null}. - */ - private final ConcurrentSkipListMap> - dnsRecords = new ConcurrentSkipListMap<>(); + private final AtomicReference> + dnsRecords = new AtomicReference<>(ImmutableSortedMap.of()); private final ConcurrentLinkedQueue changes = new ConcurrentLinkedQueue<>(); ZoneContainer(ManagedZone zone) { this.zone = zone; - this.dnsRecords.put(zone.getName(), ImmutableList.of()); + this.dnsRecords.set(ImmutableSortedMap.of()); } ManagedZone zone() { return zone; } - ConcurrentSkipListMap> dnsRecords() { + AtomicReference> dnsRecords() { return dnsRecords; } @@ -283,6 +229,7 @@ private enum Error { INTERNAL_ERROR(500, "global", "internalError", "INTERNAL_ERROR"), BAD_REQUEST(400, "global", "badRequest", "BAD_REQUEST"), INVALID(400, "global", "invalid", "INVALID"), + CONTAINER_NOT_EMPTY(400, "global", "containerNotEmpty", "CONTAINER_NOT_EMPTY"), NOT_AVAILABLE(400, "global", "managedZoneDnsNameNotAvailable", "NOT_AVAILABLE"), NOT_FOUND(404, "global", "notFound", "NOT_FOUND"), ALREADY_EXISTS(409, "global", "alreadyExists", "ALREADY_EXISTS"), @@ -325,34 +272,38 @@ private String toJson(String message) throws IOException { private class RequestHandler implements HttpHandler { - /** - * Chooses the proper handler for a request. - */ private Response pickHandler(HttpExchange exchange, CallRegex regex) { + URI relative = BASE_CONTEXT.relativize(exchange.getRequestURI()); + String path = relative.getPath(); + String[] tokens = path.split("/"); + String projectId = tokens.length > 0 ? tokens[0] : null; + String zoneName = tokens.length > 2 ? tokens[2] : null; + String changeId = tokens.length > 4 ? tokens[4] : null; + String query = relative.getQuery(); switch (regex) { case CHANGE_GET: - return handleChangeGet(exchange); + return getChange(projectId, zoneName, changeId, query); case CHANGE_LIST: - return handleChangeList(exchange); + return listChanges(projectId, zoneName, query); case ZONE_GET: - return handleZoneGet(exchange); + return getZone(projectId, zoneName, query); case ZONE_DELETE: - return handleZoneDelete(exchange); + return deleteZone(projectId, zoneName); case ZONE_LIST: - return handleZoneList(exchange); + return listZones(projectId, query); case PROJECT_GET: - return handleProjectGet(exchange); + return getProject(projectId, query); case RECORD_LIST: - return handleDnsRecordList(exchange); + return listDnsRecords(projectId, zoneName, query); case ZONE_CREATE: try { - return handleZoneCreate(exchange); + return handleZoneCreate(exchange, projectId, query); } catch (IOException ex) { return Error.BAD_REQUEST.response(ex.getMessage()); } case CHANGE_CREATE: try { - return handleChangeCreate(exchange); + return handleChangeCreate(exchange, projectId, zoneName, query); } catch (IOException ex) { return Error.BAD_REQUEST.response(ex.getMessage()); } @@ -367,122 +318,53 @@ public void handle(HttpExchange exchange) throws IOException { String rawPath = exchange.getRequestURI().getRawPath(); for (CallRegex regex : CallRegex.values()) { if (requestMethod.equals(regex.method) && rawPath.matches(regex.pathRegex)) { - // there is a match, pass the handling accordingly Response response = pickHandler(exchange, regex); writeResponse(exchange, response); - return; // only one match is possible + return; } } - // could not be matched, the service returns 404 page not found here - writeResponse(exchange, Error.NOT_FOUND.response("The url does not match any API call.")); - } - - private Response handleZoneDelete(HttpExchange exchange) { - String path = BASE_CONTEXT.relativize(exchange.getRequestURI()).getPath(); - String[] tokens = path.split("/"); - String projectId = tokens[0]; - String zoneName = tokens[2]; - return deleteZone(projectId, zoneName); - } - - private Response handleZoneGet(HttpExchange exchange) { - String path = BASE_CONTEXT.relativize(exchange.getRequestURI()).getPath(); - String[] tokens = path.split("/"); - String projectId = tokens[0]; - String zoneName = tokens[2]; - String query = BASE_CONTEXT.relativize(exchange.getRequestURI()).getQuery(); - String[] fields = OptionParsersAndExtractors.parseGetOptions(query); - return getZone(projectId, zoneName, fields); - } - - private Response handleZoneList(HttpExchange exchange) { - String path = BASE_CONTEXT.relativize(exchange.getRequestURI()).getPath(); - String[] tokens = path.split("/"); - String projectId = tokens[0]; - String query = BASE_CONTEXT.relativize(exchange.getRequestURI()).getQuery(); - Map options = OptionParsersAndExtractors.parseListZonesOptions(query); - return listZones(projectId, options); - } - - private Response handleProjectGet(HttpExchange exchange) { - String path = BASE_CONTEXT.relativize(exchange.getRequestURI()).getPath(); - String[] tokens = path.split("/"); - String projectId = tokens[0]; - String query = BASE_CONTEXT.relativize(exchange.getRequestURI()).getQuery(); - String[] fields = OptionParsersAndExtractors.parseGetOptions(query); - return getProject(projectId, fields); + writeResponse(exchange, Error.NOT_FOUND.response(String.format( + "The url %s for %s method does not match any API call.", + requestMethod, exchange.getRequestURI()))); } /** * @throws IOException if the request cannot be parsed. */ - private Response handleChangeCreate(HttpExchange exchange) throws IOException { - String path = BASE_CONTEXT.relativize(exchange.getRequestURI()).getPath(); - String[] tokens = path.split("/"); - String projectId = tokens[0]; - String zoneName = tokens[2]; - String query = BASE_CONTEXT.relativize(exchange.getRequestURI()).getQuery(); - String[] fields = OptionParsersAndExtractors.parseGetOptions(query); + private Response handleChangeCreate(HttpExchange exchange, String projectId, String zoneName, + String query) throws IOException { String requestBody = decodeContent(exchange.getRequestHeaders(), exchange.getRequestBody()); - Change change = jsonFactory.fromString(requestBody, Change.class); + Change change; + try { + change = jsonFactory.fromString(requestBody, Change.class); + } catch (IllegalArgumentException ex) { + return Error.REQUIRED.response( + "The 'entity.change' parameter is required but was missing."); + } + String[] fields = OptionParsers.parseGetOptions(query); return createChange(projectId, zoneName, change, fields); } - private Response handleChangeGet(HttpExchange exchange) { - String path = BASE_CONTEXT.relativize(exchange.getRequestURI()).getPath(); - String[] tokens = path.split("/"); - String projectId = tokens[0]; - String zoneName = tokens[2]; - String changeId = tokens[4]; - String query = BASE_CONTEXT.relativize(exchange.getRequestURI()).getQuery(); - String[] fields = OptionParsersAndExtractors.parseGetOptions(query); - return getChange(projectId, zoneName, changeId, fields); - } - - private Response handleChangeList(HttpExchange exchange) { - String path = BASE_CONTEXT.relativize(exchange.getRequestURI()).getPath(); - String[] tokens = path.split("/"); - String projectId = tokens[0]; - String zoneName = tokens[2]; - String query = BASE_CONTEXT.relativize(exchange.getRequestURI()).getQuery(); - Map options = OptionParsersAndExtractors.parseListChangesOptions(query); - return listChanges(projectId, zoneName, options); - } - - private Response handleDnsRecordList(HttpExchange exchange) { - String path = BASE_CONTEXT.relativize(exchange.getRequestURI()).getPath(); - String[] tokens = path.split("/"); - String projectId = tokens[0]; - String zoneName = tokens[2]; - String query = BASE_CONTEXT.relativize(exchange.getRequestURI()).getQuery(); - Map options = OptionParsersAndExtractors.parseListDnsRecordsOptions(query); - return listDnsRecords(projectId, zoneName, options); - } - /** * @throws IOException if the request cannot be parsed. */ - private Response handleZoneCreate(HttpExchange exchange) throws IOException { - String path = BASE_CONTEXT.relativize(exchange.getRequestURI()).getPath(); - String[] tokens = path.split("/"); - String projectId = tokens[0]; - String query = BASE_CONTEXT.relativize(exchange.getRequestURI()).getQuery(); - String[] options = OptionParsersAndExtractors.parseGetOptions(query); + private Response handleZoneCreate(HttpExchange exchange, String projectId, String query) + throws IOException { String requestBody = decodeContent(exchange.getRequestHeaders(), exchange.getRequestBody()); ManagedZone zone; try { - // IllegalArgumentException if the request body is an empty string zone = jsonFactory.fromString(requestBody, ManagedZone.class); } catch (IllegalArgumentException ex) { return Error.REQUIRED.response( "The 'entity.managedZone' parameter is required but was missing."); } + String[] options = OptionParsers.parseGetOptions(query); return createZone(projectId, zone, options); } } private LocalDnsHelper(long delay) { - this.delayChange = delay; // 0 makes this synchronous + this.delayChange = delay; try { server = HttpServer.create(new InetSocketAddress(0), 0); port = server.getAddress().getPort(); @@ -501,9 +383,9 @@ ConcurrentSkipListMap projects() { /** * Creates new {@link LocalDnsHelper} instance that listens to requests on the local machine. This - * instance processes changes separate threads. The parameter determines how long a thread should - * wait before processing a change. If it is set to 0, the threading is turned off and the mock - * will behave synchronously. + * instance processes changes in separate thread. The parameter determines how long a thread + * should wait before processing a change. If it is set to 0, the threading is turned off and the + * mock will behave synchronously. * * @param delay delay for processing changes in ms or 0 for synchronous processing */ @@ -512,10 +394,10 @@ public static LocalDnsHelper create(Long delay) { } /** - * Returns a DnsOptions instance that sets the host to use the mock server. + * Returns a {@link DnsOptions} instance that sets the host to use the mock server. */ public DnsOptions options() { - return DnsOptions.builder().host("http://localhost:" + port).build(); + return DnsOptions.builder().projectId(PROJECT_ID).host("http://localhost:" + port).build(); } /** @@ -539,6 +421,7 @@ private static void writeResponse(HttpExchange exchange, Response response) { exchange.getResponseHeaders().add("Connection", "close"); exchange.sendResponseHeaders(response.code(), response.body().length()); if (response.code() != 204) { + // the server automatically sends headers and closes output stream when 204 is returned outputStream.write(response.body().getBytes(StandardCharsets.UTF_8)); } outputStream.close(); @@ -547,18 +430,15 @@ private static void writeResponse(HttpExchange exchange, Response response) { } } - /** - * Decodes content of the HttpRequest. - */ 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)) { + if (ENCODINGS.contains(encoding)) { input = new GZIPInputStream(inputStream); - } else if (!encoding.equals("identity")) { + } else if (!"identity".equals(encoding)) { throw new IOException( "The request has the following unsupported HTTP content encoding: " + encoding); } @@ -574,25 +454,25 @@ private static String decodeContent(Headers headers, InputStream inputStream) th * * @param context managedZones | projects | rrsets | changes */ + @VisibleForTesting static Response toListResponse(List serializedObjects, String context, String pageToken, boolean includePageToken) { - // start building response StringBuilder responseBody = new StringBuilder(); responseBody.append("{\"").append(context).append("\": ["); Joiner.on(",").appendTo(responseBody, serializedObjects); - responseBody.append("]"); - // add page token only if exists and is asked for + responseBody.append(']'); + // add page token only if it exists and is asked for if (pageToken != null && includePageToken) { - responseBody.append(",\"nextPageToken\": \"").append(pageToken).append("\""); + responseBody.append(",\"nextPageToken\": \"").append(pageToken).append('"'); } - responseBody.append("}"); + responseBody.append('}'); return new Response(HTTP_OK, responseBody.toString()); } /** * Prepares DNS records that are created by default for each zone. */ - private static ImmutableList defaultRecords(ManagedZone zone) { + private static ImmutableSortedMap defaultRecords(ManagedZone zone) { ResourceRecordSet soa = new ResourceRecordSet(); soa.setTtl(21600); soa.setName(zone.getDnsName()); @@ -606,17 +486,15 @@ private static ImmutableList defaultRecords(ManagedZone zone) { ns.setName(zone.getDnsName()); ns.setRrdatas(zone.getNameServers()); ns.setType("NS"); - RrsetWrapper nsWrapper = new RrsetWrapper(ns); - RrsetWrapper soaWrapper = new RrsetWrapper(soa); - ImmutableList results = ImmutableList.of(nsWrapper, soaWrapper); - nsWrapper.setId(getUniqueId(results)); - soaWrapper.setId(getUniqueId(results)); - return results; + String nsId = getUniqueId(ImmutableSet.of()); + String soaId = getUniqueId(ImmutableSet.of(nsId)); + return ImmutableSortedMap.of(nsId, ns, soaId, soa); } /** * Returns a list of four nameservers randomly chosen from the predefined set. */ + @VisibleForTesting static List randomNameservers() { ArrayList nameservers = Lists.newArrayList( "dns1.googlecloud.com", "dns2.googlecloud.com", "dns3.googlecloud.com", @@ -630,23 +508,14 @@ static List randomNameservers() { } /** - * Returns a hex string id (used for a dns record) unique within the set of wrappers. + * Returns a hex string id (used for a dns record) unique within the set of ids. */ - static String getUniqueId(List wrappers) { - TreeSet ids = Sets.newTreeSet(Lists.transform(wrappers, - new Function() { - @Override - public String apply(RrsetWrapper input) { - return input.id() == null ? "null" : input.id(); - } - })); + @VisibleForTesting + static String getUniqueId(Set ids) { String id; do { id = Long.toHexString(System.currentTimeMillis()) + Long.toHexString(Math.abs(ID_GENERATOR.nextLong())); - if (!ids.contains(id)) { - return id; - } } while (ids.contains(id)); return id; } @@ -654,21 +523,19 @@ public String apply(RrsetWrapper input) { /** * Tests if a DNS record matches name and type (if provided). Used for filtering. */ + @VisibleForTesting static boolean matchesCriteria(ResourceRecordSet record, String name, String type) { if (type != null && !record.getType().equals(type)) { return false; } - if (name != null && !record.getName().equals(name)) { - return false; - } - return true; + return name == null || record.getName().equals(name); } /** * Returns a project container. Never returns {@code null} because we assume that all projects * exists. */ - ProjectContainer findProject(String projectId) { + private ProjectContainer findProject(String projectId) { ProjectContainer defaultProject = createProject(projectId); projects.putIfAbsent(projectId, defaultProject); return projects.get(projectId); @@ -677,6 +544,7 @@ ProjectContainer findProject(String projectId) { /** * Returns a zone container. Returns {@code null} if zone does not exist within project. */ + @VisibleForTesting ZoneContainer findZone(String projectId, String zoneName) { ProjectContainer projectContainer = findProject(projectId); // never null return projectContainer.zones().get(zoneName); @@ -685,6 +553,7 @@ ZoneContainer findZone(String projectId, String zoneName) { /** * Returns a change found by its id. Returns {@code null} if such a change does not exist. */ + @VisibleForTesting Change findChange(String projectId, String zoneName, String changeId) { ZoneContainer wrapper = findZone(projectId, zoneName); return wrapper == null ? null : wrapper.findChange(changeId); @@ -693,20 +562,20 @@ Change findChange(String projectId, String zoneName, String changeId) { /** * Returns a response to getChange service call. */ - Response getChange(String projectId, String zoneName, String changeId, String[] fields) { + @VisibleForTesting + Response getChange(String projectId, String zoneName, String changeId, String query) { Change change = findChange(projectId, zoneName, changeId); if (change == null) { ZoneContainer zone = findZone(projectId, zoneName); if (zone == null) { - // zone does not exist return Error.NOT_FOUND.response(String.format( "The 'parameters.managedZone' resource named '%s' does not exist.", zoneName)); } - // zone exists but change does not return Error.NOT_FOUND.response(String.format( "The 'parameters.changeId' resource named '%s' does not exist.", changeId)); } - Change result = OptionParsersAndExtractors.extractFields(change, fields); + String[] fields = OptionParsers.parseGetOptions(query); + Change result = OptionParsers.extractFields(change, fields); try { return new Response(HTTP_OK, jsonFactory.toString(result)); } catch (IOException e) { @@ -719,13 +588,15 @@ Response getChange(String projectId, String zoneName, String changeId, String[] /** * Returns a response to getZone service call. */ - Response getZone(String projectId, String zoneName, String[] fields) { + @VisibleForTesting + Response getZone(String projectId, String zoneName, String query) { ZoneContainer container = findZone(projectId, zoneName); if (container == null) { return Error.NOT_FOUND.response(String.format( "The 'parameters.managedZone' resource named '%s' does not exist.", zoneName)); } - ManagedZone result = OptionParsersAndExtractors.extractFields(container.zone(), fields); + String[] fields = OptionParsers.parseGetOptions(query); + ManagedZone result = OptionParsers.extractFields(container.zone(), fields); try { return new Response(HTTP_OK, jsonFactory.toString(result)); } catch (IOException e) { @@ -738,11 +609,11 @@ Response getZone(String projectId, String zoneName, String[] fields) { * We assume that every project exists. If we do not have it in the collection yet, we just create * a new default project instance with default quota. */ - Response getProject(String projectId, String[] fields) { - ProjectContainer defaultProject = createProject(projectId); - projects.putIfAbsent(projectId, defaultProject); - Project project = projects.get(projectId).project(); // project is now guaranteed to exist - Project result = OptionParsersAndExtractors.extractFields(project, fields); + @VisibleForTesting + Response getProject(String projectId, String query) { + String[] fields = OptionParsers.parseGetOptions(query); + Project project = findProject(projectId).project(); // creates project if needed + Project result = OptionParsers.extractFields(project, fields); try { return new Response(HTTP_OK, jsonFactory.toString(result)); } catch (IOException e) { @@ -752,8 +623,7 @@ Response getProject(String projectId, String[] fields) { } /** - * Creates a project. It generates a project number randomly (we do not have project numbers - * available). + * Creates a project. It generates a project number randomly. */ private ProjectContainer createProject(String projectId) { Quota quota = new Quota(); @@ -771,14 +641,21 @@ private ProjectContainer createProject(String projectId) { return new ProjectContainer(project); } - /** - * Deletes a zone. - */ + @VisibleForTesting Response deleteZone(String projectId, String zoneName) { + ZoneContainer zone = findZone(projectId, zoneName); + ImmutableSortedMap rrsets = zone == null + ? ImmutableSortedMap.of() : zone.dnsRecords().get(); + ImmutableList defaults = ImmutableList.of("NS", "SOA"); + for (ResourceRecordSet current : rrsets.values()) { + if (!defaults.contains(current.getType())) { + return Error.CONTAINER_NOT_EMPTY.response(String.format( + "The resource named '%s' cannot be deleted because it is not empty", zoneName)); + } + } ProjectContainer projectContainer = projects.get(projectId); ZoneContainer previous = projectContainer.zones.remove(zoneName); return previous == null - // map was not in the collection ? Error.NOT_FOUND.response(String.format( "The 'parameters.managedZone' resource named '%s' does not exist.", zoneName)) : new Response(HTTP_NO_CONTENT, "{}"); @@ -787,14 +664,12 @@ Response deleteZone(String projectId, String zoneName) { /** * Creates new managed zone and stores it in the collection. Assumes that project exists. */ - Response createZone(String projectId, ManagedZone zone, String[] fields) { - checkNotNull(zone, "Zone to create cannot be null"); - // check if the provided data is valid + @VisibleForTesting + Response createZone(String projectId, ManagedZone zone, String... fields) { Response errorResponse = checkZone(zone); if (errorResponse != null) { return errorResponse; } - // create a copy of the managed zone in order to avoid side effects ManagedZone completeZone = new ManagedZone(); completeZone.setName(zone.getName()); completeZone.setDnsName(zone.getDnsName()); @@ -802,13 +677,10 @@ Response createZone(String projectId, ManagedZone zone, String[] fields) { completeZone.setNameServerSet(zone.getNameServerSet()); completeZone.setCreationTime(ISODateTimeFormat.dateTime().withZoneUTC() .print(System.currentTimeMillis())); - completeZone.setId( - new BigInteger(String.valueOf(Math.abs(ID_GENERATOR.nextLong() % Long.MAX_VALUE)))); + completeZone.setId(BigInteger.valueOf(Math.abs(ID_GENERATOR.nextLong() % Long.MAX_VALUE))); completeZone.setNameServers(randomNameservers()); ZoneContainer zoneContainer = new ZoneContainer(completeZone); - // create the default NS and SOA records - zoneContainer.dnsRecords().put(zone.getName(), defaultRecords(completeZone)); - // place the zone in the data collection + zoneContainer.dnsRecords().set(defaultRecords(completeZone)); ProjectContainer projectContainer = findProject(projectId); ZoneContainer oldValue = projectContainer.zones().putIfAbsent( completeZone.getName(), zoneContainer); @@ -816,8 +688,7 @@ Response createZone(String projectId, ManagedZone zone, String[] fields) { return Error.ALREADY_EXISTS.response(String.format( "The resource 'entity.managedZone' named '%s' already exists", completeZone.getName())); } - // now return the desired attributes - ManagedZone result = OptionParsersAndExtractors.extractFields(completeZone, fields); + ManagedZone result = OptionParsers.extractFields(completeZone, fields); try { return new Response(HTTP_OK, jsonFactory.toString(result)); } catch (IOException e) { @@ -827,30 +698,28 @@ Response createZone(String projectId, ManagedZone zone, String[] fields) { } /** - * Creates a new change, stores it, and invokes processing in a new thread. + * Creates a new change, stores it, and if delayChange > 0, invokes processing in a new thread. */ - Response createChange(String projectId, String zoneName, Change change, String[] fields) { + Response createChange(String projectId, String zoneName, Change change, String... fields) { ZoneContainer zoneContainer = findZone(projectId, zoneName); if (zoneContainer == null) { return Error.NOT_FOUND.response(String.format( "The 'parameters.managedZone' resource named %s does not exist.", zoneName)); } - // check that the change to be applied is valid Response response = checkChange(change, zoneContainer); if (response != null) { return response; } - // start applying - Change completeChange = new Change(); // copy to avoid side effects + Change completeChange = new Change(); if (change.getAdditions() != null) { completeChange.setAdditions(ImmutableList.copyOf(change.getAdditions())); } if (change.getDeletions() != null) { completeChange.setDeletions(ImmutableList.copyOf(change.getDeletions())); } - /* we need to get the proper ID in concurrent environment - the element fell on an index between 0 and maxId - we will reset all IDs in this range (all of them are valid) */ + /* We need to set ID for the change. We are working in concurrent environment. We know that the + element fell on an index between 0 and maxId, so we will reset all IDs in this range (all of + them are valid for the respective objects). */ ConcurrentLinkedQueue changeSequence = zoneContainer.changes(); changeSequence.add(completeChange); int maxId = changeSequence.size(); @@ -859,13 +728,13 @@ we will reset all IDs in this range (all of them are valid) */ if (index == maxId) { break; } - c.setId(String.valueOf(++index)); // indexing from 1 + c.setId(String.valueOf(++index)); } - completeChange.setStatus("pending"); // not finished yet + completeChange.setStatus("pending"); completeChange.setStartTime(ISODateTimeFormat.dateTime().withZoneUTC() - .print(System.currentTimeMillis())); // accepted + .print(System.currentTimeMillis())); invokeChange(projectId, zoneName, completeChange.getId()); - Change result = OptionParsersAndExtractors.extractFields(completeChange, fields); + Change result = OptionParsers.extractFields(completeChange, fields); try { return new Response(HTTP_OK, jsonFactory.toString(result)); } catch (IOException e) { @@ -876,28 +745,20 @@ we will reset all IDs in this range (all of them are valid) */ } /** - * Applies change. Uses a new thread which applies the change only if DELAY_CHANGE is > 0. + * Applies change. Uses a different pooled thread which applies the change only if {@code + * delayChange} is > 0. */ - private Thread invokeChange(final String projectId, final String zoneName, + private void invokeChange(final String projectId, final String zoneName, final String changeId) { if (delayChange > 0) { - Thread thread = new Thread() { + EXECUTORS.schedule(new Runnable() { @Override public void run() { - try { - Thread.sleep(delayChange); // simulate delayed execution - } catch (InterruptedException ex) { - log.log(Level.WARNING, "Thread was interrupted while sleeping.", ex); - } - // start applying the changes applyExistingChange(projectId, zoneName, changeId); } - }; - thread.start(); - return thread; + }, delayChange, TimeUnit.MILLISECONDS); } else { applyExistingChange(projectId, zoneName, changeId); - return null; } } @@ -910,44 +771,55 @@ private void applyExistingChange(String projectId, String zoneName, String chang return; // no such change exists, nothing to do } ZoneContainer wrapper = findZone(projectId, zoneName); - ConcurrentSkipListMap> dnsRecords = wrapper.dnsRecords(); + if (wrapper == null) { + return; // no such zone exists; it might have been deleted by another thread + } + AtomicReference> dnsRecords = + wrapper.dnsRecords(); while (true) { // managed zone must have a set of records which is not null - ImmutableList original = dnsRecords.get(zoneName); - assert original != null; - List copy = Lists.newLinkedList(original); + ImmutableSortedMap original = dnsRecords.get(); + // the copy will be populated when handling deletions + SortedMap copy = new TreeMap<>(); // apply deletions first List deletions = change.getDeletions(); if (deletions != null) { - List transformedDeletions = Lists.transform(deletions, - RrsetWrapper.WRAP_FUNCTION); - copy.removeAll(transformedDeletions); + for (Map.Entry entry : original.entrySet()) { + if (!deletions.contains(entry.getValue())) { + copy.put(entry.getKey(), entry.getValue()); + } + } + } else { + copy.putAll(original); } // apply additions List additions = change.getAdditions(); if (additions != null) { for (ResourceRecordSet addition : additions) { - String id = getUniqueId(copy); - RrsetWrapper rrsetWrapper = new RrsetWrapper(addition); - rrsetWrapper.setId(id); - copy.add(rrsetWrapper); + ResourceRecordSet rrset = new ResourceRecordSet(); + rrset.setName(addition.getName()); + rrset.setRrdatas(ImmutableList.copyOf(addition.getRrdatas())); + rrset.setTtl(addition.getTtl()); + rrset.setType(addition.getType()); + String id = getUniqueId(copy.keySet()); + copy.put(id, rrset); } } - // make it immutable and replace - boolean success = dnsRecords.replace(zoneName, original, ImmutableList.copyOf(copy)); + boolean success = dnsRecords.compareAndSet(original, ImmutableSortedMap.copyOf(copy)); if (success) { break; // success if no other thread modified the value in the meantime } } - // set status to done change.setStatus("done"); } /** * Lists zones. Next page token is the last listed zone name and is returned only of there is more - * to list. + * to list and if the user does not exclude nextPageToken from field options. */ - Response listZones(String projectId, Map options) { + @VisibleForTesting + Response listZones(String projectId, String query) { + Map options = OptionParsers.parseListZonesOptions(query); Response response = checkListOptions(options); if (response != null) { return response; @@ -958,46 +830,43 @@ Response listZones(String projectId, Map options) { String pageToken = (String) options.get("pageToken"); Integer maxResults = options.get("maxResults") == null ? null : Integer.valueOf((String) options.get("maxResults")); - // matches will be included in the result if true - boolean listing = (pageToken == null || !containers.containsKey(pageToken)); - boolean sizeReached = false; // maximum result size was reached, we should not return more - boolean hasMorePages = false; // should next page token be included in the response? + boolean sizeReached = false; + boolean hasMorePages = false; LinkedList serializedZones = new LinkedList<>(); String lastZoneName = null; - for (ZoneContainer zoneContainer : containers.values()) { + ConcurrentNavigableMap fragment = + pageToken != null ? containers.tailMap(pageToken, false) : containers; + for (ZoneContainer zoneContainer : fragment.values()) { ManagedZone zone = zoneContainer.zone(); - if (listing) { - if (dnsName == null || zone.getDnsName().equals(dnsName)) { - if (sizeReached) { - // we do not add this, just note that there would be more and there should be a token - hasMorePages = true; - break; - } else { - try { - lastZoneName = zone.getName(); - serializedZones.addLast(jsonFactory.toString( - OptionParsersAndExtractors.extractFields(zone, fields))); - } catch (IOException e) { - return Error.INTERNAL_ERROR.response(String.format( - "Error when serializing managed zone %s in project %s", zone.getName(), - projectId)); - } + if (dnsName == null || zone.getDnsName().equals(dnsName)) { + if (sizeReached) { + // we do not add this, just note that there would be more and there should be a token + hasMorePages = true; + break; + } else { + try { + lastZoneName = zone.getName(); + serializedZones.addLast(jsonFactory.toString( + OptionParsers.extractFields(zone, fields))); + } catch (IOException e) { + return Error.INTERNAL_ERROR.response(String.format( + "Error when serializing managed zone %s in project %s", lastZoneName, projectId)); } } } - // either we are listing already, or we check if we should start in the next iteration - listing = zone.getName().equals(pageToken) || listing; - sizeReached = (maxResults != null) && maxResults.equals(serializedZones.size()); + sizeReached = maxResults != null && maxResults.equals(serializedZones.size()); } boolean includePageToken = - hasMorePages && (fields == null || ImmutableList.copyOf(fields).contains("nextPageToken")); + hasMorePages && (fields == null || Arrays.asList(fields).contains("nextPageToken")); return toListResponse(serializedZones, "managedZones", lastZoneName, includePageToken); } /** - * Lists DNS records for a zone. Next page token is ID of the last record listed. + * Lists DNS records for a zone. Next page token is the ID of the last record listed. */ - Response listDnsRecords(String projectId, String zoneName, Map options) { + @VisibleForTesting + Response listDnsRecords(String projectId, String zoneName, String query) { + Map options = OptionParsers.parseListDnsRecordsOptions(query); Response response = checkListOptions(options); if (response != null) { return response; @@ -1007,52 +876,51 @@ Response listDnsRecords(String projectId, String zoneName, Map o return Error.NOT_FOUND.response(String.format( "The 'parameters.managedZone' resource named '%s' does not exist.", zoneName)); } - List dnsRecords = zoneContainer.dnsRecords().get(zoneName); + ImmutableSortedMap dnsRecords = zoneContainer.dnsRecords().get(); String[] fields = (String[]) options.get("fields"); String name = (String) options.get("name"); String type = (String) options.get("type"); String pageToken = (String) options.get("pageToken"); + ImmutableSortedMap fragment = + pageToken != null ? dnsRecords.tailMap(pageToken, false) : dnsRecords; Integer maxResults = options.get("maxResults") == null ? null : Integer.valueOf((String) options.get("maxResults")); - boolean listing = (pageToken == null); // matches will be included in the result if true - boolean sizeReached = false; // maximum result size was reached, we should not return more - boolean hasMorePages = false; // should next page token be included in the response? + boolean sizeReached = false; + boolean hasMorePages = false; LinkedList serializedRrsets = new LinkedList<>(); String lastRecordId = null; - for (RrsetWrapper recordWrapper : dnsRecords) { - ResourceRecordSet record = recordWrapper.rrset(); - if (listing) { - if (matchesCriteria(record, name, type)) { - if (sizeReached) { - // we do not add this, just note that there would be more and there should be a token - hasMorePages = true; - break; - } else { - lastRecordId = recordWrapper.id(); - try { - serializedRrsets.addLast(jsonFactory.toString( - OptionParsersAndExtractors.extractFields(record, fields))); - } catch (IOException e) { - return Error.INTERNAL_ERROR.response(String.format( - "Error when serializing resource record set in managed zone %s in project %s", - zoneName, projectId)); - } + for (String recordId : fragment.keySet()) { + ResourceRecordSet record = fragment.get(recordId); + if (matchesCriteria(record, name, type)) { + if (sizeReached) { + // we do not add this, just note that there would be more and there should be a token + hasMorePages = true; + break; + } else { + lastRecordId = recordId; + try { + serializedRrsets.addLast(jsonFactory.toString( + OptionParsers.extractFields(record, fields))); + } catch (IOException e) { + return Error.INTERNAL_ERROR.response(String.format( + "Error when serializing resource record set in managed zone %s in project %s", + zoneName, projectId)); } } } - // either we are listing already, or we check if we should start in the next iteration - listing = recordWrapper.id().equals(pageToken) || listing; - sizeReached = (maxResults != null) && maxResults.equals(serializedRrsets.size()); + sizeReached = maxResults != null && maxResults.equals(serializedRrsets.size()); } boolean includePageToken = - hasMorePages && (fields == null || ImmutableList.copyOf(fields).contains("nextPageToken")); + hasMorePages && (fields == null || Arrays.asList(fields).contains("nextPageToken")); return toListResponse(serializedRrsets, "rrsets", lastRecordId, includePageToken); } /** - * Lists changes. Next page token is ID of the last change listed. + * Lists changes. Next page token is the ID of the last change listed. */ - Response listChanges(String projectId, String zoneName, Map options) { + @VisibleForTesting + Response listChanges(String projectId, String zoneName, String query) { + Map options = OptionParsers.parseListChangesOptions(query); Response response = checkListOptions(options); if (response != null) { return response; @@ -1063,7 +931,7 @@ Response listChanges(String projectId, String zoneName, Map opti "The 'parameters.managedZone' resource named '%s' does not exist", zoneName)); } // take a sorted snapshot of the current change list - TreeMap changes = new TreeMap<>(); + NavigableMap changes = new TreeMap<>(); for (Change c : zoneContainer.changes()) { if (c.getId() != null) { changes.put(Integer.valueOf(c.getId()), c); @@ -1074,51 +942,54 @@ Response listChanges(String projectId, String zoneName, Map opti String pageToken = (String) options.get("pageToken"); Integer maxResults = options.get("maxResults") == null ? null : Integer.valueOf((String) options.get("maxResults")); - // we are not reading sort by as it the only key is the change sequence + // as the only supported field is change sequence, we are not reading sortBy NavigableSet keys; if ("descending".equals(sortOrder)) { keys = changes.descendingKeySet(); } else { keys = changes.navigableKeySet(); } - boolean listing = (pageToken == null); // matches will be included in the result if true - boolean sizeReached = false; // maximum result size was reached, we should not return more - boolean hasMorePages = false; // should next page token be included in the response? + Integer from = null; + try { + from = Integer.valueOf(pageToken); + } catch (NumberFormatException ex) { + // ignore page token + } + keys = from != null ? keys.tailSet(from, false) : keys; + NavigableMap fragment = + from != null && changes.containsKey(from) ? changes.tailMap(from, false) : changes; + boolean sizeReached = false; + boolean hasMorePages = false; LinkedList serializedResults = new LinkedList<>(); String lastChangeId = null; for (Integer key : keys) { - Change change = changes.get(key); - if (listing) { - if (sizeReached) { - // we do not add this, just note that there would be more and there should be a token - hasMorePages = true; - break; - } else { - lastChangeId = change.getId(); - try { - serializedResults.addLast(jsonFactory.toString( - OptionParsersAndExtractors.extractFields(change, fields))); - } catch (IOException e) { - return Error.INTERNAL_ERROR.response(String.format( - "Error when serializing change %s in managed zone %s in project %s", - change.getId(), zoneName, projectId)); - } + Change change = fragment.get(key); + if (sizeReached) { + // we do not add this, just note that there would be more and there should be a token + hasMorePages = true; + break; + } else { + lastChangeId = change.getId(); + try { + serializedResults.addLast(jsonFactory.toString( + OptionParsers.extractFields(change, fields))); + } catch (IOException e) { + return Error.INTERNAL_ERROR.response(String.format( + "Error when serializing change %s in managed zone %s in project %s", + lastChangeId, zoneName, projectId)); } } - - // either we are listing already, or we check if we should start in the next iteration - listing = change.getId().equals(pageToken) || listing; - sizeReached = (maxResults != null) && maxResults.equals(serializedResults.size()); + sizeReached = maxResults != null && maxResults.equals(serializedResults.size()); } boolean includePageToken = - hasMorePages && (fields == null || ImmutableList.copyOf(fields).contains("nextPageToken")); + hasMorePages && (fields == null || Arrays.asList(fields).contains("nextPageToken")); return toListResponse(serializedResults, "changes", lastChangeId, includePageToken); } /** * Validates a zone to be created. */ - static Response checkZone(ManagedZone zone) { + private static Response checkZone(ManagedZone zone) { if (zone.getName() == null) { return Error.REQUIRED.response( "The 'entity.managedZone.name' parameter is required but was missing."); @@ -1138,7 +1009,8 @@ static Response checkZone(ManagedZone zone) { } catch (NumberFormatException ex) { // expected } - if (zone.getName().isEmpty()) { + if (zone.getName().isEmpty() || zone.getName().length() > 32 + || !ZONE_NAME_RE.matcher(zone.getName()).matches()) { return Error.INVALID.response( String.format("Invalid value for 'entity.managedZone.name': '%s'", zone.getName())); } @@ -1146,9 +1018,7 @@ static Response checkZone(ManagedZone zone) { return Error.INVALID.response( String.format("Invalid value for 'entity.managedZone.dnsName': '%s'", zone.getDnsName())); } - TreeSet forbidden = Sets.newTreeSet( - ImmutableList.of("google.com.", "com.", "example.com.", "net.", "org.")); - if (forbidden.contains(zone.getDnsName())) { + if (FORBIDDEN.contains(zone.getDnsName())) { return Error.NOT_AVAILABLE.response(String.format( "The '%s' managed zone is not available to be created.", zone.getDnsName())); } @@ -1158,12 +1028,10 @@ static Response checkZone(ManagedZone zone) { /** * Validates a change to be created. */ + @VisibleForTesting static Response checkChange(Change change, ZoneContainer zone) { - checkNotNull(zone); - if ((change.getDeletions() != null && change.getDeletions().size() > 0) - || (change.getAdditions() != null && change.getAdditions().size() > 0)) { - // ok, this is what we want - } else { + if ((change.getDeletions() == null || change.getDeletions().size() <= 0) + && (change.getAdditions() == null || change.getAdditions().size() <= 0)) { return Error.REQUIRED.response("The 'entity.change' parameter is required but was missing."); } if (change.getAdditions() != null) { @@ -1186,7 +1054,7 @@ static Response checkChange(Change change, ZoneContainer zone) { counter++; } } - return additionsMeetDeletions(change.getAdditions(), change.getDeletions(), zone); + return checkAdditionsDeletions(change.getAdditions(), change.getDeletions(), zone); // null if everything is ok } @@ -1197,6 +1065,7 @@ static Response checkChange(Change change, ZoneContainer zone) { * @param index the index or addition or deletion in the list * @param zone the zone that this change is applied to */ + @VisibleForTesting static Response checkRrset(ResourceRecordSet rrset, ZoneContainer zone, int index, String type) { if (rrset.getName() == null || !rrset.getName().endsWith(zone.zone().getDnsName())) { return Error.INVALID.response(String.format( @@ -1226,8 +1095,7 @@ static Response checkRrset(ResourceRecordSet rrset, ZoneContainer zone, int inde if ("deletions".equals(type)) { // check that deletion has a match by name and type boolean found = false; - for (RrsetWrapper rrsetWrapper : zone.dnsRecords().get(zone.zone().getName())) { - ResourceRecordSet wrappedRrset = rrsetWrapper.rrset(); + for (ResourceRecordSet wrappedRrset : zone.dnsRecords().get().values()) { if (rrset.getName().equals(wrappedRrset.getName()) && rrset.getType().equals(wrappedRrset.getType())) { found = true; @@ -1241,7 +1109,7 @@ static Response checkRrset(ResourceRecordSet rrset, ZoneContainer zone, int inde } // if found, we still need an exact match if ("deletions".equals(type) - && !zone.dnsRecords().get(zone.zone().getName()).contains(new RrsetWrapper(rrset))) { + && !zone.dnsRecords().get().containsValue(rrset)) { // such a record does not exist return Error.CONDITION_NOT_MET.response(String.format( "Precondition not met for 'entity.change.deletions[%s]", index)); @@ -1251,24 +1119,22 @@ static Response checkRrset(ResourceRecordSet rrset, ZoneContainer zone, int inde } /** - * Checks that for each record that already exists, we have a matching deletion. Furthermore, - * check that mandatory SOA and NS records stay. + * Checks against duplicate additions (for each record to be added that already exists, we must + * have a matching deletion. Furthermore, check that mandatory SOA and NS records stay. */ - static Response additionsMeetDeletions(List additions, + static Response checkAdditionsDeletions(List additions, List deletions, ZoneContainer zone) { if (additions != null) { int index = 0; for (ResourceRecordSet rrset : additions) { - for (RrsetWrapper wrapper : zone.dnsRecords().get(zone.zone().getName())) { - ResourceRecordSet wrappedRrset = wrapper.rrset(); + for (ResourceRecordSet wrappedRrset : zone.dnsRecords().get().values()) { if (rrset.getName().equals(wrappedRrset.getName()) - && rrset.getType().equals(wrappedRrset.getType())) { - // such a record exist and we must have a deletion - if (deletions == null || !deletions.contains(wrappedRrset)) { - return Error.ALREADY_EXISTS.response(String.format( - "The 'entity.change.additions[%s]' resource named '%s (%s)' already exists.", - index, rrset.getName(), rrset.getType())); - } + && rrset.getType().equals(wrappedRrset.getType()) + // such a record exist and we must have a deletion + && (deletions == null || !deletions.contains(wrappedRrset))) { + return Error.ALREADY_EXISTS.response(String.format( + "The 'entity.change.additions[%s]' resource named '%s (%s)' already exists.", + index, rrset.getName(), rrset.getType())); } } if (rrset.getType().equals("SOA") && findByNameAndType(deletions, null, "SOA") == null) { @@ -1323,44 +1189,11 @@ private static ResourceRecordSet findByNameAndType(Iterable r * We only provide the most basic validation for A and AAAA records. */ static boolean checkRrData(String data, String type) { - // todo add validation for other records - String[] tokens; switch (type) { case "A": - tokens = data.split("\\."); - if (tokens.length != 4) { - return false; - } - for (String token : tokens) { - try { - Integer number = Integer.valueOf(token); - if (number < 0 || number > 255) { - return false; - } - } catch (NumberFormatException ex) { - return false; - } - } - return true; + return !data.contains(":") && isInetAddress(data); case "AAAA": - tokens = data.split(":", -1); - if (tokens.length != 8) { - return false; - } - for (String token : tokens) { - try { - if (!token.isEmpty()) { - // empty is ok - Long number = Long.parseLong(token, 16); - if (number < 0 || number > 0xFFFF) { - return false; - } - } - } catch (NumberFormatException ex) { - return false; - } - } - return true; + return data.contains(":") && isInetAddress(data); default: return true; } @@ -1369,11 +1202,12 @@ static boolean checkRrData(String data, String type) { /** * Check supplied listing options. */ + @VisibleForTesting static Response checkListOptions(Map options) { // for general listing String maxResultsString = (String) options.get("maxResults"); if (maxResultsString != null) { - Integer maxResults = null; + Integer maxResults; try { maxResults = Integer.valueOf(maxResultsString); } catch (NumberFormatException ex) { @@ -1386,24 +1220,21 @@ static Response checkListOptions(Map options) { } } String dnsName = (String) options.get("dnsName"); - if (dnsName != null) { - if (!dnsName.endsWith(".")) { - return Error.INVALID.response(String.format( - "Invalid value for 'parameters.dnsName': '%s'", dnsName)); - } + if (dnsName != null && !dnsName.endsWith(".")) { + return Error.INVALID.response(String.format( + "Invalid value for 'parameters.dnsName': '%s'", dnsName)); } // for listing dns records, name must be fully qualified String name = (String) options.get("name"); - if (name != null) { - if (!name.endsWith(".")) { - return Error.INVALID.response(String.format( - "Invalid value for 'parameters.name': '%s'", name)); - } + if (name != null && !name.endsWith(".")) { + return Error.INVALID.response(String.format( + "Invalid value for 'parameters.name': '%s'", name)); } String type = (String) options.get("type"); // must be provided with name if (type != null) { if (name == null) { - return Error.INVALID.response("Invalid value for 'parameters.name': ''"); + return Error.INVALID.response("Invalid value for 'parameters.name': '' " + + "(name must be specified if type is specified)"); } if (!TYPES.contains(type)) { return Error.INVALID.response(String.format( diff --git a/gcloud-java-dns/src/main/java/com/google/gcloud/testing/OptionParsersAndExtractors.java b/gcloud-java-dns/src/main/java/com/google/gcloud/dns/testing/OptionParsers.java similarity index 80% rename from gcloud-java-dns/src/main/java/com/google/gcloud/testing/OptionParsersAndExtractors.java rename to gcloud-java-dns/src/main/java/com/google/gcloud/dns/testing/OptionParsers.java index 26759f7e3ccc..ecd7e8179efe 100644 --- a/gcloud-java-dns/src/main/java/com/google/gcloud/testing/OptionParsersAndExtractors.java +++ b/gcloud-java-dns/src/main/java/com/google/gcloud/dns/testing/OptionParsers.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.google.gcloud.testing; +package com.google.gcloud.dns.testing; import com.google.api.services.dns.model.Change; import com.google.api.services.dns.model.ManagedZone; @@ -27,12 +27,8 @@ /** * Utility helpers for LocalDnsHelper. */ -class OptionParsersAndExtractors { +class OptionParsers { - /** - * Makes a map of list options. Expects query to be only query part of the url (i.e., what follows - * the '?'). - */ static Map parseListZonesOptions(String query) { Map options = new HashMap<>(); if (query != null) { @@ -41,20 +37,15 @@ static Map parseListZonesOptions(String query) { String[] argEntry = arg.split("="); switch (argEntry[0]) { case "fields": - // List fields are in the form "managedZones(field1, field2, ...)" + // List fields are in the form "managedZones(field1, field2, ...),nextPageToken" String replaced = argEntry[1].replace("managedZones(", ","); replaced = replaced.replace(")", ","); // we will get empty strings, but it does not matter, they will be ignored - options.put( - "fields", - replaced.split(",")); + options.put("fields", replaced.split(",")); break; case "dnsName": options.put("dnsName", argEntry[1]); break; - case "nextPageToken": - options.put("nextPageToken", argEntry[1]); - break; case "pageToken": options.put("pageToken", argEntry[1]); break; @@ -70,10 +61,6 @@ static Map parseListZonesOptions(String query) { return options; } - /** - * Makes a map of list options. Expects query to be only query part of the url (i.e., what follows - * the '?'). This format is common for all of zone, change and project. - */ static String[] parseGetOptions(String query) { if (query != null) { String[] args = query.split("&"); @@ -85,14 +72,11 @@ static String[] parseGetOptions(String query) { } } } - return null; + return new String[0]; } - /** - * Extracts only request fields. - */ - static ManagedZone extractFields(ManagedZone fullZone, String[] fields) { - if (fields == null) { + static ManagedZone extractFields(ManagedZone fullZone, String... fields) { + if (fields == null || fields.length == 0) { return fullZone; } ManagedZone managedZone = new ManagedZone(); @@ -126,22 +110,17 @@ static ManagedZone extractFields(ManagedZone fullZone, String[] fields) { return managedZone; } - /** - * Extracts only request fields. - */ - static Change extractFields(Change fullChange, String[] fields) { - if (fields == null) { + static Change extractFields(Change fullChange, String... fields) { + if (fields == null || fields.length == 0) { return fullChange; } Change change = new Change(); for (String field : fields) { switch (field) { case "additions": - // todo the fragmentation is ignored here as our api does not support it change.setAdditions(fullChange.getAdditions()); break; case "deletions": - // todo the fragmentation is ignored here as our api does not support it change.setDeletions(fullChange.getDeletions()); break; case "id": @@ -160,11 +139,8 @@ static Change extractFields(Change fullChange, String[] fields) { return change; } - /** - * Extracts only request fields. - */ - static Project extractFields(Project fullProject, String[] fields) { - if (fields == null) { + static Project extractFields(Project fullProject, String... fields) { + if (fields == null || fields.length == 0) { return fullProject; } Project project = new Project(); @@ -186,11 +162,8 @@ static Project extractFields(Project fullProject, String[] fields) { return project; } - /** - * Extracts only request fields. - */ - static ResourceRecordSet extractFields(ResourceRecordSet fullRecord, String[] fields) { - if (fields == null) { + static ResourceRecordSet extractFields(ResourceRecordSet fullRecord, String... fields) { + if (fields == null || fields.length == 0) { return fullRecord; } ResourceRecordSet record = new ResourceRecordSet(); @@ -223,16 +196,9 @@ static Map parseListChangesOptions(String query) { String[] argEntry = arg.split("="); switch (argEntry[0]) { case "fields": - // todo we do not support fragmentation in deletions and additions in the library String replaced = argEntry[1].replace("changes(", ",").replace(")", ","); options.put("fields", replaced.split(",")); // empty strings will be ignored break; - case "name": - options.put("name", argEntry[1]); - break; - case "nextPageToken": - options.put("nextPageToken", argEntry[1]); - break; case "pageToken": options.put("pageToken", argEntry[1]); break; @@ -275,9 +241,6 @@ static Map parseListDnsRecordsOptions(String query) { case "pageToken": options.put("pageToken", argEntry[1]); break; - case "nextPageToken": - options.put("nextPageToken", argEntry[1]); - break; case "maxResults": // parsing to int is done while handling options.put("maxResults", argEntry[1]); diff --git a/gcloud-java-dns/src/main/java/com/google/gcloud/spi/DefaultDnsRpc.java b/gcloud-java-dns/src/main/java/com/google/gcloud/spi/DefaultDnsRpc.java index b72a21445a80..1df0a8a2f831 100644 --- a/gcloud-java-dns/src/main/java/com/google/gcloud/spi/DefaultDnsRpc.java +++ b/gcloud-java-dns/src/main/java/com/google/gcloud/spi/DefaultDnsRpc.java @@ -162,9 +162,9 @@ public Change getChangeRequest(String zoneName, String changeRequestId, Map EMPTY_RPC_OPTIONS = ImmutableMap.of(); - private static final DnsRpc RPC = - new DefaultDnsRpc(LOCAL_DNS_HELPER.options()); + private static final DnsRpc RPC = new DefaultDnsRpc(LOCAL_DNS_HELPER.options()); private static final String REAL_PROJECT_ID = LOCAL_DNS_HELPER.options().projectId(); private Map optionsMap; - private ManagedZone minimalZone = new ManagedZone(); // to be adjusted as needed - @BeforeClass public static void before() { - RRSET1.setName(DNS_NAME); - RRSET1.setType(RRSET_TYPE); - RRSET1.setRrdatas(ImmutableList.of("1.1.1.1")); ZONE1.setName(ZONE_NAME1); ZONE1.setDescription(""); ZONE1.setDnsName(DNS_NAME); - ZONE1.setNameServerSet("somenameserveset"); + ZONE1.setNameServerSet("somenameserverset"); ZONE2.setName(ZONE_NAME2); ZONE2.setDescription(""); ZONE2.setDnsName(DNS_NAME); - ZONE2.setNameServerSet("somenameserveset"); + ZONE2.setNameServerSet("somenameserverset"); + RRSET1.setName(DNS_NAME); + RRSET1.setType(RRSET_TYPE); + RRSET1.setRrdatas(ImmutableList.of("1.1.1.1")); RRSET2.setName(DNS_NAME); RRSET2.setType(RRSET_TYPE); + RRSET2.setRrdatas(ImmutableList.of("123.132.153.156")); RRSET_KEEP.setName(DNS_NAME); RRSET_KEEP.setType("MX"); RRSET_KEEP.setRrdatas(ImmutableList.of("255.255.255.254")); - RRSET2.setRrdatas(ImmutableList.of("123.132.153.156")); CHANGE1.setAdditions(ImmutableList.of(RRSET1, RRSET2)); CHANGE2.setDeletions(ImmutableList.of(RRSET2)); CHANGE_KEEP.setAdditions(ImmutableList.of(RRSET_KEEP)); @@ -98,17 +99,13 @@ public static void before() { LOCAL_DNS_HELPER.start(); } + @Rule + public Timeout globalTimeout = Timeout.seconds(60); + @Before public void setUp() { resetProjects(); optionsMap = new HashMap<>(); - minimalZone = copyZone(ZONE1); - } - - private static void resetProjects() { - for (String project : LOCAL_DNS_HELPER.projects().keySet()) { - LOCAL_DNS_HELPER.projects().remove(project); - } } @AfterClass @@ -116,279 +113,55 @@ public static void after() { LOCAL_DNS_HELPER.stop(); } - @Test - public void testMatchesCriteria() { - assertTrue(LocalDnsHelper.matchesCriteria(RRSET1, RRSET1.getName(), RRSET1.getType())); - assertFalse(LocalDnsHelper.matchesCriteria(RRSET1, RRSET1.getName(), "anothertype")); - assertTrue(LocalDnsHelper.matchesCriteria(RRSET1, null, RRSET1.getType())); - assertTrue(LocalDnsHelper.matchesCriteria(RRSET1, RRSET1.getName(), null)); - assertFalse(LocalDnsHelper.matchesCriteria(RRSET1, "anothername", RRSET1.getType())); - } - - @Test - public void testGetUniqueId() { - assertNotNull(LocalDnsHelper.getUniqueId(Lists.newLinkedList())); - } - - @Test - public void testFindProject() { - assertEquals(0, LOCAL_DNS_HELPER.projects().size()); - LocalDnsHelper.ProjectContainer project = LOCAL_DNS_HELPER.findProject(PROJECT_ID1); - assertNotNull(project); - assertTrue(LOCAL_DNS_HELPER.projects().containsKey(PROJECT_ID1)); - assertNotNull(LOCAL_DNS_HELPER.findProject(PROJECT_ID2)); - assertTrue(LOCAL_DNS_HELPER.projects().containsKey(PROJECT_ID2)); - assertTrue(LOCAL_DNS_HELPER.projects().containsKey(PROJECT_ID1)); - assertNotNull(project.zones()); - assertEquals(0, project.zones().size()); - assertNotNull(project.project()); - assertNotNull(project.project().getQuota()); + private static void resetProjects() { + for (String project : LOCAL_DNS_HELPER.projects().keySet()) { + LOCAL_DNS_HELPER.projects().remove(project); + } } - @Test - public void testCreateAndFindZone() { - LocalDnsHelper.ZoneContainer zone1 = LOCAL_DNS_HELPER.findZone(PROJECT_ID1, ZONE_NAME1); - assertTrue(LOCAL_DNS_HELPER.projects().containsKey(PROJECT_ID1)); - assertNull(zone1); - LOCAL_DNS_HELPER.createZone(PROJECT_ID1, ZONE1, null); // we do not care about options - zone1 = LOCAL_DNS_HELPER.findZone(PROJECT_ID1, ZONE1.getName()); - assertNotNull(zone1); - // cannot call equals because id and timestamp got assigned - assertEquals(ZONE_NAME1, zone1.zone().getName()); - assertNotNull(zone1.changes()); - assertTrue(zone1.changes().isEmpty()); - assertNotNull(zone1.dnsRecords()); - assertEquals(2, zone1.dnsRecords().get(ZONE_NAME1).size()); // default SOA and NS - LOCAL_DNS_HELPER.createZone(PROJECT_ID2, ZONE1, null); // project does not exits yet - assertEquals(ZONE1.getName(), - LOCAL_DNS_HELPER.findZone(PROJECT_ID2, ZONE_NAME1).zone().getName()); + private static void assertEqChangesIgnoreStatus(Change expected, Change actual) { + assertEquals(expected.getAdditions(), actual.getAdditions()); + assertEquals(expected.getDeletions(), actual.getDeletions()); + assertEquals(expected.getId(), actual.getId()); + assertEquals(expected.getStartTime(), actual.getStartTime()); } @Test - public void testCreateAndFindZoneUsingRpc() { - // zone does not exist yet - ManagedZone zone1 = RPC.getZone(ZONE_NAME1, EMPTY_RPC_OPTIONS); - assertTrue(LOCAL_DNS_HELPER.projects().containsKey(REAL_PROJECT_ID)); // check internal state - assertNull(zone1); - // create zone - ManagedZone createdZone = RPC.create(ZONE1, EMPTY_RPC_OPTIONS); - assertEquals(ZONE1.getName(), createdZone.getName()); - assertEquals(ZONE1.getDescription(), createdZone.getDescription()); - assertEquals(ZONE1.getDnsName(), createdZone.getDnsName()); - assertEquals(4, createdZone.getNameServers().size()); - // get the same zone zone - ManagedZone zone = RPC.getZone(ZONE1.getName(), EMPTY_RPC_OPTIONS); - assertEquals(createdZone, zone); + public void testCreateZone() { + ManagedZone created = RPC.create(ZONE1, EMPTY_RPC_OPTIONS); // check that default records were created - DnsRpc.ListResult resourceRecordSetListResult + DnsRpc.ListResult listResult = RPC.listDnsRecords(ZONE1.getName(), EMPTY_RPC_OPTIONS); - assertEquals(2, Lists.newLinkedList(resourceRecordSetListResult.results()).size()); - } - - @Test - public void testDeleteZone() { - LOCAL_DNS_HELPER.createZone(PROJECT_ID1, ZONE1, null); - LocalDnsHelper.Response response = LOCAL_DNS_HELPER.deleteZone(PROJECT_ID1, ZONE1.getName()); - assertEquals(204, response.code()); - // deleting non-existent zone - response = LOCAL_DNS_HELPER.deleteZone(PROJECT_ID1, ZONE1.getName()); - assertEquals(404, response.code()); - assertNull(LOCAL_DNS_HELPER.findZone(PROJECT_ID1, ZONE1.getName())); - LOCAL_DNS_HELPER.createZone(PROJECT_ID1, ZONE1, null); - LOCAL_DNS_HELPER.createZone(PROJECT_ID1, ZONE2, null); - assertNotNull(LOCAL_DNS_HELPER.findZone(PROJECT_ID1, ZONE1.getName())); - assertNotNull(LOCAL_DNS_HELPER.findZone(PROJECT_ID1, ZONE2.getName())); - // delete in reverse order - response = LOCAL_DNS_HELPER.deleteZone(PROJECT_ID1, ZONE1.getName()); - assertEquals(204, response.code()); - assertNull(LOCAL_DNS_HELPER.findZone(PROJECT_ID1, ZONE1.getName())); - assertNotNull(LOCAL_DNS_HELPER.findZone(PROJECT_ID1, ZONE2.getName())); - LOCAL_DNS_HELPER.deleteZone(PROJECT_ID1, ZONE2.getName()); - assertEquals(204, response.code()); - assertNull(LOCAL_DNS_HELPER.findZone(PROJECT_ID1, ZONE1.getName())); - assertNull(LOCAL_DNS_HELPER.findZone(PROJECT_ID1, ZONE2.getName())); - } - - @Test - public void testDeleteZoneUsingRpc() { - RPC.create(ZONE1, EMPTY_RPC_OPTIONS); - assertTrue(RPC.deleteZone(ZONE1.getName())); - assertNull(RPC.getZone(ZONE1.getName(), EMPTY_RPC_OPTIONS)); - // deleting non-existent zone - assertFalse(RPC.deleteZone(ZONE1.getName())); - assertNull(RPC.getZone(ZONE1.getName(), EMPTY_RPC_OPTIONS)); - RPC.create(ZONE1, EMPTY_RPC_OPTIONS); - RPC.create(ZONE2, EMPTY_RPC_OPTIONS); - assertNotNull(RPC.getZone(ZONE1.getName(), EMPTY_RPC_OPTIONS)); - assertNotNull(RPC.getZone(ZONE2.getName(), EMPTY_RPC_OPTIONS)); - // delete in reverse order - assertTrue(RPC.deleteZone(ZONE1.getName())); - assertNull(RPC.getZone(ZONE1.getName(), EMPTY_RPC_OPTIONS)); - assertNotNull(RPC.getZone(ZONE2.getName(), EMPTY_RPC_OPTIONS)); - assertTrue(RPC.deleteZone(ZONE2.getName())); - assertNull(RPC.getZone(ZONE1.getName(), EMPTY_RPC_OPTIONS)); - assertNull(RPC.getZone(ZONE2.getName(), EMPTY_RPC_OPTIONS)); - } - - @Test - public void testCreateAndApplyChange() { - LocalDnsHelper localDnsThreaded = LocalDnsHelper.create(5 * 1000L); // using threads here - localDnsThreaded.createZone(PROJECT_ID1, ZONE1, null); - assertNull(localDnsThreaded.findZone(PROJECT_ID1, ZONE_NAME1).findChange("1")); - LocalDnsHelper.Response response - = localDnsThreaded.createChange(PROJECT_ID1, ZONE_NAME1, CHANGE1, null); // add - assertEquals(200, response.code()); - assertNotNull(localDnsThreaded.findZone(PROJECT_ID1, ZONE_NAME1).findChange("1")); - assertNull(localDnsThreaded.findZone(PROJECT_ID1, ZONE_NAME1).findChange("2")); - localDnsThreaded.createChange(PROJECT_ID1, ZONE_NAME1, CHANGE1, null); // add - response = localDnsThreaded.createChange(PROJECT_ID1, ZONE_NAME1, CHANGE1, null); // add - assertEquals(200, response.code()); - assertNotNull(localDnsThreaded.findZone(PROJECT_ID1, ZONE_NAME1).findChange("1")); - assertNotNull(localDnsThreaded.findZone(PROJECT_ID1, ZONE_NAME1).findChange("2")); - localDnsThreaded.createChange(PROJECT_ID1, ZONE_NAME1, CHANGE2, null); // delete - assertNotNull(localDnsThreaded.findZone(PROJECT_ID1, ZONE_NAME1).findChange("1")); - assertNotNull(localDnsThreaded.findZone(PROJECT_ID1, ZONE_NAME1).findChange("2")); - assertNotNull(localDnsThreaded.findZone(PROJECT_ID1, ZONE_NAME1).findChange("3")); - localDnsThreaded.createChange(PROJECT_ID1, ZONE_NAME1, CHANGE_KEEP, null); // id is "4" - // check execution - Change change = localDnsThreaded.findChange(PROJECT_ID1, ZONE_NAME1, "4"); - for (int i = 0; i < 10 && !change.getStatus().equals("done"); i++) { - // change has not been finished yet; wait at most 20 seconds - // it takes 5 seconds for the thread to kick in in the first place - try { - Thread.sleep(2 * 1000); - } catch (InterruptedException e) { - fail("Test was interrupted"); - } + ImmutableList defaultTypes = ImmutableList.of("SOA", "NS"); + Iterator iterator = listResult.results().iterator(); + assertTrue(defaultTypes.contains(iterator.next().getType())); + assertTrue(defaultTypes.contains(iterator.next().getType())); + assertFalse(iterator.hasNext()); + assertEquals(created, LOCAL_DNS_HELPER.findZone(REAL_PROJECT_ID, ZONE1.getName()).zone()); + ManagedZone zone = RPC.getZone(ZONE_NAME1, EMPTY_RPC_OPTIONS); + assertEquals(created, zone); + try { + RPC.create(null, EMPTY_RPC_OPTIONS); + fail("Zone cannot be null"); + } catch (DnsException ex) { + // expected + assertEquals(400, ex.code()); + assertTrue(ex.getMessage().contains("entity.managedZone")); } - assertEquals("done", change.getStatus()); - List list = - localDnsThreaded.findZone(PROJECT_ID1, ZONE_NAME1).dnsRecords().get(ZONE_NAME1); - assertTrue(list.contains(new LocalDnsHelper.RrsetWrapper(RRSET_KEEP))); - localDnsThreaded.stop(); - } - - @Test - public void testCreateAndApplyChangeUsingRpc() { - // not using threads - RPC.create(ZONE1, EMPTY_RPC_OPTIONS); - assertNull(RPC.getChangeRequest(ZONE1.getName(), "1", EMPTY_RPC_OPTIONS)); - //add - Change createdChange = RPC.applyChangeRequest(ZONE1.getName(), CHANGE1, EMPTY_RPC_OPTIONS); - assertEquals(createdChange.getAdditions(), CHANGE1.getAdditions()); - assertEquals(createdChange.getDeletions(), CHANGE1.getDeletions()); - assertNotNull(createdChange.getStartTime()); - assertEquals("1", createdChange.getId()); - Change retrievedChange = RPC.getChangeRequest(ZONE1.getName(), "1", EMPTY_RPC_OPTIONS); - assertEquals(createdChange, retrievedChange); - assertNull(RPC.getChangeRequest(ZONE1.getName(), "2", EMPTY_RPC_OPTIONS)); + // create zone twice try { - Change anotherChange = RPC.applyChangeRequest(ZONE1.getName(), CHANGE1, EMPTY_RPC_OPTIONS); + RPC.create(ZONE1, EMPTY_RPC_OPTIONS); + fail("Zone already exists."); } catch (DnsException ex) { + // expected assertEquals(409, ex.code()); + assertTrue(ex.getMessage().contains("already exists")); } - assertNotNull(RPC.getChangeRequest(ZONE1.getName(), "1", EMPTY_RPC_OPTIONS)); - assertNull(RPC.getChangeRequest(ZONE1.getName(), "2", EMPTY_RPC_OPTIONS)); - // delete - RPC.applyChangeRequest(ZONE1.getName(), CHANGE2, EMPTY_RPC_OPTIONS); - assertNotNull(RPC.getChangeRequest(ZONE1.getName(), "1", EMPTY_RPC_OPTIONS)); - assertNotNull(RPC.getChangeRequest(ZONE1.getName(), "2", EMPTY_RPC_OPTIONS)); - Change last = RPC.applyChangeRequest(ZONE1.getName(), CHANGE_KEEP, EMPTY_RPC_OPTIONS); - assertEquals("done", last.getStatus()); - // todo(mderka) replace with real call - List list = - LOCAL_DNS_HELPER.findZone(REAL_PROJECT_ID, ZONE_NAME1).dnsRecords().get(ZONE_NAME1); - assertTrue(list.contains(new LocalDnsHelper.RrsetWrapper(RRSET_KEEP))); - Iterable results = - RPC.listDnsRecords(ZONE1.getName(), EMPTY_RPC_OPTIONS).results(); - boolean ok = false; - for (ResourceRecordSet dnsRecord : results) { - if (dnsRecord.getName().equals(RRSET_KEEP.getName()) - && dnsRecord.getType().equals(RRSET_KEEP.getType())) { - ok = true; - } - } - assertTrue(ok); - } - - @Test - public void testFindChange() { - LOCAL_DNS_HELPER.createZone(PROJECT_ID1, ZONE1, null); - Change change = LOCAL_DNS_HELPER.findChange(PROJECT_ID1, ZONE1.getName(), "somerandomchange"); - assertNull(change); - LOCAL_DNS_HELPER.createChange(PROJECT_ID1, ZONE1.getName(), CHANGE1, null); - // changes are sequential so we should find ID 1 - assertNotNull(LOCAL_DNS_HELPER.findChange(PROJECT_ID1, ZONE1.getName(), "1")); - // add another - LOCAL_DNS_HELPER.createChange(PROJECT_ID1, ZONE_NAME1, CHANGE2, null); - assertNotNull(LOCAL_DNS_HELPER.findChange(PROJECT_ID1, ZONE1.getName(), "1")); - assertNotNull(LOCAL_DNS_HELPER.findChange(PROJECT_ID1, ZONE1.getName(), "2")); - // try to find non-existent change - assertNull(LOCAL_DNS_HELPER.findChange(PROJECT_ID1, ZONE1.getName(), "3")); - // try to find a change in yet non-existent project - assertNull(LOCAL_DNS_HELPER.findChange(PROJECT_ID2, ZONE1.getName(), "3")); - } - - @Test - public void testRandomNameServers() { - assertEquals(4, LocalDnsHelper.randomNameservers().size()); - assertEquals(4, LocalDnsHelper.randomNameservers().size()); - assertEquals(4, LocalDnsHelper.randomNameservers().size()); - assertEquals(4, LocalDnsHelper.randomNameservers().size()); - } - - @Test - public void testGetProject() { - // only interested in no exceptions and non-null response here - assertNotNull(LOCAL_DNS_HELPER.getProject(PROJECT_ID1, null)); - assertNotNull(LOCAL_DNS_HELPER.getProject(PROJECT_ID2, null)); - Project project = RPC.getProject(EMPTY_RPC_OPTIONS); - assertNotNull(project.getQuota()); - assertEquals(REAL_PROJECT_ID, project.getId()); - // fields options - Map options = new HashMap<>(); - options.put(DnsRpc.Option.FIELDS, "number"); - project = RPC.getProject(options); - assertNull(project.getId()); - assertNotNull(project.getNumber()); - assertNull(project.getQuota()); - options.put(DnsRpc.Option.FIELDS, "id"); - project = RPC.getProject(options); - assertNotNull(project.getId()); - assertNull(project.getNumber()); - assertNull(project.getQuota()); - options.put(DnsRpc.Option.FIELDS, "quota"); - project = RPC.getProject(options); - assertNull(project.getId()); - assertNull(project.getNumber()); - assertNotNull(project.getQuota()); - } - - @Test - public void testGetZone() { - // non-existent - LocalDnsHelper.Response response = LOCAL_DNS_HELPER.getZone(PROJECT_ID1, ZONE_NAME1, null); - assertEquals(404, response.code()); - assertTrue(response.body().contains("does not exist")); - // existent - LOCAL_DNS_HELPER.createZone(PROJECT_ID1, ZONE1, null); - response = LOCAL_DNS_HELPER.getZone(PROJECT_ID1, ZONE1.getName(), null); - assertEquals(200, response.code()); - } - - @Test - public void testGetZoneUsingRpc() { - // non-existent - assertNull(RPC.getZone(ZONE_NAME1, EMPTY_RPC_OPTIONS)); - // existent - ManagedZone created = RPC.create(ZONE1, EMPTY_RPC_OPTIONS); - ManagedZone zone = RPC.getZone(ZONE_NAME1, EMPTY_RPC_OPTIONS); - assertEquals(created, zone); - assertEquals(ZONE1.getName(), zone.getName()); // field options + resetProjects(); Map options = new HashMap<>(); options.put(DnsRpc.Option.FIELDS, "id"); - zone = RPC.getZone(ZONE1.getName(), options); + zone = RPC.create(ZONE1, options); assertNull(zone.getCreationTime()); assertNull(zone.getName()); assertNull(zone.getDnsName()); @@ -396,8 +169,9 @@ public void testGetZoneUsingRpc() { assertNull(zone.getNameServers()); assertNull(zone.getNameServerSet()); assertNotNull(zone.getId()); + resetProjects(); options.put(DnsRpc.Option.FIELDS, "creationTime"); - zone = RPC.getZone(ZONE1.getName(), options); + zone = RPC.create(ZONE1, options); assertNotNull(zone.getCreationTime()); assertNull(zone.getName()); assertNull(zone.getDnsName()); @@ -406,7 +180,8 @@ public void testGetZoneUsingRpc() { assertNull(zone.getNameServerSet()); assertNull(zone.getId()); options.put(DnsRpc.Option.FIELDS, "dnsName"); - zone = RPC.getZone(ZONE1.getName(), options); + resetProjects(); + zone = RPC.create(ZONE1, options); assertNull(zone.getCreationTime()); assertNull(zone.getName()); assertNotNull(zone.getDnsName()); @@ -415,7 +190,8 @@ public void testGetZoneUsingRpc() { assertNull(zone.getNameServerSet()); assertNull(zone.getId()); options.put(DnsRpc.Option.FIELDS, "description"); - zone = RPC.getZone(ZONE1.getName(), options); + resetProjects(); + zone = RPC.create(ZONE1, options); assertNull(zone.getCreationTime()); assertNull(zone.getName()); assertNull(zone.getDnsName()); @@ -424,7 +200,8 @@ public void testGetZoneUsingRpc() { assertNull(zone.getNameServerSet()); assertNull(zone.getId()); options.put(DnsRpc.Option.FIELDS, "nameServers"); - zone = RPC.getZone(ZONE1.getName(), options); + resetProjects(); + zone = RPC.create(ZONE1, options); assertNull(zone.getCreationTime()); assertNull(zone.getName()); assertNull(zone.getDnsName()); @@ -433,7 +210,8 @@ public void testGetZoneUsingRpc() { assertNull(zone.getNameServerSet()); assertNull(zone.getId()); options.put(DnsRpc.Option.FIELDS, "nameServerSet"); - zone = RPC.getZone(ZONE1.getName(), options); + resetProjects(); + zone = RPC.create(ZONE1, options); assertNull(zone.getCreationTime()); assertNull(zone.getName()); assertNull(zone.getDnsName()); @@ -443,7 +221,8 @@ public void testGetZoneUsingRpc() { assertNull(zone.getId()); // several combined options.put(DnsRpc.Option.FIELDS, "nameServerSet,description,id,name"); - zone = RPC.getZone(ZONE1.getName(), options); + resetProjects(); + zone = RPC.create(ZONE1, options); assertNull(zone.getCreationTime()); assertNotNull(zone.getName()); assertNull(zone.getDnsName()); @@ -454,50 +233,18 @@ public void testGetZoneUsingRpc() { } @Test - public void testCreateZone() { - // only interested in no exceptions and non-null response here - LocalDnsHelper.Response response = LOCAL_DNS_HELPER.createZone(PROJECT_ID1, ZONE1, null); - assertEquals(200, response.code()); - assertEquals(1, LOCAL_DNS_HELPER.projects().get(PROJECT_ID1).zones().size()); - try { - LOCAL_DNS_HELPER.createZone(PROJECT_ID1, null, null); - fail("Zone cannot be null"); - } catch (NullPointerException ex) { - // expected - } - // create zone twice - response = LOCAL_DNS_HELPER.createZone(PROJECT_ID1, ZONE1, null); - assertEquals(409, response.code()); - assertTrue(response.body().contains("already exists")); - } - - @Test - public void testCreateZoneUsingRpc() { + public void testGetZone() { + // non-existent + assertNull(RPC.getZone(ZONE_NAME1, EMPTY_RPC_OPTIONS)); + // existent ManagedZone created = RPC.create(ZONE1, EMPTY_RPC_OPTIONS); - assertEquals(created, LOCAL_DNS_HELPER.findZone(REAL_PROJECT_ID, ZONE1.getName()).zone()); ManagedZone zone = RPC.getZone(ZONE_NAME1, EMPTY_RPC_OPTIONS); assertEquals(created, zone); - try { - RPC.create(null, EMPTY_RPC_OPTIONS); - fail("Zone cannot be null"); - } catch (DnsException ex) { - // expected - assertEquals(400, ex.code()); - assertTrue(ex.getMessage().contains("entity.managedZone")); - } - // create zone twice - try { - RPC.create(ZONE1, EMPTY_RPC_OPTIONS); - } catch (DnsException ex) { - // expected - assertEquals(409, ex.code()); - assertTrue(ex.getMessage().contains("already exists")); - } + assertEquals(ZONE1.getName(), zone.getName()); // field options - resetProjects(); Map options = new HashMap<>(); options.put(DnsRpc.Option.FIELDS, "id"); - zone = RPC.create(ZONE1, options); + zone = RPC.getZone(ZONE1.getName(), options); assertNull(zone.getCreationTime()); assertNull(zone.getName()); assertNull(zone.getDnsName()); @@ -505,9 +252,8 @@ public void testCreateZoneUsingRpc() { assertNull(zone.getNameServers()); assertNull(zone.getNameServerSet()); assertNotNull(zone.getId()); - resetProjects(); options.put(DnsRpc.Option.FIELDS, "creationTime"); - zone = RPC.create(ZONE1, options); + zone = RPC.getZone(ZONE1.getName(), options); assertNotNull(zone.getCreationTime()); assertNull(zone.getName()); assertNull(zone.getDnsName()); @@ -516,8 +262,7 @@ public void testCreateZoneUsingRpc() { assertNull(zone.getNameServerSet()); assertNull(zone.getId()); options.put(DnsRpc.Option.FIELDS, "dnsName"); - resetProjects(); - zone = RPC.create(ZONE1, options); + zone = RPC.getZone(ZONE1.getName(), options); assertNull(zone.getCreationTime()); assertNull(zone.getName()); assertNotNull(zone.getDnsName()); @@ -526,8 +271,7 @@ public void testCreateZoneUsingRpc() { assertNull(zone.getNameServerSet()); assertNull(zone.getId()); options.put(DnsRpc.Option.FIELDS, "description"); - resetProjects(); - zone = RPC.create(ZONE1, options); + zone = RPC.getZone(ZONE1.getName(), options); assertNull(zone.getCreationTime()); assertNull(zone.getName()); assertNull(zone.getDnsName()); @@ -536,8 +280,7 @@ public void testCreateZoneUsingRpc() { assertNull(zone.getNameServerSet()); assertNull(zone.getId()); options.put(DnsRpc.Option.FIELDS, "nameServers"); - resetProjects(); - zone = RPC.create(ZONE1, options); + zone = RPC.getZone(ZONE1.getName(), options); assertNull(zone.getCreationTime()); assertNull(zone.getName()); assertNull(zone.getDnsName()); @@ -546,8 +289,7 @@ public void testCreateZoneUsingRpc() { assertNull(zone.getNameServerSet()); assertNull(zone.getId()); options.put(DnsRpc.Option.FIELDS, "nameServerSet"); - resetProjects(); - zone = RPC.create(ZONE1, options); + zone = RPC.getZone(ZONE1.getName(), options); assertNull(zone.getCreationTime()); assertNull(zone.getName()); assertNull(zone.getDnsName()); @@ -557,8 +299,7 @@ public void testCreateZoneUsingRpc() { assertNull(zone.getId()); // several combined options.put(DnsRpc.Option.FIELDS, "nameServerSet,description,id,name"); - resetProjects(); - zone = RPC.create(ZONE1, options); + zone = RPC.getZone(ZONE1.getName(), options); assertNull(zone.getCreationTime()); assertNotNull(zone.getName()); assertNull(zone.getDnsName()); @@ -568,25 +309,144 @@ public void testCreateZoneUsingRpc() { assertNotNull(zone.getId()); } + @Test + public void testDeleteZone() { + RPC.create(ZONE1, EMPTY_RPC_OPTIONS); + assertTrue(RPC.deleteZone(ZONE1.getName())); + assertNull(RPC.getZone(ZONE1.getName(), EMPTY_RPC_OPTIONS)); + // deleting non-existent zone + assertFalse(RPC.deleteZone(ZONE1.getName())); + assertNull(RPC.getZone(ZONE1.getName(), EMPTY_RPC_OPTIONS)); + RPC.create(ZONE1, EMPTY_RPC_OPTIONS); + RPC.create(ZONE2, EMPTY_RPC_OPTIONS); + assertNotNull(RPC.getZone(ZONE1.getName(), EMPTY_RPC_OPTIONS)); + assertNotNull(RPC.getZone(ZONE2.getName(), EMPTY_RPC_OPTIONS)); + // delete in reverse order + assertTrue(RPC.deleteZone(ZONE1.getName())); + assertNull(RPC.getZone(ZONE1.getName(), EMPTY_RPC_OPTIONS)); + assertNotNull(RPC.getZone(ZONE2.getName(), EMPTY_RPC_OPTIONS)); + assertTrue(RPC.deleteZone(ZONE2.getName())); + assertNull(RPC.getZone(ZONE1.getName(), EMPTY_RPC_OPTIONS)); + assertNull(RPC.getZone(ZONE2.getName(), EMPTY_RPC_OPTIONS)); + RPC.create(ZONE1, EMPTY_RPC_OPTIONS); + RPC.applyChangeRequest(ZONE1.getName(), CHANGE_KEEP, EMPTY_RPC_OPTIONS); + try { + RPC.deleteZone(ZONE1.getName()); + fail(); + } catch (DnsException ex) { + // expected + assertEquals(400, ex.code()); + assertTrue(ex.getMessage().contains("not empty")); + } + } + + @Test + public void testCreateAndApplyChange() { + executeCreateAndApplyChangeTest(RPC); + } + + @Test + public void testCreateAndApplyChangeWithThreads() { + LocalDnsHelper localDnsThreaded = LocalDnsHelper.create(50L); + localDnsThreaded.start(); + DnsRpc rpc = new DefaultDnsRpc(localDnsThreaded.options()); + executeCreateAndApplyChangeTest(rpc); + localDnsThreaded.stop(); + } + + private static void waitForChangeToComplete(DnsRpc rpc, String zoneName, String changeId) { + while (true) { + Change change = rpc.getChangeRequest(zoneName, changeId, EMPTY_RPC_OPTIONS); + if ("done".equals(change.getStatus())) { + return; + } + try { + Thread.sleep(50L); + } catch (InterruptedException e) { + fail("Thread was interrupted while waiting for change processing."); + } + } + } + + private static void executeCreateAndApplyChangeTest(DnsRpc rpc) { + rpc.create(ZONE1, EMPTY_RPC_OPTIONS); + assertNull(rpc.getChangeRequest(ZONE1.getName(), "1", EMPTY_RPC_OPTIONS)); + // add + Change createdChange = rpc.applyChangeRequest(ZONE1.getName(), CHANGE1, EMPTY_RPC_OPTIONS); + assertEquals(CHANGE1.getAdditions(), createdChange.getAdditions()); + assertEquals(CHANGE1.getDeletions(), createdChange.getDeletions()); + assertNotNull(createdChange.getStartTime()); + assertEquals("1", createdChange.getId()); + waitForChangeToComplete(rpc, ZONE1.getName(), "1"); // necessary for the following to return 409 + try { + rpc.applyChangeRequest(ZONE1.getName(), CHANGE1, EMPTY_RPC_OPTIONS); + fail(); + } catch (DnsException ex) { + assertEquals(409, ex.code()); + assertTrue(ex.getMessage().contains("already exists")); + } + assertNotNull(rpc.getChangeRequest(ZONE1.getName(), "1", EMPTY_RPC_OPTIONS)); + assertNull(rpc.getChangeRequest(ZONE1.getName(), "2", EMPTY_RPC_OPTIONS)); + // delete + rpc.applyChangeRequest(ZONE1.getName(), CHANGE2, EMPTY_RPC_OPTIONS); + assertNotNull(rpc.getChangeRequest(ZONE1.getName(), "1", EMPTY_RPC_OPTIONS)); + assertNotNull(rpc.getChangeRequest(ZONE1.getName(), "2", EMPTY_RPC_OPTIONS)); + waitForChangeToComplete(rpc, ZONE1.getName(), "2"); + rpc.applyChangeRequest(ZONE1.getName(), CHANGE_KEEP, EMPTY_RPC_OPTIONS); + waitForChangeToComplete(rpc, ZONE1.getName(), "3"); + Iterable results = + rpc.listDnsRecords(ZONE1.getName(), EMPTY_RPC_OPTIONS).results(); + List defaults = ImmutableList.of("SOA", "NS"); + boolean rrsetKeep = false; + boolean rrset1 = false; + for (ResourceRecordSet dnsRecord : results) { + if (dnsRecord.getName().equals(RRSET_KEEP.getName()) + && dnsRecord.getType().equals(RRSET_KEEP.getType())) { + rrsetKeep = true; + } else if (dnsRecord.getName().equals(RRSET1.getName()) + && dnsRecord.getType().equals(RRSET1.getType())) { + rrset1 = true; + } else if (!defaults.contains(dnsRecord.getType())) { + fail(String.format("Record with type %s should not exist", dnsRecord.getType())); + } + } + assertTrue(rrset1); + assertTrue(rrsetKeep); + } + + @Test + public void testGetProject() { + // the projects are automatically created when getProject is called + assertNotNull(LOCAL_DNS_HELPER.getProject(PROJECT_ID1, null)); + assertNotNull(LOCAL_DNS_HELPER.getProject(PROJECT_ID2, null)); + Project project = RPC.getProject(EMPTY_RPC_OPTIONS); + assertNotNull(project.getQuota()); + assertEquals(REAL_PROJECT_ID, project.getId()); + // fields options + Map options = new HashMap<>(); + options.put(DnsRpc.Option.FIELDS, "number"); + project = RPC.getProject(options); + assertNull(project.getId()); + assertNotNull(project.getNumber()); + assertNull(project.getQuota()); + options.put(DnsRpc.Option.FIELDS, "id"); + project = RPC.getProject(options); + assertNotNull(project.getId()); + assertNull(project.getNumber()); + assertNull(project.getQuota()); + options.put(DnsRpc.Option.FIELDS, "quota"); + project = RPC.getProject(options); + assertNull(project.getId()); + assertNull(project.getNumber()); + assertNotNull(project.getQuota()); + } + @Test public void testCreateChange() { - // non-existent zone - LocalDnsHelper.Response response = - LOCAL_DNS_HELPER.createChange(PROJECT_ID1, ZONE_NAME1, CHANGE1, null); - assertEquals(404, response.code()); - // existent zone - assertNotNull(LOCAL_DNS_HELPER.createZone(PROJECT_ID1, ZONE1, null)); - assertNull(LOCAL_DNS_HELPER.findChange(PROJECT_ID1, ZONE_NAME1, "1")); - response = LOCAL_DNS_HELPER.createChange(PROJECT_ID1, ZONE_NAME1, CHANGE1, null); - assertEquals(200, response.code()); - assertNotNull(LOCAL_DNS_HELPER.findChange(PROJECT_ID1, ZONE_NAME1, "1")); - } - - @Test - public void testCreateChangeUsingRpc() { // non-existent zone try { RPC.applyChangeRequest(ZONE_NAME1, CHANGE1, EMPTY_RPC_OPTIONS); + fail("Zone was not created yet."); } catch (DnsException ex) { assertEquals(404, ex.code()); } @@ -637,23 +497,6 @@ public void testCreateChangeUsingRpc() { @Test public void testGetChange() { - // existent - assertEquals(200, LOCAL_DNS_HELPER.createZone(PROJECT_ID1, ZONE1, null).code()); - assertEquals(200, LOCAL_DNS_HELPER.createChange(PROJECT_ID1, ZONE_NAME1, CHANGE1, null).code()); - assertEquals(200, LOCAL_DNS_HELPER.getChange(PROJECT_ID1, ZONE_NAME1, "1", null).code()); - // non-existent - LocalDnsHelper.Response response = - LOCAL_DNS_HELPER.getChange(PROJECT_ID1, ZONE_NAME1, "2", null); - assertEquals(404, response.code()); - assertTrue(response.body().contains("parameters.changeId")); - // non-existent zone - response = LOCAL_DNS_HELPER.getChange(PROJECT_ID1, ZONE_NAME2, "1", null); - assertEquals(404, response.code()); - assertTrue(response.body().contains("parameters.managedZone")); - } - - @Test - public void testGetChangeUsingRpc() { // existent RPC.create(ZONE1, EMPTY_RPC_OPTIONS); Change created = RPC.applyChangeRequest(ZONE1.getName(), CHANGE1, EMPTY_RPC_OPTIONS); @@ -664,9 +507,11 @@ public void testGetChangeUsingRpc() { // non-existent zone try { RPC.getChangeRequest(ZONE_NAME2, "1", EMPTY_RPC_OPTIONS); + fail(); } catch (DnsException ex) { // expected assertEquals(404, ex.code()); + assertTrue(ex.getMessage().contains("managedZone")); } // field options RPC.applyChangeRequest(ZONE1.getName(), CHANGE_KEEP, EMPTY_RPC_OPTIONS); @@ -711,43 +556,6 @@ public void testGetChangeUsingRpc() { @Test public void testListZones() { - // only interested in no exceptions and non-null response here - optionsMap.put("dnsName", null); - optionsMap.put("fields", null); - optionsMap.put("pageToken", null); - optionsMap.put("maxResults", null); - LocalDnsHelper.Response response = LOCAL_DNS_HELPER.listZones(PROJECT_ID1, optionsMap); - assertEquals(200, response.code()); - // some zones exists - LOCAL_DNS_HELPER.createZone(PROJECT_ID1, ZONE1, null); - response = LOCAL_DNS_HELPER.listZones(PROJECT_ID1, optionsMap); - assertEquals(200, response.code()); - LOCAL_DNS_HELPER.createZone(PROJECT_ID1, ZONE2, null); - response = LOCAL_DNS_HELPER.listZones(PROJECT_ID1, optionsMap); - assertEquals(200, response.code()); - // error in options - optionsMap.put("maxResults", "aaa"); - response = LOCAL_DNS_HELPER.listZones(PROJECT_ID1, optionsMap); - assertEquals(400, response.code()); - optionsMap.put("maxResults", "0"); - response = LOCAL_DNS_HELPER.listZones(PROJECT_ID1, optionsMap); - assertEquals(400, response.code()); - optionsMap.put("maxResults", "-1"); - response = LOCAL_DNS_HELPER.listZones(PROJECT_ID1, optionsMap); - assertEquals(400, response.code()); - optionsMap.put("maxResults", "15"); - response = LOCAL_DNS_HELPER.listZones(PROJECT_ID1, optionsMap); - assertEquals(200, response.code()); - optionsMap.put("dnsName", "aaa"); - response = LOCAL_DNS_HELPER.listZones(PROJECT_ID1, optionsMap); - assertEquals(400, response.code()); - optionsMap.put("dnsName", "aaa."); - response = LOCAL_DNS_HELPER.listZones(PROJECT_ID1, optionsMap); - assertEquals(200, response.code()); - } - - @Test - public void testListZonesUsingRpc() { Iterable results = RPC.listZones(EMPTY_RPC_OPTIONS).results(); ImmutableList zones = ImmutableList.copyOf(results); assertEquals(0, zones.size()); @@ -767,32 +575,38 @@ public void testListZonesUsingRpc() { options.put(DnsRpc.Option.PAGE_SIZE, 0); try { RPC.listZones(options); + fail(); } catch (DnsException ex) { // expected assertEquals(400, ex.code()); + assertTrue(ex.getMessage().contains("parameters.maxResults")); } options = new HashMap<>(); options.put(DnsRpc.Option.PAGE_SIZE, -1); try { RPC.listZones(options); + fail(); } catch (DnsException ex) { // expected assertEquals(400, ex.code()); + assertTrue(ex.getMessage().contains("parameters.maxResults")); } // ok size options = new HashMap<>(); - options.put(DnsRpc.Option.PAGE_SIZE, 1); + options.put(DnsRpc.Option.PAGE_SIZE, 335); results = RPC.listZones(options).results(); zones = ImmutableList.copyOf(results); - assertEquals(1, zones.size()); + assertEquals(2, zones.size()); // dns name problems options = new HashMap<>(); options.put(DnsRpc.Option.DNS_NAME, "aaa"); try { RPC.listZones(options); + fail(); } catch (DnsException ex) { // expected assertEquals(400, ex.code()); + assertTrue(ex.getMessage().contains("parameters.dnsName")); } // ok name options = new HashMap<>(); @@ -848,9 +662,9 @@ public void testListZonesUsingRpc() { assertNull(zone.getNameServerSet()); assertNull(zone.getId()); options.put(DnsRpc.Option.FIELDS, "managedZones(nameServerSet)"); - DnsRpc.ListResult managedZoneListResult = RPC.listZones(options); - zone = managedZoneListResult.results().iterator().next(); - assertNull(managedZoneListResult.pageToken()); + DnsRpc.ListResult listResult = RPC.listZones(options); + zone = listResult.results().iterator().next(); + assertNull(listResult.pageToken()); assertNull(zone.getCreationTime()); assertNull(zone.getName()); assertNull(zone.getDnsName()); @@ -862,8 +676,8 @@ public void testListZonesUsingRpc() { options.put(DnsRpc.Option.FIELDS, "managedZones(nameServerSet,description,id,name),nextPageToken"); options.put(DnsRpc.Option.PAGE_SIZE, 1); - managedZoneListResult = RPC.listZones(options); - zone = managedZoneListResult.results().iterator().next(); + listResult = RPC.listZones(options); + zone = listResult.results().iterator().next(); assertNull(zone.getCreationTime()); assertNotNull(zone.getName()); assertNull(zone.getDnsName()); @@ -871,80 +685,19 @@ public void testListZonesUsingRpc() { assertNull(zone.getNameServers()); assertNotNull(zone.getNameServerSet()); assertNotNull(zone.getId()); - assertEquals(zone.getName(), managedZoneListResult.pageToken()); - // paging - options = new HashMap<>(); - options.put(DnsRpc.Option.PAGE_SIZE, 1); - managedZoneListResult = RPC.listZones(options); - ImmutableList page1 = ImmutableList.copyOf(managedZoneListResult.results()); - assertEquals(1, page1.size()); - options.put(DnsRpc.Option.PAGE_TOKEN, managedZoneListResult.pageToken()); - managedZoneListResult = RPC.listZones(options); - ImmutableList page2 = ImmutableList.copyOf(managedZoneListResult.results()); - assertEquals(1, page2.size()); - assertNotEquals(page1.get(0), page2.get(0)); + assertEquals(zone.getName(), listResult.pageToken()); } @Test public void testListDnsRecords() { - // only interested in no exceptions and non-null response here - optionsMap.put("name", null); - optionsMap.put("fields", null); - optionsMap.put("type", null); - optionsMap.put("pageToken", null); - optionsMap.put("maxResults", null); - // no zone exists - LocalDnsHelper.Response response = LOCAL_DNS_HELPER.listDnsRecords(PROJECT_ID1, ZONE_NAME1, - optionsMap); - assertEquals(404, response.code()); - // zone exists but has no records - LOCAL_DNS_HELPER.createZone(PROJECT_ID1, ZONE1, null); - LOCAL_DNS_HELPER.listDnsRecords(PROJECT_ID1, ZONE_NAME1, optionsMap); - // zone has records - LOCAL_DNS_HELPER.createChange(PROJECT_ID1, ZONE_NAME1, CHANGE1, null); - response = LOCAL_DNS_HELPER.listDnsRecords(PROJECT_ID1, ZONE_NAME1, optionsMap); - assertEquals(200, response.code()); - // error in options - optionsMap.put("maxResults", "aaa"); - response = LOCAL_DNS_HELPER.listZones(PROJECT_ID1, optionsMap); - assertEquals(400, response.code()); - optionsMap.put("maxResults", "0"); - response = LOCAL_DNS_HELPER.listZones(PROJECT_ID1, optionsMap); - assertEquals(400, response.code()); - optionsMap.put("maxResults", "-1"); - response = LOCAL_DNS_HELPER.listZones(PROJECT_ID1, optionsMap); - assertEquals(400, response.code()); - optionsMap.put("maxResults", "15"); - response = LOCAL_DNS_HELPER.listZones(PROJECT_ID1, optionsMap); - assertEquals(200, response.code()); - optionsMap.put("name", "aaa"); - response = LOCAL_DNS_HELPER.listZones(PROJECT_ID1, optionsMap); - assertEquals(400, response.code()); - optionsMap.put("name", "aaa."); - response = LOCAL_DNS_HELPER.listZones(PROJECT_ID1, optionsMap); - assertEquals(200, response.code()); - optionsMap.put("name", null); - optionsMap.put("type", "A"); - response = LOCAL_DNS_HELPER.listZones(PROJECT_ID1, optionsMap); - assertEquals(400, response.code()); - optionsMap.put("name", "aaa."); - optionsMap.put("type", "a"); - response = LOCAL_DNS_HELPER.listZones(PROJECT_ID1, optionsMap); - assertEquals(400, response.code()); - optionsMap.put("name", "aaaa."); - optionsMap.put("type", "A"); - response = LOCAL_DNS_HELPER.listZones(PROJECT_ID1, optionsMap); - assertEquals(200, response.code()); - } - - @Test - public void testListDnsRecordsUsingRpc() { // no zone exists try { RPC.listDnsRecords(ZONE_NAME1, EMPTY_RPC_OPTIONS); + fail(); } catch (DnsException ex) { // expected assertEquals(404, ex.code()); + assertTrue(ex.getMessage().contains("managedZone")); } // zone exists but has no records RPC.create(ZONE1, EMPTY_RPC_OPTIONS); @@ -962,34 +715,35 @@ public void testListDnsRecordsUsingRpc() { options.put(DnsRpc.Option.PAGE_SIZE, 0); try { RPC.listDnsRecords(ZONE1.getName(), options); + fail(); } catch (DnsException ex) { // expected assertEquals(400, ex.code()); + assertTrue(ex.getMessage().contains("parameters.maxResults")); } options.put(DnsRpc.Option.PAGE_SIZE, -1); try { RPC.listDnsRecords(ZONE1.getName(), options); + fail(); } catch (DnsException ex) { // expected assertEquals(400, ex.code()); + assertTrue(ex.getMessage().contains("parameters.maxResults")); } - options.put(DnsRpc.Option.PAGE_SIZE, 1); - results = RPC.listDnsRecords(ZONE1.getName(), options).results(); - records = ImmutableList.copyOf(results); - assertEquals(1, records.size()); options.put(DnsRpc.Option.PAGE_SIZE, 15); results = RPC.listDnsRecords(ZONE1.getName(), options).results(); records = ImmutableList.copyOf(results); assertEquals(3, records.size()); - // dnsName filter options = new HashMap<>(); options.put(DnsRpc.Option.NAME, "aaa"); try { RPC.listDnsRecords(ZONE1.getName(), options); + fail(); } catch (DnsException ex) { // expected assertEquals(400, ex.code()); + assertTrue(ex.getMessage().contains("parameters.name")); } options.put(DnsRpc.Option.NAME, "aaa."); results = RPC.listDnsRecords(ZONE1.getName(), options).results(); @@ -999,17 +753,21 @@ public void testListDnsRecordsUsingRpc() { options.put(DnsRpc.Option.DNS_TYPE, "A"); try { RPC.listDnsRecords(ZONE1.getName(), options); + fail(); } catch (DnsException ex) { // expected assertEquals(400, ex.code()); + assertTrue(ex.getMessage().contains("parameters.name")); } options.put(DnsRpc.Option.NAME, "aaa."); options.put(DnsRpc.Option.DNS_TYPE, "a"); try { RPC.listDnsRecords(ZONE1.getName(), options); + fail(); } catch (DnsException ex) { // expected assertEquals(400, ex.code()); + assertTrue(ex.getMessage().contains("parameters.type")); } options.put(DnsRpc.Option.NAME, DNS_NAME); options.put(DnsRpc.Option.DNS_TYPE, "SOA"); @@ -1019,137 +777,74 @@ public void testListDnsRecordsUsingRpc() { // field options options = new HashMap<>(); options.put(DnsRpc.Option.FIELDS, "rrsets(name)"); - DnsRpc.ListResult resourceRecordSetListResult = + DnsRpc.ListResult listResult = RPC.listDnsRecords(ZONE1.getName(), options); - records = ImmutableList.copyOf(resourceRecordSetListResult.results()); + records = ImmutableList.copyOf(listResult.results()); ResourceRecordSet record = records.get(0); assertNotNull(record.getName()); assertNull(record.getRrdatas()); assertNull(record.getType()); assertNull(record.getTtl()); - assertNull(resourceRecordSetListResult.pageToken()); + assertNull(listResult.pageToken()); options.put(DnsRpc.Option.FIELDS, "rrsets(rrdatas)"); - resourceRecordSetListResult = RPC.listDnsRecords(ZONE1.getName(), options); - records = ImmutableList.copyOf(resourceRecordSetListResult.results()); + listResult = RPC.listDnsRecords(ZONE1.getName(), options); + records = ImmutableList.copyOf(listResult.results()); record = records.get(0); assertNull(record.getName()); assertNotNull(record.getRrdatas()); assertNull(record.getType()); assertNull(record.getTtl()); - assertNull(resourceRecordSetListResult.pageToken()); + assertNull(listResult.pageToken()); options.put(DnsRpc.Option.FIELDS, "rrsets(ttl)"); - resourceRecordSetListResult = RPC.listDnsRecords(ZONE1.getName(), options); - records = ImmutableList.copyOf(resourceRecordSetListResult.results()); + listResult = RPC.listDnsRecords(ZONE1.getName(), options); + records = ImmutableList.copyOf(listResult.results()); record = records.get(0); assertNull(record.getName()); assertNull(record.getRrdatas()); assertNull(record.getType()); assertNotNull(record.getTtl()); - assertNull(resourceRecordSetListResult.pageToken()); + assertNull(listResult.pageToken()); options.put(DnsRpc.Option.FIELDS, "rrsets(type)"); - resourceRecordSetListResult = RPC.listDnsRecords(ZONE1.getName(), options); - records = ImmutableList.copyOf(resourceRecordSetListResult.results()); + listResult = RPC.listDnsRecords(ZONE1.getName(), options); + records = ImmutableList.copyOf(listResult.results()); record = records.get(0); assertNull(record.getName()); assertNull(record.getRrdatas()); assertNotNull(record.getType()); assertNull(record.getTtl()); - assertNull(resourceRecordSetListResult.pageToken()); + assertNull(listResult.pageToken()); options.put(DnsRpc.Option.FIELDS, "nextPageToken"); - resourceRecordSetListResult = RPC.listDnsRecords(ZONE1.getName(), options); - records = ImmutableList.copyOf(resourceRecordSetListResult.results()); + listResult = RPC.listDnsRecords(ZONE1.getName(), options); + records = ImmutableList.copyOf(listResult.results()); record = records.get(0); assertNull(record.getName()); assertNull(record.getRrdatas()); assertNull(record.getType()); assertNull(record.getTtl()); - assertNull(resourceRecordSetListResult.pageToken()); + assertNull(listResult.pageToken()); options.put(DnsRpc.Option.FIELDS, "nextPageToken,rrsets(name,rrdatas)"); options.put(DnsRpc.Option.PAGE_SIZE, 1); - resourceRecordSetListResult = RPC.listDnsRecords(ZONE1.getName(), options); - records = ImmutableList.copyOf(resourceRecordSetListResult.results()); + listResult = RPC.listDnsRecords(ZONE1.getName(), options); + records = ImmutableList.copyOf(listResult.results()); assertEquals(1, records.size()); record = records.get(0); assertNotNull(record.getName()); assertNotNull(record.getRrdatas()); assertNull(record.getType()); assertNull(record.getTtl()); - assertNotNull(resourceRecordSetListResult.pageToken()); - // paging - options.put(DnsRpc.Option.PAGE_TOKEN, resourceRecordSetListResult.pageToken()); - resourceRecordSetListResult = RPC.listDnsRecords(ZONE1.getName(), options); - records = ImmutableList.copyOf(resourceRecordSetListResult.results()); - assertEquals(1, records.size()); - ResourceRecordSet nextRecord = records.get(0); - assertNotEquals(record, nextRecord); + assertNotNull(listResult.pageToken()); } @Test public void testListChanges() { - optionsMap.put("sortBy", null); - optionsMap.put("sortOrder", null); - optionsMap.put("fields", null); - optionsMap.put("pageToken", null); - optionsMap.put("maxResults", null); - // no such zone exists - LocalDnsHelper.Response response = - LOCAL_DNS_HELPER.listChanges(PROJECT_ID1, ZONE_NAME1, optionsMap); - assertEquals(404, response.code()); - assertTrue(response.body().contains("managedZone")); - // zone exists but has no changes - LOCAL_DNS_HELPER.createZone(PROJECT_ID1, ZONE1, null); - assertNotNull(LOCAL_DNS_HELPER.listChanges(PROJECT_ID1, ZONE_NAME1, optionsMap)); - // zone has changes - LOCAL_DNS_HELPER.createChange(PROJECT_ID1, ZONE_NAME1, CHANGE1, null); - assertNotNull(LOCAL_DNS_HELPER.listChanges(PROJECT_ID1, ZONE_NAME1, optionsMap)); - LOCAL_DNS_HELPER.createChange(PROJECT_ID1, ZONE_NAME1, CHANGE1, null); - LOCAL_DNS_HELPER.createChange(PROJECT_ID1, ZONE_NAME1, CHANGE2, null); - LOCAL_DNS_HELPER.createChange(PROJECT_ID1, ZONE_NAME1, CHANGE2, null); - assertNotNull(LOCAL_DNS_HELPER.listChanges(PROJECT_ID1, ZONE_NAME1, optionsMap)); - // error in options - optionsMap.put("maxResults", "aaa"); - response = LOCAL_DNS_HELPER.listChanges(PROJECT_ID1, ZONE_NAME1, optionsMap); - assertEquals(400, response.code()); - optionsMap.put("maxResults", "0"); - response = LOCAL_DNS_HELPER.listChanges(PROJECT_ID1, ZONE_NAME1, optionsMap); - assertEquals(400, response.code()); - optionsMap.put("maxResults", "-1"); - response = LOCAL_DNS_HELPER.listChanges(PROJECT_ID1, ZONE_NAME1, optionsMap); - assertEquals(400, response.code()); - optionsMap.put("maxResults", "15"); - response = LOCAL_DNS_HELPER.listChanges(PROJECT_ID1, ZONE_NAME1, optionsMap); - assertEquals(200, response.code()); - optionsMap.put("sortBy", "changeSequence"); - response = LOCAL_DNS_HELPER.listChanges(PROJECT_ID1, ZONE_NAME1, optionsMap); - assertEquals(200, response.code()); - optionsMap.put("sortBy", "something else"); - response = LOCAL_DNS_HELPER.listChanges(PROJECT_ID1, ZONE_NAME1, optionsMap); - assertEquals(400, response.code()); - assertTrue(response.body().contains("Allowed values: [changesequence]")); - optionsMap.put("sortBy", "ChAnGeSeQuEnCe"); // is not case sensitive - response = LOCAL_DNS_HELPER.listChanges(PROJECT_ID1, ZONE_NAME1, optionsMap); - assertEquals(200, response.code()); - optionsMap.put("sortOrder", "ascending"); - response = LOCAL_DNS_HELPER.listChanges(PROJECT_ID1, ZONE_NAME1, optionsMap); - assertEquals(200, response.code()); - optionsMap.put("sortBy", null); - optionsMap.put("sortOrder", "descending"); - response = LOCAL_DNS_HELPER.listChanges(PROJECT_ID1, ZONE_NAME1, optionsMap); - assertEquals(200, response.code()); - optionsMap.put("sortOrder", "somethingelse"); - response = LOCAL_DNS_HELPER.listChanges(PROJECT_ID1, ZONE_NAME1, optionsMap); - assertEquals(400, response.code()); - assertTrue(response.body().contains("parameters.sortOrder")); - } - - @Test - public void testListChangesUsingRpc() { // no such zone exists try { RPC.listChangeRequests(ZONE_NAME1, EMPTY_RPC_OPTIONS); + fail(); } catch (DnsException ex) { // expected assertEquals(404, ex.code()); + assertTrue(ex.getMessage().contains("managedZone")); } // zone exists but has no changes RPC.create(ZONE1, EMPTY_RPC_OPTIONS); @@ -1168,24 +863,25 @@ public void testListChangesUsingRpc() { options.put(DnsRpc.Option.PAGE_SIZE, 0); try { RPC.listChangeRequests(ZONE1.getName(), options); + fail(); } catch (DnsException ex) { // expected assertEquals(400, ex.code()); + assertTrue(ex.getMessage().contains("parameters.maxResults")); } options.put(DnsRpc.Option.PAGE_SIZE, -1); try { RPC.listChangeRequests(ZONE1.getName(), options); + fail(); } catch (DnsException ex) { // expected assertEquals(400, ex.code()); + assertTrue(ex.getMessage().contains("parameters.maxResults")); } options.put(DnsRpc.Option.PAGE_SIZE, 15); - try { - RPC.listChangeRequests(ZONE1.getName(), options); - } catch (DnsException ex) { - // expected - assertEquals(400, ex.code()); - } + results = RPC.listChangeRequests(ZONE1.getName(), options).results(); + changes = ImmutableList.copyOf(results); + assertEquals(3, changes.size()); options = new HashMap<>(); options.put(DnsRpc.Option.SORTING_ORDER, "descending"); results = RPC.listChangeRequests(ZONE1.getName(), options).results(); @@ -1200,17 +896,18 @@ public void testListChangesUsingRpc() { options.put(DnsRpc.Option.SORTING_ORDER, "something else"); try { RPC.listChangeRequests(ZONE1.getName(), options); + fail(); } catch (DnsException ex) { // expected assertEquals(400, ex.code()); + assertTrue(ex.getMessage().contains("parameters.sortOrder")); } // field options RPC.applyChangeRequest(ZONE1.getName(), CHANGE_COMPLEX, EMPTY_RPC_OPTIONS); options = new HashMap<>(); options.put(DnsRpc.Option.SORTING_ORDER, "descending"); options.put(DnsRpc.Option.FIELDS, "changes(additions)"); - DnsRpc.ListResult changeListResult = - RPC.listChangeRequests(ZONE1.getName(), options); + DnsRpc.ListResult changeListResult = RPC.listChangeRequests(ZONE1.getName(), options); changes = ImmutableList.copyOf(changeListResult.results()); Change complex = changes.get(0); assertNotNull(complex.getAdditions()); @@ -1270,20 +967,68 @@ public void testListChangesUsingRpc() { assertNull(complex.getStartTime()); assertNull(complex.getStatus()); assertNotNull(changeListResult.pageToken()); - // paging - options.put(DnsRpc.Option.FIELDS, "nextPageToken,changes(id)"); + } + + @Test + public void testDnsRecordPaging() { + RPC.create(ZONE1, EMPTY_RPC_OPTIONS); + List complete = ImmutableList.copyOf( + RPC.listDnsRecords(ZONE1.getName(), EMPTY_RPC_OPTIONS).results()); + Map options = new HashMap<>(); options.put(DnsRpc.Option.PAGE_SIZE, 1); - changeListResult = RPC.listChangeRequests(ZONE1.getName(), options); - changes = ImmutableList.copyOf(changeListResult.results()); + DnsRpc.ListResult listResult = RPC.listDnsRecords(ZONE1.getName(), options); + ImmutableList records = ImmutableList.copyOf(listResult.results()); + assertEquals(1, records.size()); + assertEquals(complete.get(0), records.get(0)); + options.put(DnsRpc.Option.PAGE_TOKEN, listResult.pageToken()); + listResult = RPC.listDnsRecords(ZONE1.getName(), options); + records = ImmutableList.copyOf(listResult.results()); + assertEquals(1, records.size()); + assertEquals(complete.get(1), records.get(0)); + } + + @Test + public void testZonePaging() { + RPC.create(ZONE1, EMPTY_RPC_OPTIONS); + RPC.create(ZONE2, EMPTY_RPC_OPTIONS); + ImmutableList complete = ImmutableList.copyOf( + RPC.listZones(EMPTY_RPC_OPTIONS).results()); + Map options = new HashMap<>(); + options.put(DnsRpc.Option.PAGE_SIZE, 1); + DnsRpc.ListResult listResult = RPC.listZones(options); + ImmutableList page1 = ImmutableList.copyOf(listResult.results()); + assertEquals(1, page1.size()); + assertEquals(complete.get(0), page1.get(0)); + assertEquals(page1.get(0).getName(), listResult.pageToken()); + options.put(DnsRpc.Option.PAGE_TOKEN, listResult.pageToken()); + listResult = RPC.listZones(options); + ImmutableList page2 = ImmutableList.copyOf(listResult.results()); + assertEquals(1, page2.size()); + assertEquals(complete.get(1), page2.get(0)); + assertNull(listResult.pageToken()); + } + + @Test + public void testChangePaging() { + RPC.create(ZONE1, EMPTY_RPC_OPTIONS); + RPC.applyChangeRequest(ZONE1.getName(), CHANGE1, EMPTY_RPC_OPTIONS); + RPC.applyChangeRequest(ZONE1.getName(), CHANGE2, EMPTY_RPC_OPTIONS); + RPC.applyChangeRequest(ZONE1.getName(), CHANGE_KEEP, EMPTY_RPC_OPTIONS); + ImmutableList complete = + ImmutableList.copyOf(RPC.listChangeRequests(ZONE1.getName(), EMPTY_RPC_OPTIONS).results()); + Map options = new HashMap<>(); + options.put(DnsRpc.Option.PAGE_SIZE, 1); + DnsRpc.ListResult changeListResult = RPC.listChangeRequests(ZONE1.getName(), options); + List changes = ImmutableList.copyOf(changeListResult.results()); assertEquals(1, changes.size()); - final Change first = changes.get(0); - assertNotNull(changeListResult.pageToken()); + assertEquals(complete.get(0), changes.get(0)); + assertEquals(complete.get(0).getId(), changeListResult.pageToken()); options.put(DnsRpc.Option.PAGE_TOKEN, changeListResult.pageToken()); changeListResult = RPC.listChangeRequests(ZONE1.getName(), options); changes = ImmutableList.copyOf(changeListResult.results()); assertEquals(1, changes.size()); - Change second = changes.get(0); - assertNotEquals(first, second); + assertEquals(complete.get(1), changes.get(0)); + assertEquals(complete.get(1).getId(), changeListResult.pageToken()); } @Test @@ -1303,95 +1048,71 @@ public void testToListResponse() { } @Test - public void testCheckZone() { + public void testCreateZoneValidation() { + ManagedZone minimalZone = copyZone(ZONE1); // no name ManagedZone copy = copyZone(minimalZone); copy.setName(null); - LocalDnsHelper.Response response = LocalDnsHelper.checkZone(copy); + LocalDnsHelper.Response response = LOCAL_DNS_HELPER.createZone(PROJECT_ID1, copy); assertEquals(400, response.code()); assertTrue(response.body().contains("entity.managedZone.name")); // no description copy = copyZone(minimalZone); copy.setDescription(null); - response = LocalDnsHelper.checkZone(copy); + response = LOCAL_DNS_HELPER.createZone(PROJECT_ID1, copy); assertEquals(400, response.code()); assertTrue(response.body().contains("entity.managedZone.description")); - // no description + // no dns name copy = copyZone(minimalZone); copy.setDnsName(null); - response = LocalDnsHelper.checkZone(copy); + response = LOCAL_DNS_HELPER.createZone(PROJECT_ID1, copy); assertEquals(400, response.code()); assertTrue(response.body().contains("entity.managedZone.dnsName")); - // zone name is a number + // zone name does not start with a letter copy = copyZone(minimalZone); - copy.setName("123456"); - response = LocalDnsHelper.checkZone(copy); + copy.setName("1aaaaaa"); + response = LOCAL_DNS_HELPER.createZone(PROJECT_ID1, copy); assertEquals(400, response.code()); assertTrue(response.body().contains("entity.managedZone.name")); assertTrue(response.body().contains("Invalid")); - // dns name does not end with period + // zone name is too long copy = copyZone(minimalZone); - copy.setDnsName("aaaaaa.com"); - response = LocalDnsHelper.checkZone(copy); + copy.setName("123456aaaa123456aaaa123456aaaa123456aaaa123456aaaa123456aaaa123456aaaa123456aa"); + response = LOCAL_DNS_HELPER.createZone(PROJECT_ID1, copy); assertEquals(400, response.code()); - assertTrue(response.body().contains("entity.managedZone.dnsName")); + assertTrue(response.body().contains("entity.managedZone.name")); assertTrue(response.body().contains("Invalid")); - // dns name is reserved - copy = copyZone(minimalZone); - copy.setDnsName("com."); - response = LocalDnsHelper.checkZone(copy); - assertEquals(400, response.code()); - assertTrue(response.body().contains("not available to be created.")); - // empty description should pass + // zone name contains invalid characters copy = copyZone(minimalZone); - copy.setDescription(""); - assertNull(LocalDnsHelper.checkZone(copy)); - } - - @Test - public void testCreateZoneValidatesZone() { - // no name - ManagedZone copy = copyZone(minimalZone); - copy.setName(null); - LocalDnsHelper.Response response = LOCAL_DNS_HELPER.createZone(PROJECT_ID1, copy, null); + copy.setName("x1234AA6aa"); + response = LOCAL_DNS_HELPER.createZone(PROJECT_ID1, copy); assertEquals(400, response.code()); assertTrue(response.body().contains("entity.managedZone.name")); - // no description - copy = copyZone(minimalZone); - copy.setDescription(null); - response = LOCAL_DNS_HELPER.createZone(PROJECT_ID1, copy, null); - assertEquals(400, response.code()); - assertTrue(response.body().contains("entity.managedZone.description")); - // no dns name - copy = copyZone(minimalZone); - copy.setDnsName(null); - response = LOCAL_DNS_HELPER.createZone(PROJECT_ID1, copy, null); - assertEquals(400, response.code()); - assertTrue(response.body().contains("entity.managedZone.dnsName")); - // zone name is a number + assertTrue(response.body().contains("Invalid")); + // zone name contains invalid characters copy = copyZone(minimalZone); - copy.setName("123456"); - response = LOCAL_DNS_HELPER.createZone(PROJECT_ID1, copy, null); + copy.setName("x a"); + response = LOCAL_DNS_HELPER.createZone(PROJECT_ID1, copy); assertEquals(400, response.code()); assertTrue(response.body().contains("entity.managedZone.name")); assertTrue(response.body().contains("Invalid")); // dns name does not end with period copy = copyZone(minimalZone); copy.setDnsName("aaaaaa.com"); - response = LOCAL_DNS_HELPER.createZone(PROJECT_ID1, copy, null); + response = LOCAL_DNS_HELPER.createZone(PROJECT_ID1, copy); assertEquals(400, response.code()); assertTrue(response.body().contains("entity.managedZone.dnsName")); assertTrue(response.body().contains("Invalid")); // dns name is reserved copy = copyZone(minimalZone); copy.setDnsName("com."); - response = LOCAL_DNS_HELPER.createZone(PROJECT_ID1, copy, null); + response = LOCAL_DNS_HELPER.createZone(PROJECT_ID1, copy); assertEquals(400, response.code()); assertTrue(response.body().contains("not available to be created.")); // empty description should pass copy = copyZone(minimalZone); copy.setDescription(""); - response = LOCAL_DNS_HELPER.createZone(PROJECT_ID1, copy, null); + response = LOCAL_DNS_HELPER.createZone(PROJECT_ID1, copy); assertEquals(200, response.code()); } @@ -1474,8 +1195,8 @@ public void testCheckRrset() { valid.setTtl(500); Change validChange = new Change(); validChange.setAdditions(ImmutableList.of(valid)); - LOCAL_DNS_HELPER.createZone(PROJECT_ID1, ZONE1, null); - LOCAL_DNS_HELPER.createChange(PROJECT_ID1, ZONE_NAME1, validChange, null); + LOCAL_DNS_HELPER.createZone(PROJECT_ID1, ZONE1); + LOCAL_DNS_HELPER.createChange(PROJECT_ID1, ZONE_NAME1, validChange); // delete with field mismatch LocalDnsHelper.ZoneContainer zone = LOCAL_DNS_HELPER.findZone(PROJECT_ID1, ZONE_NAME1); valid.setTtl(valid.getTtl() + 20); @@ -1501,11 +1222,10 @@ public void testCheckRrdata() { assertFalse(LocalDnsHelper.checkRrData("111.255.12", "A")); assertFalse(LocalDnsHelper.checkRrData("111.255.12.11.11", "A")); // AAAA - assertTrue(LocalDnsHelper.checkRrData(":::::::", "AAAA")); - assertTrue(LocalDnsHelper.checkRrData("1F:fa:09fd::343:aaaa:AAAA:", "AAAA")); - assertTrue(LocalDnsHelper.checkRrData("0000:FFFF:09fd::343:aaaa:AAAA:", "AAAA")); + assertTrue(LocalDnsHelper.checkRrData("1F:fa:09fd::343:aaaa:AAAA:0", "AAAA")); + assertTrue(LocalDnsHelper.checkRrData("0000:FFFF:09fd::343:aaaa:AAAA:0", "AAAA")); assertFalse(LocalDnsHelper.checkRrData("-2:::::::", "AAAA")); - assertTrue(LocalDnsHelper.checkRrData("0:::::::", "AAAA")); + assertTrue(LocalDnsHelper.checkRrData("0::0", "AAAA")); assertFalse(LocalDnsHelper.checkRrData("::1FFFF:::::", "AAAA")); assertFalse(LocalDnsHelper.checkRrData("::aqaa:::::", "AAAA")); assertFalse(LocalDnsHelper.checkRrData("::::::::", "AAAA")); // too long @@ -1587,62 +1307,59 @@ public void testCheckChange() { ResourceRecordSet nonExistent = new ResourceRecordSet(); nonExistent.setName(ZONE1.getDnsName()); nonExistent.setType("AAAA"); - nonExistent.setRrdatas(ImmutableList.of(":::::::")); + nonExistent.setRrdatas(ImmutableList.of("0:0:0:0:5::6")); Change delete = new Change(); delete.setDeletions(ImmutableList.of(nonExistent)); response = LocalDnsHelper.checkChange(delete, zoneContainer); assertEquals(404, response.code()); assertTrue(response.body().contains("deletions[0]")); - } @Test - public void testAdditionsMeetDeletions() { + public void testCheckAdditionsDeletions() { ResourceRecordSet validA = new ResourceRecordSet(); validA.setName(ZONE1.getDnsName()); validA.setType("A"); validA.setRrdatas(ImmutableList.of("0.255.1.5")); Change validChange = new Change(); validChange.setAdditions(ImmutableList.of(validA)); - LOCAL_DNS_HELPER.createZone(PROJECT_ID1, ZONE1, null); - LOCAL_DNS_HELPER.createChange(PROJECT_ID1, ZONE_NAME1, validChange, null); + LOCAL_DNS_HELPER.createZone(PROJECT_ID1, ZONE1); + LOCAL_DNS_HELPER.createChange(PROJECT_ID1, ZONE_NAME1, validChange); LocalDnsHelper.ZoneContainer container = LOCAL_DNS_HELPER.findZone(PROJECT_ID1, ZONE_NAME1); LocalDnsHelper.Response response = - LocalDnsHelper.additionsMeetDeletions(ImmutableList.of(validA), null, container); + LocalDnsHelper.checkAdditionsDeletions(ImmutableList.of(validA), null, container); assertEquals(409, response.code()); assertTrue(response.body().contains("already exists")); - } @Test - public void testCreateChangeValidatesChangeContent() { + public void testCreateChangeContentValidation() { ResourceRecordSet validA = new ResourceRecordSet(); validA.setName(ZONE1.getDnsName()); validA.setType("A"); validA.setRrdatas(ImmutableList.of("0.255.1.5")); Change validChange = new Change(); validChange.setAdditions(ImmutableList.of(validA)); - LOCAL_DNS_HELPER.createZone(PROJECT_ID1, ZONE1, null); - LOCAL_DNS_HELPER.createChange(PROJECT_ID1, ZONE_NAME1, validChange, null); + LOCAL_DNS_HELPER.createZone(PROJECT_ID1, ZONE1); + LOCAL_DNS_HELPER.createChange(PROJECT_ID1, ZONE_NAME1, validChange); LocalDnsHelper.Response response = - LOCAL_DNS_HELPER.createChange(PROJECT_ID1, ZONE_NAME1, validChange, null); + LOCAL_DNS_HELPER.createChange(PROJECT_ID1, ZONE_NAME1, validChange); assertEquals(409, response.code()); assertTrue(response.body().contains("already exists")); // delete with field mismatch Change delete = new Change(); validA.setTtl(20); // mismatch delete.setDeletions(ImmutableList.of(validA)); - response = LOCAL_DNS_HELPER.createChange(PROJECT_ID1, ZONE_NAME1, delete, null); + response = LOCAL_DNS_HELPER.createChange(PROJECT_ID1, ZONE_NAME1, delete); assertEquals(412, response.code()); assertTrue(response.body().contains("entity.change.deletions[0]")); // delete and add SOA Change addition = new Change(); - ImmutableList rrsetWrappers - = LOCAL_DNS_HELPER.findZone(PROJECT_ID1, ZONE_NAME1).dnsRecords().get(ZONE_NAME1); + ImmutableCollection dnsRecords = + LOCAL_DNS_HELPER.findZone(PROJECT_ID1, ZONE_NAME1).dnsRecords().get().values(); LinkedList deletions = new LinkedList<>(); LinkedList additions = new LinkedList<>(); - for (LocalDnsHelper.RrsetWrapper wrapper : rrsetWrappers) { - ResourceRecordSet rrset = wrapper.rrset(); + for (ResourceRecordSet rrset : dnsRecords) { if (rrset.getType().equals("SOA")) { deletions.add(rrset); ResourceRecordSet copy = copyRrset(rrset); @@ -1653,12 +1370,12 @@ public void testCreateChangeValidatesChangeContent() { } delete.setDeletions(deletions); addition.setAdditions(additions); - response = LOCAL_DNS_HELPER.createChange(PROJECT_ID1, ZONE_NAME1, delete, null); + response = LOCAL_DNS_HELPER.createChange(PROJECT_ID1, ZONE_NAME1, delete); assertEquals(400, response.code()); assertTrue(response.body().contains( "zone must contain exactly one resource record set of type 'SOA' at the apex")); assertTrue(response.body().contains("deletions[0]")); - response = LOCAL_DNS_HELPER.createChange(PROJECT_ID1, ZONE_NAME1, addition, null); + response = LOCAL_DNS_HELPER.createChange(PROJECT_ID1, ZONE_NAME1, addition); assertEquals(400, response.code()); assertTrue(response.body().contains( "zone must contain exactly one resource record set of type 'SOA' at the apex")); @@ -1666,8 +1383,7 @@ public void testCreateChangeValidatesChangeContent() { // delete NS deletions = new LinkedList<>(); additions = new LinkedList<>(); - for (LocalDnsHelper.RrsetWrapper wrapper : rrsetWrappers) { - ResourceRecordSet rrset = wrapper.rrset(); + for (ResourceRecordSet rrset : dnsRecords) { if (rrset.getType().equals("NS")) { deletions.add(rrset); ResourceRecordSet copy = copyRrset(rrset); @@ -1678,96 +1394,38 @@ public void testCreateChangeValidatesChangeContent() { } delete.setDeletions(deletions); addition.setAdditions(additions); - response = LOCAL_DNS_HELPER.createChange(PROJECT_ID1, ZONE_NAME1, delete, null); + response = LOCAL_DNS_HELPER.createChange(PROJECT_ID1, ZONE_NAME1, delete); assertEquals(400, response.code()); assertTrue(response.body().contains( "zone must contain exactly one resource record set of type 'NS' at the apex")); - response = LOCAL_DNS_HELPER.createChange(PROJECT_ID1, ZONE_NAME1, addition, null); + response = LOCAL_DNS_HELPER.createChange(PROJECT_ID1, ZONE_NAME1, addition); assertEquals(400, response.code()); assertTrue(response.body().contains( "zone must contain exactly one resource record set of type 'NS' at the apex")); assertTrue(response.body().contains("additions[0]")); // change (delete + add) addition.setDeletions(deletions); - response = LOCAL_DNS_HELPER.createChange(PROJECT_ID1, ZONE_NAME1, addition, null); + response = LOCAL_DNS_HELPER.createChange(PROJECT_ID1, ZONE_NAME1, addition); assertEquals(200, response.code()); } @Test - public void testCreateChangeValidatesChange() { - LOCAL_DNS_HELPER.createZone(PROJECT_ID1, ZONE1, null); - ResourceRecordSet validA = new ResourceRecordSet(); - validA.setName(ZONE1.getDnsName()); - validA.setType("A"); - validA.setRrdatas(ImmutableList.of("0.255.1.5")); - ResourceRecordSet invalidA = new ResourceRecordSet(); - invalidA.setName(ZONE1.getDnsName()); - invalidA.setType("A"); - invalidA.setRrdatas(ImmutableList.of("0.-255.1.5")); - Change validChange = new Change(); - validChange.setAdditions(ImmutableList.of(validA)); - Change invalidChange = new Change(); - invalidChange.setAdditions(ImmutableList.of(invalidA)); - LocalDnsHelper.Response response = - LOCAL_DNS_HELPER.createChange(PROJECT_ID1, ZONE_NAME1, validChange, null); - assertEquals(200, response.code()); - response = LOCAL_DNS_HELPER.createChange(PROJECT_ID1, ZONE_NAME1, invalidChange, null); - assertEquals(400, response.code()); - // only empty additions/deletions - Change empty = new Change(); - empty.setAdditions(ImmutableList.of()); - empty.setDeletions(ImmutableList.of()); - response = LOCAL_DNS_HELPER.createChange(PROJECT_ID1, ZONE_NAME1, empty, null); - assertEquals(400, response.code()); - assertTrue(response.body().contains( - "The 'entity.change' parameter is required but was missing.")); - // non-matching name - validA.setName(ZONE1.getDnsName() + ".aaa."); - response = LOCAL_DNS_HELPER.createChange(PROJECT_ID1, ZONE_NAME1, validChange, null); - assertEquals(400, response.code()); - assertTrue(response.body().contains("additions[0].name")); - // wrong type - validA.setName(ZONE1.getDnsName()); // revert - validA.setType("ABCD"); - response = LOCAL_DNS_HELPER.createChange(PROJECT_ID1, ZONE_NAME1, validChange, null); - assertEquals(400, response.code()); - assertTrue(response.body().contains("additions[0].type")); - // wrong ttl - validA.setType("A"); // revert - validA.setTtl(-1); - response = LOCAL_DNS_HELPER.createChange(PROJECT_ID1, ZONE_NAME1, validChange, null); - assertEquals(400, response.code()); - assertTrue(response.body().contains("additions[0].ttl")); - validA.setTtl(null); // revert - // null name - validA.setName(null); - response = LOCAL_DNS_HELPER.createChange(PROJECT_ID1, ZONE_NAME1, validChange, null); - assertEquals(400, response.code()); - assertTrue(response.body().contains("additions[0].name")); - validA.setName(ZONE1.getDnsName()); - // null type - validA.setType(null); - response = LOCAL_DNS_HELPER.createChange(PROJECT_ID1, ZONE_NAME1, validChange, null); - assertEquals(400, response.code()); - assertTrue(response.body().contains("additions[0].type")); - validA.setType("A"); - // null rrdata - final List temp = validA.getRrdatas(); // preserve - validA.setRrdatas(null); - response = LOCAL_DNS_HELPER.createChange(PROJECT_ID1, ZONE_NAME1, validChange, null); - assertEquals(400, response.code()); - assertTrue(response.body().contains("additions[0].rrdata")); - validA.setRrdatas(temp); - // delete non-existent - ResourceRecordSet nonExistent = new ResourceRecordSet(); - nonExistent.setName(ZONE1.getDnsName()); - nonExistent.setType("AAAA"); - nonExistent.setRrdatas(ImmutableList.of(":::::::")); - Change delete = new Change(); - delete.setDeletions(ImmutableList.of(nonExistent)); - response = LOCAL_DNS_HELPER.createChange(PROJECT_ID1, ZONE_NAME1, delete, null); - assertEquals(404, response.code()); - assertTrue(response.body().contains("deletions[0]")); + public void testMatchesCriteria() { + assertTrue(LocalDnsHelper.matchesCriteria(RRSET1, RRSET1.getName(), RRSET1.getType())); + assertFalse(LocalDnsHelper.matchesCriteria(RRSET1, RRSET1.getName(), "anothertype")); + assertTrue(LocalDnsHelper.matchesCriteria(RRSET1, null, RRSET1.getType())); + assertTrue(LocalDnsHelper.matchesCriteria(RRSET1, RRSET1.getName(), null)); + assertFalse(LocalDnsHelper.matchesCriteria(RRSET1, "anothername", RRSET1.getType())); + } + + @Test + public void testGetUniqueId() { + assertNotNull(LocalDnsHelper.getUniqueId(new HashSet())); + } + + @Test + public void testRandomNameServers() { + assertEquals(4, LocalDnsHelper.randomNameservers().size()); } private static ManagedZone copyZone(ManagedZone original) {