diff --git a/gcloud-java-dns/src/main/java/com/google/gcloud/dns/testing/LocalDnsHelper.java b/gcloud-java-dns/src/main/java/com/google/gcloud/dns/testing/LocalDnsHelper.java
new file mode 100644
index 000000000000..7079d0e71269
--- /dev/null
+++ b/gcloud-java-dns/src/main/java/com/google/gcloud/dns/testing/LocalDnsHelper.java
@@ -0,0 +1,1267 @@
+/*
+ * 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.dns.testing;
+
+import static com.google.common.net.InetAddresses.isInetAddress;
+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.json.jackson.JacksonFactory;
+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.annotations.VisibleForTesting;
+import com.google.common.base.Joiner;
+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;
+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.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.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.atomic.AtomicReference;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+import java.util.regex.Pattern;
+import java.util.zip.GZIPInputStream;
+
+/**
+ * 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.
+ *
+ *
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 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
+ * types.
+ */
+@SuppressWarnings("restriction")
+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 JacksonFactory();
+ 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_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 final String projectId;
+
+ static {
+ try {
+ BASE_CONTEXT = new URI(CONTEXT);
+ } catch (URISyntaxException e) {
+ throw new IllegalArgumentException(
+ "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", 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;
+
+ CallRegex(String method, String pathRegex) {
+ this.pathRegex = pathRegex;
+ this.method = method;
+ }
+ }
+
+ /**
+ * Associates a project with a collection of ManagedZones.
+ */
+ 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 records.
+ */
+ static class ZoneContainer {
+ private final ManagedZone zone;
+ private final AtomicReference>
+ dnsRecords = new AtomicReference<>(ImmutableSortedMap.of());
+ private final ConcurrentLinkedQueue changes = new ConcurrentLinkedQueue<>();
+
+ ZoneContainer(ManagedZone zone) {
+ this.zone = zone;
+ this.dnsRecords.set(ImmutableSortedMap.of());
+ }
+
+ ManagedZone zone() {
+ return zone;
+ }
+
+ AtomicReference> 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"),
+ 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"),
+ 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 {
+
+ 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 getChange(projectId, zoneName, changeId, query);
+ case CHANGE_LIST:
+ return listChanges(projectId, zoneName, query);
+ case ZONE_GET:
+ return getZone(projectId, zoneName, query);
+ case ZONE_DELETE:
+ return deleteZone(projectId, zoneName);
+ case ZONE_LIST:
+ return listZones(projectId, query);
+ case PROJECT_GET:
+ return getProject(projectId, query);
+ case RECORD_LIST:
+ return listDnsRecords(projectId, zoneName, query);
+ case ZONE_CREATE:
+ try {
+ return handleZoneCreate(exchange, projectId, query);
+ } catch (IOException ex) {
+ return Error.BAD_REQUEST.response(ex.getMessage());
+ }
+ case CHANGE_CREATE:
+ try {
+ return handleChangeCreate(exchange, projectId, zoneName, query);
+ } 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)) {
+ Response response = pickHandler(exchange, regex);
+ writeResponse(exchange, response);
+ return;
+ }
+ }
+ 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, String projectId, String zoneName,
+ String query) throws IOException {
+ String requestBody = decodeContent(exchange.getRequestHeaders(), exchange.getRequestBody());
+ 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);
+ }
+
+ /**
+ * @throws IOException if the request cannot be parsed.
+ */
+ private Response handleZoneCreate(HttpExchange exchange, String projectId, String query)
+ throws IOException {
+ String requestBody = decodeContent(exchange.getRequestHeaders(), exchange.getRequestBody());
+ ManagedZone zone;
+ try {
+ 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(String projectId, long delay) {
+ this.delayChange = delay;
+ try {
+ this.projectId = projectId;
+ 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 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
+ */
+ public static LocalDnsHelper create(String projectId, Long delay) {
+ return new LocalDnsHelper(projectId, delay);
+ }
+
+ /**
+ * Returns a {@link DnsOptions} instance that sets the host to use the mock server.
+ */
+ public DnsOptions options() {
+ try {
+ return DnsOptions.builder().projectId(projectId).host("http://localhost:" + port).build();
+ } catch (Exception e) {
+ e.printStackTrace();
+ return null;
+ }
+ }
+
+ /**
+ * 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());
+ 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();
+ } catch (IOException e) {
+ log.log(Level.WARNING, "IOException encountered when sending response.", e);
+ }
+ }
+
+ private static String decodeContent(Headers headers, InputStream inputStream) throws IOException {
+ List contentEncoding = headers.get("Content-encoding");
+ InputStream input = inputStream;
+ try {
+ if (contentEncoding != null && !contentEncoding.isEmpty()) {
+ String encoding = contentEncoding.get(0);
+ if (SUPPORTED_ENCODINGS.contains(encoding)) {
+ input = new GZIPInputStream(inputStream);
+ } else if (!"identity".equals(encoding)) {
+ 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.
+ *
+ * @param context managedZones | projects | rrsets | changes
+ */
+ @VisibleForTesting
+ static Response toListResponse(List serializedObjects, String context, String pageToken,
+ boolean includePageToken) {
+ StringBuilder responseBody = new StringBuilder();
+ responseBody.append("{\"").append(context).append("\": [");
+ Joiner.on(",").appendTo(responseBody, serializedObjects);
+ 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('}');
+ return new Response(HTTP_OK, responseBody.toString());
+ }
+
+ /**
+ * Prepares DNS records that are created by default for each zone.
+ */
+ private static ImmutableSortedMap 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");
+ 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",
+ "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 within the set of ids.
+ */
+ @VisibleForTesting
+ static String getUniqueId(Set ids) {
+ String id;
+ do {
+ id = Long.toHexString(System.currentTimeMillis())
+ + Long.toHexString(Math.abs(ID_GENERATOR.nextLong()));
+ } while (ids.contains(id));
+ return id;
+ }
+
+ /**
+ * 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;
+ }
+ return name == null || record.getName().equals(name);
+ }
+
+ /**
+ * Returns a project container. Never returns {@code null} because we assume that all projects
+ * exists.
+ */
+ private ProjectContainer findProject(String projectId) {
+ ProjectContainer defaultProject = createProject(projectId);
+ projects.putIfAbsent(projectId, defaultProject);
+ return projects.get(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);
+ }
+
+ /**
+ * 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);
+ }
+
+ /**
+ * Returns a response to getChange service call.
+ */
+ @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) {
+ return Error.NOT_FOUND.response(String.format(
+ "The 'parameters.managedZone' resource named '%s' does not exist.", zoneName));
+ }
+ return Error.NOT_FOUND.response(String.format(
+ "The 'parameters.changeId' resource named '%s' does not exist.", changeId));
+ }
+ String[] fields = OptionParsers.parseGetOptions(query);
+ Change result = OptionParsers.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));
+ }
+ }
+
+ /**
+ * Returns a response to getZone service call.
+ */
+ @VisibleForTesting
+ Response getZone(String projectId, String zoneName, String query) {
+ String[] fields = OptionParsers.parseGetOptions(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 = OptionParsers.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));
+ }
+ }
+
+ /**
+ * 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.
+ */
+ @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) {
+ return Error.INTERNAL_ERROR.response(
+ String.format("Error when serializing project %s.", projectId));
+ }
+ }
+
+ /**
+ * Creates a project. It generates a project number randomly.
+ */
+ 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);
+ }
+
+ @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
+ ? 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.
+ */
+ @VisibleForTesting
+ Response createZone(String projectId, ManagedZone zone, String... fields) {
+ Response errorResponse = checkZone(zone);
+ if (errorResponse != null) {
+ return errorResponse;
+ }
+ 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()));
+ completeZone.setId(BigInteger.valueOf(Math.abs(ID_GENERATOR.nextLong() % Long.MAX_VALUE)));
+ completeZone.setNameServers(randomNameservers());
+ ZoneContainer zoneContainer = new ZoneContainer(completeZone);
+ zoneContainer.dnsRecords().set(defaultRecords(completeZone));
+ 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()));
+ }
+ ManagedZone result = OptionParsers.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()));
+ }
+ }
+
+ /**
+ * 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) {
+ 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));
+ }
+ Response response = checkChange(change, zoneContainer);
+ if (response != null) {
+ return response;
+ }
+ 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 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();
+ int index = 0;
+ for (Change c : changeSequence) {
+ if (index == maxId) {
+ break;
+ }
+ c.setId(String.valueOf(++index));
+ }
+ completeChange.setStatus("pending");
+ completeChange.setStartTime(ISODateTimeFormat.dateTime().withZoneUTC()
+ .print(System.currentTimeMillis()));
+ invokeChange(projectId, zoneName, completeChange.getId());
+ Change result = OptionParsers.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));
+ }
+ }
+
+ /**
+ * 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);
+ 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
+ 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) {
+ 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) {
+ 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);
+ }
+ }
+ boolean success = dnsRecords.compareAndSet(original, ImmutableSortedMap.copyOf(copy));
+ if (success) {
+ break; // success if no other thread modified the value in the meantime
+ }
+ }
+ change.setStatus("done");
+ }
+
+ /**
+ * Lists zones. Next page token is the last listed zone name and is returned only of there is more
+ * to list and if the user does not exclude nextPageToken from field options.
+ */
+ @VisibleForTesting
+ Response listZones(String projectId, String query) {
+ Map options = OptionParsers.parseListZonesOptions(query);
+ 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"));
+ boolean sizeReached = false;
+ boolean hasMorePages = false;
+ LinkedList serializedZones = new LinkedList<>();
+ String lastZoneName = null;
+ ConcurrentNavigableMap fragment =
+ pageToken != null ? containers.tailMap(pageToken, false) : containers;
+ for (ZoneContainer zoneContainer : fragment.values()) {
+ ManagedZone zone = zoneContainer.zone();
+ 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));
+ }
+ }
+ }
+ sizeReached = maxResults != null && maxResults.equals(serializedZones.size());
+ }
+ boolean includePageToken =
+ hasMorePages && (fields == null || Arrays.asList(fields).contains("nextPageToken"));
+ return toListResponse(serializedZones, "managedZones", lastZoneName, includePageToken);
+ }
+
+ /**
+ * Lists DNS records for a zone. Next page token is the ID of the last record listed.
+ */
+ @VisibleForTesting
+ Response listDnsRecords(String projectId, String zoneName, String query) {
+ Map options = OptionParsers.parseListDnsRecordsOptions(query);
+ 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));
+ }
+ 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 sizeReached = false;
+ boolean hasMorePages = false;
+ LinkedList serializedRrsets = new LinkedList<>();
+ String lastRecordId = null;
+ 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));
+ }
+ }
+ }
+ sizeReached = maxResults != null && maxResults.equals(serializedRrsets.size());
+ }
+ boolean includePageToken =
+ hasMorePages && (fields == null || Arrays.asList(fields).contains("nextPageToken"));
+ return toListResponse(serializedRrsets, "rrsets", lastRecordId, includePageToken);
+ }
+
+ /**
+ * Lists changes. Next page token is the ID of the last change listed.
+ */
+ @VisibleForTesting
+ Response listChanges(String projectId, String zoneName, String query) {
+ Map options = OptionParsers.parseListChangesOptions(query);
+ 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));
+ }
+ // take a sorted snapshot of the current change list
+ NavigableMap 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"));
+ // 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();
+ }
+ 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 = 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));
+ }
+ }
+ sizeReached = maxResults != null && maxResults.equals(serializedResults.size());
+ }
+ boolean includePageToken =
+ hasMorePages && (fields == null || Arrays.asList(fields).contains("nextPageToken"));
+ return toListResponse(serializedResults, "changes", lastChangeId, includePageToken);
+ }
+
+ /**
+ * Validates a zone to be created.
+ */
+ private 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() || 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()));
+ }
+ if (zone.getDnsName().isEmpty() || !zone.getDnsName().endsWith(".")) {
+ return Error.INVALID.response(
+ String.format("Invalid value for 'entity.managedZone.dnsName': '%s'", 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()));
+ }
+ return null;
+ }
+
+ /**
+ * Validates a change to be created.
+ */
+ @VisibleForTesting
+ static Response checkChange(Change change, ZoneContainer zone) {
+ 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) {
+ 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 checkAdditionsDeletions(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
+ */
+ @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(
+ "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 (ResourceRecordSet wrappedRrset : zone.dnsRecords().get().values()) {
+ 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().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));
+ }
+ }
+ return null;
+ }
+
+ /**
+ * 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 checkAdditionsDeletions(List additions,
+ List deletions, ZoneContainer zone) {
+ if (additions != null) {
+ int index = 0;
+ for (ResourceRecordSet rrset : additions) {
+ 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
+ && (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) {
+ switch (type) {
+ case "A":
+ return !data.contains(":") && isInetAddress(data);
+ case "AAAA":
+ return data.contains(":") && isInetAddress(data);
+ default:
+ return true;
+ }
+ }
+
+ /**
+ * Check supplied listing options.
+ */
+ @VisibleForTesting
+ static Response checkListOptions(Map options) {
+ // for general listing
+ String maxResultsString = (String) options.get("maxResults");
+ if (maxResultsString != null) {
+ Integer maxResults;
+ 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 && !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 && !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': '' "
+ + "(name must be specified if type is specified)");
+ }
+ 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/dns/testing/OptionParsers.java b/gcloud-java-dns/src/main/java/com/google/gcloud/dns/testing/OptionParsers.java
new file mode 100644
index 000000000000..ecd7e8179efe
--- /dev/null
+++ b/gcloud-java-dns/src/main/java/com/google/gcloud/dns/testing/OptionParsers.java
@@ -0,0 +1,255 @@
+/*
+ * 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.dns.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 OptionParsers {
+
+ 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, ...),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(","));
+ 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;
+ }
+
+ 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 new String[0];
+ }
+
+ static ManagedZone extractFields(ManagedZone fullZone, String... fields) {
+ if (fields == null || fields.length == 0) {
+ 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;
+ }
+
+ 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":
+ change.setAdditions(fullChange.getAdditions());
+ break;
+ case "deletions":
+ 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;
+ }
+
+ static Project extractFields(Project fullProject, String... fields) {
+ if (fields == null || fields.length == 0) {
+ 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;
+ }
+
+ static ResourceRecordSet extractFields(ResourceRecordSet fullRecord, String... fields) {
+ if (fields == null || fields.length == 0) {
+ 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":
+ String replaced = argEntry[1].replace("changes(", ",").replace(")", ",");
+ options.put("fields", replaced.split(",")); // empty strings will be ignored
+ 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":
+ 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 "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/main/java/com/google/gcloud/spi/DefaultDnsRpc.java b/gcloud-java-dns/src/main/java/com/google/gcloud/spi/DefaultDnsRpc.java
index 1df0a8a2f831..6c0c65a585cd 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
@@ -33,8 +33,8 @@
public class DefaultDnsRpc implements DnsRpc {
private static final String SORT_BY = "changeSequence";
- private final Dns dns;
- private final DnsOptions options;
+ private Dns dns;
+ private DnsOptions options;
private static DnsException translate(IOException exception) {
return new DnsException(exception);
@@ -44,13 +44,18 @@ private static DnsException translate(IOException exception) {
* Constructs an instance of this rpc client with provided {@link DnsOptions}.
*/
public DefaultDnsRpc(DnsOptions options) {
- HttpTransport transport = options.httpTransportFactory().create();
- HttpRequestInitializer initializer = options.httpRequestInitializer();
- this.dns = new Dns.Builder(transport, new JacksonFactory(), initializer)
- .setRootUrl(options.host())
- .setApplicationName(options.applicationName())
- .build();
- this.options = options;
+ try {
+ HttpTransport transport = options.httpTransportFactory().create();
+ HttpRequestInitializer initializer = options.httpRequestInitializer();
+ this.dns =
+ new Dns.Builder(transport, new JacksonFactory(), initializer)
+ .setRootUrl(options.host())
+ .setApplicationName(options.applicationName())
+ .build();
+ this.options = options;
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
}
@Override
diff --git a/gcloud-java-dns/src/test/java/com/google/gcloud/dns/testing/LocalDnsHelperTest.java b/gcloud-java-dns/src/test/java/com/google/gcloud/dns/testing/LocalDnsHelperTest.java
new file mode 100644
index 000000000000..52671c2c6ab6
--- /dev/null
+++ b/gcloud-java-dns/src/test/java/com/google/gcloud/dns/testing/LocalDnsHelperTest.java
@@ -0,0 +1,1446 @@
+/*
+ * 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.dns.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.Project;
+import com.google.api.services.dns.model.ResourceRecordSet;
+import com.google.common.collect.ImmutableCollection;
+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.AfterClass;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Iterator;
+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 static final Change CHANGE_COMPLEX = new Change();
+ private static final LocalDnsHelper LOCAL_DNS_HELPER =
+ LocalDnsHelper.create("someprojectid", 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;
+
+ @BeforeClass
+ public static void before() {
+ ZONE1.setName(ZONE_NAME1);
+ ZONE1.setDescription("");
+ ZONE1.setDnsName(DNS_NAME);
+ ZONE1.setNameServerSet("somenameserverset");
+ ZONE2.setName(ZONE_NAME2);
+ ZONE2.setDescription("");
+ ZONE2.setDnsName(DNS_NAME);
+ 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"));
+ 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() {
+ resetProjects();
+ optionsMap = new HashMap<>();
+ }
+
+ @AfterClass
+ public static void after() {
+ LOCAL_DNS_HELPER.stop();
+ }
+
+ private static void resetProjects() {
+ for (String project : LOCAL_DNS_HELPER.projects().keySet()) {
+ LOCAL_DNS_HELPER.projects().remove(project);
+ }
+ }
+
+ 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());
+ }
+
+ private static void waitUntilComplete(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(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);
+ assertEqChangesIgnoreStatus(createdChange, retrievedChange);
+ assertNull(rpc.getChangeRequest(ZONE1.getName(), "2", EMPTY_RPC_OPTIONS));
+ waitUntilComplete(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());
+ }
+ 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));
+ waitUntilComplete(rpc, ZONE1.getName(), "2");
+ rpc.applyChangeRequest(ZONE1.getName(), CHANGE_KEEP, EMPTY_RPC_OPTIONS);
+ waitUntilComplete(rpc, ZONE1.getName(), "3");
+ 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 testCreateZone() {
+ ManagedZone created = RPC.create(ZONE1, EMPTY_RPC_OPTIONS);
+ // check that default records were created
+ DnsRpc.ListResult listResult
+ = RPC.listDnsRecords(ZONE1.getName(), EMPTY_RPC_OPTIONS);
+ 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"));
+ }
+ // create zone twice
+ try {
+ RPC.create(ZONE1, EMPTY_RPC_OPTIONS);
+ fail("Zone already exists.");
+ } 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 testGetZone() {
+ // 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 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("someprojectid", 50L);
+ localDnsThreaded.start();
+ DnsRpc rpc = new DefaultDnsRpc(localDnsThreaded.options());
+ executeCreateAndApplyChangeTest(rpc);
+ localDnsThreaded.stop();
+ }
+
+ @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
+ try {
+ RPC.applyChangeRequest(ZONE_NAME1, CHANGE1, EMPTY_RPC_OPTIONS);
+ fail("Zone was not created yet.");
+ } 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
+ 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);
+ 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);
+ 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() {
+ 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);
+ 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, 335);
+ results = RPC.listZones(options).results();
+ zones = ImmutableList.copyOf(results);
+ 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<>();
+ 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 listResult = RPC.listZones(options);
+ zone = listResult.results().iterator().next();
+ assertNull(listResult.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);
+ listResult = RPC.listZones(options);
+ zone = listResult.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(), listResult.pageToken());
+ }
+
+ @Test
+ public void testListDnsRecords() {
+ // 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);
+ 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);
+ 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, 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();
+ 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);
+ 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");
+ 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 listResult =
+ RPC.listDnsRecords(ZONE1.getName(), options);
+ records = ImmutableList.copyOf(listResult.results());
+ ResourceRecordSet record = records.get(0);
+ assertNotNull(record.getName());
+ assertNull(record.getRrdatas());
+ assertNull(record.getType());
+ assertNull(record.getTtl());
+ assertNull(listResult.pageToken());
+ options.put(DnsRpc.Option.FIELDS, "rrsets(rrdatas)");
+ 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(listResult.pageToken());
+ options.put(DnsRpc.Option.FIELDS, "rrsets(ttl)");
+ 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(listResult.pageToken());
+ options.put(DnsRpc.Option.FIELDS, "rrsets(type)");
+ 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(listResult.pageToken());
+ options.put(DnsRpc.Option.FIELDS, "nextPageToken");
+ 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(listResult.pageToken());
+ options.put(DnsRpc.Option.FIELDS, "nextPageToken,rrsets(name,rrdatas)");
+ options.put(DnsRpc.Option.PAGE_SIZE, 1);
+ 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(listResult.pageToken());
+ }
+
+ @Test
+ public void testListChanges() {
+ // 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);
+ 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);
+ 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);
+ 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();
+ 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);
+ 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);
+ 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());
+ }
+
+ @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);
+ 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());
+ 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());
+ assertEquals(complete.get(1), changes.get(0));
+ assertEquals(complete.get(1).getId(), changeListResult.pageToken());
+ }
+
+ @Test
+ public void testToListResponse() {
+ LocalDnsHelper.Response response = LocalDnsHelper.toListResponse(
+ 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"), "contextB", "IncludeThisPageToken", false);
+ assertFalse(response.body().contains("IncludeThisPageToken"));
+ assertTrue(response.body().contains("contextB"));
+ response = LocalDnsHelper.toListResponse(
+ Lists.newArrayList("some", "multiple", "words"), "contextC", null, true);
+ assertFalse(response.body().contains("pageToken"));
+ assertTrue(response.body().contains("contextC"));
+ }
+
+ @Test
+ public void testCreateZoneValidation() {
+ ManagedZone minimalZone = copyZone(ZONE1);
+ // no name
+ ManagedZone copy = copyZone(minimalZone);
+ copy.setName(null);
+ 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 = LOCAL_DNS_HELPER.createZone(PROJECT_ID1, copy);
+ 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);
+ assertEquals(400, response.code());
+ assertTrue(response.body().contains("entity.managedZone.dnsName"));
+ // zone name does not start with a letter
+ copy = copyZone(minimalZone);
+ 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"));
+ // zone name is too long
+ copy = copyZone(minimalZone);
+ copy.setName("123456aaaa123456aaaa123456aaaa123456aaaa123456aaaa123456aaaa123456aaaa123456aa");
+ response = LOCAL_DNS_HELPER.createZone(PROJECT_ID1, copy);
+ assertEquals(400, response.code());
+ assertTrue(response.body().contains("entity.managedZone.name"));
+ assertTrue(response.body().contains("Invalid"));
+ // zone name contains invalid characters
+ copy = copyZone(minimalZone);
+ copy.setName("x1234AA6aa");
+ response = LOCAL_DNS_HELPER.createZone(PROJECT_ID1, copy);
+ assertEquals(400, response.code());
+ assertTrue(response.body().contains("entity.managedZone.name"));
+ assertTrue(response.body().contains("Invalid"));
+ // zone name contains invalid characters
+ copy = copyZone(minimalZone);
+ 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);
+ 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);
+ 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);
+ 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));
+ 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);
+ 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("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::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
+ final 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("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 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);
+ LOCAL_DNS_HELPER.createChange(PROJECT_ID1, ZONE_NAME1, validChange);
+ LocalDnsHelper.ZoneContainer container = LOCAL_DNS_HELPER.findZone(PROJECT_ID1, ZONE_NAME1);
+ LocalDnsHelper.Response response =
+ LocalDnsHelper.checkAdditionsDeletions(ImmutableList.of(validA), null, container);
+ assertEquals(409, response.code());
+ assertTrue(response.body().contains("already exists"));
+ }
+
+ @Test
+ 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);
+
+ LOCAL_DNS_HELPER.createChange(PROJECT_ID1, ZONE_NAME1, validChange);
+ LocalDnsHelper.Response response =
+ 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);
+ assertEquals(412, response.code());
+ assertTrue(response.body().contains("entity.change.deletions[0]"));
+ // delete and add SOA
+ Change addition = new Change();
+ ImmutableCollection dnsRecords =
+ LOCAL_DNS_HELPER.findZone(PROJECT_ID1, ZONE_NAME1).dnsRecords().get().values();
+ LinkedList deletions = new LinkedList<>();
+ LinkedList additions = new LinkedList<>();
+ for (ResourceRecordSet rrset : dnsRecords) {
+ 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 = 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);
+ 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 (ResourceRecordSet rrset : dnsRecords) {
+ 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 = 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);
+ 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);
+ assertEquals(200, response.code());
+ }
+
+ @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(new HashSet()));
+ }
+
+ @Test
+ public void testRandomNameServers() {
+ assertEquals(4, LocalDnsHelper.randomNameservers().size());
+ }
+
+ 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;
+ }
+}