Skip to content

Commit

Permalink
Refactored change test.
Browse files Browse the repository at this point in the history
Fixed docs and formatting.
Addressed remaining codacy issues.
Added missing fails.

Assigned project ID to the helper and test.
  • Loading branch information
mderka committed Mar 4, 2016
1 parent a9e1852 commit c50ee2e
Show file tree
Hide file tree
Showing 3 changed files with 184 additions and 172 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@
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;
Expand All @@ -71,18 +72,19 @@
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 utility to create local Google Cloud DNS mock.
* A local Google Cloud DNS mock.
*
* <p>The mock runs in a separate thread, listening for HTTP requests on the local machine at an
* ephemeral port.
*
* <p>While the mock attempts to simulate the service, there are some differences in the behaviour.
* The mock will accept any project ID and never returns a notFound or another error because of
* project ID. It assumes that all project IDs exist and that the user has all the necessary
* privileges to manipulate any project. Similarly, the local simulation does not work with any
* privileges to manipulate any project. Similarly, the local simulation does not require
* verification of domain name ownership. Any request for creating a managed zone will be approved.
* The mock does not track quota and will allow the user to exceed it. The mock provides only basic
* validation of the DNS data for records of type A and AAAA. It does not validate any other record
Expand All @@ -98,12 +100,12 @@ public class LocalDnsHelper {
private static final Random ID_GENERATOR = new Random();
private static final String VERSION = "v1";
private static final String CONTEXT = "/dns/" + VERSION + "/projects";
private static final Set<String> SUPPORTED_COMPRESSION_ENCODINGS =
ImmutableSet.of("gzip", "x-gzip");
private static final Set<String> ENCODINGS = ImmutableSet.of("gzip", "x-gzip");
private static final List<String> TYPES = ImmutableList.of("A", "AAAA", "CNAME", "MX", "NAPTR",
"NS", "PTR", "SOA", "SPF", "SRV", "TXT");
private static final TreeSet<String> 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-]*");

static {
try {
Expand All @@ -117,6 +119,7 @@ public class LocalDnsHelper {
private long delayChange;
private final HttpServer server;
private final int port;
private String projectId;

/**
* For matching URLs to operations.
Expand Down Expand Up @@ -265,12 +268,13 @@ private String toJson(String message) throws IOException {
private class RequestHandler implements HttpHandler {

private Response pickHandler(HttpExchange exchange, CallRegex regex) {
String path = BASE_CONTEXT.relativize(exchange.getRequestURI()).getPath();
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 = BASE_CONTEXT.relativize(exchange.getRequestURI()).getQuery();
String query = relative.getQuery();
switch (regex) {
case CHANGE_GET:
return getChange(projectId, zoneName, changeId, query);
Expand Down Expand Up @@ -315,15 +319,15 @@ public void handle(HttpExchange exchange) throws IOException {
}
}
writeResponse(exchange, Error.NOT_FOUND.response(String.format(
"The url %s does not match any API call.", exchange.getRequestURI())));
"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[] fields = OptionParsers.parseGetOptions(query);
String requestBody = decodeContent(exchange.getRequestHeaders(), exchange.getRequestBody());
Change change;
try {
Expand All @@ -332,6 +336,7 @@ private Response handleChangeCreate(HttpExchange exchange, String projectId, Str
return Error.REQUIRED.response(
"The 'entity.change' parameter is required but was missing.");
}
String[] fields = OptionParsers.parseGetOptions(query);
return createChange(projectId, zoneName, change, fields);
}

Expand All @@ -353,8 +358,9 @@ private Response handleZoneCreate(HttpExchange exchange, String projectId, Strin
}
}

private LocalDnsHelper(long delay) {
private LocalDnsHelper(String projectId, long delay) {
this.delayChange = delay;
this.projectId = projectId;
try {
server = HttpServer.create(new InetSocketAddress(0), 0);
port = server.getAddress().getPort();
Expand All @@ -379,15 +385,15 @@ ConcurrentSkipListMap<String, ProjectContainer> projects() {
*
* @param delay delay for processing changes in ms or 0 for synchronous processing
*/
public static LocalDnsHelper create(Long delay) {
return new LocalDnsHelper(delay);
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() {
return DnsOptions.builder().host("http://localhost:" + port).build();
return DnsOptions.builder().projectId(projectId).host("http://localhost:" + port).build();
}

/**
Expand Down Expand Up @@ -426,7 +432,7 @@ private static String decodeContent(Headers headers, InputStream inputStream) th
try {
if (contentEncoding != null && !contentEncoding.isEmpty()) {
String encoding = contentEncoding.get(0);
if (SUPPORTED_COMPRESSION_ENCODINGS.contains(encoding)) {
if (ENCODINGS.contains(encoding)) {
input = new GZIPInputStream(inputStream);
} else if (!"identity".equals(encoding)) {
throw new IOException(
Expand All @@ -451,7 +457,7 @@ static Response toListResponse(List<String> serializedObjects, String context, S
responseBody.append("{\"").append(context).append("\": [");
Joiner.on(",").appendTo(responseBody, serializedObjects);
responseBody.append(']');
// add page token only if exists and is asked for
// add page token only if it exists and is asked for
if (pageToken != null && includePageToken) {
responseBody.append(",\"nextPageToken\": \"").append(pageToken).append('"');
}
Expand Down Expand Up @@ -506,9 +512,6 @@ static String getUniqueId(Set<String> ids) {
do {
id = Long.toHexString(System.currentTimeMillis())
+ Long.toHexString(Math.abs(ID_GENERATOR.nextLong()));
if (!ids.contains(id)) {
return id;
}
} while (ids.contains(id));
return id;
}
Expand Down Expand Up @@ -583,12 +586,12 @@ Response getChange(String projectId, String zoneName, String changeId, String qu
*/
@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));
}
String[] fields = OptionParsers.parseGetOptions(query);
ManagedZone result = OptionParsers.extractFields(container.zone(), fields);
try {
return new Response(HTTP_OK, jsonFactory.toString(result));
Expand Down Expand Up @@ -659,12 +662,10 @@ Response deleteZone(String projectId, String zoneName) {
*/
@VisibleForTesting
Response createZone(String projectId, ManagedZone zone, String... fields) {
// check if the provided data is valid
Response errorResponse = checkZone(zone);
if (errorResponse != null) {
return errorResponse;
}
// create a copy of the managed zone in order to avoid side effects
ManagedZone completeZone = new ManagedZone();
completeZone.setName(zone.getName());
completeZone.setDnsName(zone.getDnsName());
Expand All @@ -675,17 +676,14 @@ Response createZone(String projectId, ManagedZone zone, String... fields) {
completeZone.setId(BigInteger.valueOf(Math.abs(ID_GENERATOR.nextLong() % Long.MAX_VALUE)));
completeZone.setNameServers(randomNameservers());
ZoneContainer zoneContainer = new ZoneContainer(completeZone);
// create the default NS and SOA records
zoneContainer.dnsRecords().set(defaultRecords(completeZone));
// place the zone in the data collection
ProjectContainer projectContainer = findProject(projectId);
ZoneContainer oldValue = projectContainer.zones().putIfAbsent(
completeZone.getName(), zoneContainer);
if (oldValue != null) {
return Error.ALREADY_EXISTS.response(String.format(
"The resource 'entity.managedZone' named '%s' already exists", completeZone.getName()));
}
// now return the desired attributes
ManagedZone result = OptionParsers.extractFields(completeZone, fields);
try {
return new Response(HTTP_OK, jsonFactory.toString(result));
Expand All @@ -704,13 +702,11 @@ Response createChange(String projectId, String zoneName, Change change, String..
return Error.NOT_FOUND.response(String.format(
"The 'parameters.managedZone' resource named %s does not exist.", zoneName));
}
// check that the change to be applied is valid
Response response = checkChange(change, zoneContainer);
if (response != null) {
return response;
}
// start applying
Change completeChange = new Change(); // copy to avoid side effects
Change completeChange = new Change();
if (change.getAdditions() != null) {
completeChange.setAdditions(ImmutableList.copyOf(change.getAdditions()));
}
Expand All @@ -728,11 +724,11 @@ Response createChange(String projectId, String zoneName, Change change, String..
if (index == maxId) {
break;
}
c.setId(String.valueOf(++index)); // indexing from 1
c.setId(String.valueOf(++index));
}
completeChange.setStatus("pending"); // not finished yet
completeChange.setStatus("pending");
completeChange.setStartTime(ISODateTimeFormat.dateTime().withZoneUTC()
.print(System.currentTimeMillis())); // accepted
.print(System.currentTimeMillis()));
invokeChange(projectId, zoneName, completeChange.getId());
Change result = OptionParsers.extractFields(completeChange, fields);
try {
Expand Down Expand Up @@ -813,7 +809,6 @@ private void applyExistingChange(String projectId, String zoneName, String chang
copy.put(id, rrset);
}
}
// make it immutable and replace
boolean success = dnsRecords.compareAndSet(original, ImmutableSortedMap.copyOf(copy));
if (success) {
break; // success if no other thread modified the value in the meantime
Expand All @@ -824,7 +819,7 @@ private void applyExistingChange(String projectId, String zoneName, String chang

/**
* Lists zones. Next page token is the last listed zone name and is returned only of there is more
* to list.
* to list and if the user does not exclude nextPageToken from field options.
*/
@VisibleForTesting
Response listZones(String projectId, String query) {
Expand All @@ -839,8 +834,8 @@ Response listZones(String projectId, String query) {
String pageToken = (String) options.get("pageToken");
Integer maxResults = options.get("maxResults") == null
? null : Integer.valueOf((String) options.get("maxResults"));
boolean sizeReached = false; // maximum result size was reached, we should not return more
boolean hasMorePages = false; // should next page token be included in the response?
boolean sizeReached = false;
boolean hasMorePages = false;
LinkedList<String> serializedZones = new LinkedList<>();
String lastZoneName = null;
ConcurrentNavigableMap<String, ZoneContainer> fragment =
Expand All @@ -859,20 +854,19 @@ Response listZones(String projectId, String query) {
OptionParsers.extractFields(zone, fields)));
} catch (IOException e) {
return Error.INTERNAL_ERROR.response(String.format(
"Error when serializing managed zone %s in project %s", zone.getName(),
projectId));
"Error when serializing managed zone %s in project %s", lastZoneName, projectId));
}
}
}
sizeReached = maxResults != null && maxResults.equals(serializedZones.size());
}
boolean includePageToken =
hasMorePages && (fields == null || ImmutableList.copyOf(fields).contains("nextPageToken"));
hasMorePages && (fields == null || Arrays.asList(fields).contains("nextPageToken"));
return toListResponse(serializedZones, "managedZones", lastZoneName, includePageToken);
}

/**
* Lists DNS records for a zone. Next page token is ID of the last record listed.
* Lists DNS records for a zone. Next page token is the ID of the last record listed.
*/
@VisibleForTesting
Response listDnsRecords(String projectId, String zoneName, String query) {
Expand All @@ -895,8 +889,8 @@ Response listDnsRecords(String projectId, String zoneName, String query) {
pageToken != null ? dnsRecords.tailMap(pageToken, false) : dnsRecords;
Integer maxResults = options.get("maxResults") == null
? null : Integer.valueOf((String) options.get("maxResults"));
boolean sizeReached = false; // maximum result size was reached, we should not return more
boolean hasMorePages = false; // should next page token be included in the response?
boolean sizeReached = false;
boolean hasMorePages = false;
LinkedList<String> serializedRrsets = new LinkedList<>();
String lastRecordId = null;
for (String recordId : fragment.keySet()) {
Expand All @@ -921,12 +915,12 @@ Response listDnsRecords(String projectId, String zoneName, String query) {
sizeReached = maxResults != null && maxResults.equals(serializedRrsets.size());
}
boolean includePageToken =
hasMorePages && (fields == null || ImmutableList.copyOf(fields).contains("nextPageToken"));
hasMorePages && (fields == null || Arrays.asList(fields).contains("nextPageToken"));
return toListResponse(serializedRrsets, "rrsets", lastRecordId, includePageToken);
}

/**
* Lists changes. Next page token is ID of the last change listed.
* Lists changes. Next page token is the ID of the last change listed.
*/
@VisibleForTesting
Response listChanges(String projectId, String zoneName, String query) {
Expand All @@ -952,7 +946,7 @@ Response listChanges(String projectId, String zoneName, String query) {
String pageToken = (String) options.get("pageToken");
Integer maxResults = options.get("maxResults") == null
? null : Integer.valueOf((String) options.get("maxResults"));
// we are not reading sortBy as the only key is the change sequence
// as the only supported field is change sequence, we are not reading sortBy
NavigableSet<Integer> keys;
if ("descending".equals(sortOrder)) {
keys = changes.descendingKeySet();
Expand All @@ -968,8 +962,8 @@ Response listChanges(String projectId, String zoneName, String query) {
keys = from != null ? keys.tailSet(from, false) : keys;
NavigableMap<Integer, Change> fragment =
from != null && changes.containsKey(from) ? changes.tailMap(from, false) : changes;
boolean sizeReached = false; // maximum result size was reached, we should not return more
boolean hasMorePages = false; // should next page token be included in the response?
boolean sizeReached = false;
boolean hasMorePages = false;
LinkedList<String> serializedResults = new LinkedList<>();
String lastChangeId = null;
for (Integer key : keys) {
Expand All @@ -986,13 +980,13 @@ Response listChanges(String projectId, String zoneName, String query) {
} catch (IOException e) {
return Error.INTERNAL_ERROR.response(String.format(
"Error when serializing change %s in managed zone %s in project %s",
change.getId(), zoneName, projectId));
lastChangeId, zoneName, projectId));
}
}
sizeReached = maxResults != null && maxResults.equals(serializedResults.size());
}
boolean includePageToken =
hasMorePages && (fields == null || ImmutableList.copyOf(fields).contains("nextPageToken"));
hasMorePages && (fields == null || Arrays.asList(fields).contains("nextPageToken"));
return toListResponse(serializedResults, "changes", lastChangeId, includePageToken);
}

Expand All @@ -1019,7 +1013,8 @@ private static Response checkZone(ManagedZone zone) {
} catch (NumberFormatException ex) {
// expected
}
if (zone.getName().isEmpty()) {
if (zone.getName().isEmpty() || zone.getName().length() > 32
|| !ZONE_NAME_RE.matcher(zone.getName()).matches()) {
return Error.INVALID.response(
String.format("Invalid value for 'entity.managedZone.name': '%s'", zone.getName()));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -163,7 +163,7 @@ static Project extractFields(Project fullProject, String... fields) {
}

static ResourceRecordSet extractFields(ResourceRecordSet fullRecord, String... fields) {
if (fields == null) {
if (fields == null || fields.length == 0) {
return fullRecord;
}
ResourceRecordSet record = new ResourceRecordSet();
Expand Down Expand Up @@ -196,7 +196,6 @@ static Map<String, Object> parseListChangesOptions(String query) {
String[] argEntry = arg.split("=");
switch (argEntry[0]) {
case "fields":
// todo we do not support fragmentation in deletions and additions in the library
String replaced = argEntry[1].replace("changes(", ",").replace(")", ",");
options.put("fields", replaced.split(",")); // empty strings will be ignored
break;
Expand Down
Loading

0 comments on commit c50ee2e

Please sign in to comment.