Merge pull request #1382 from jclouds/dynect-schedule-records

dynect basic record crud
diff --git a/labs/dynect/src/main/java/org/jclouds/dynect/v3/domain/CreateRecord.java b/labs/dynect/src/main/java/org/jclouds/dynect/v3/domain/CreateRecord.java
new file mode 100644
index 0000000..4f298a5
--- /dev/null
+++ b/labs/dynect/src/main/java/org/jclouds/dynect/v3/domain/CreateRecord.java
@@ -0,0 +1,163 @@
+/**
+ * Licensed to jclouds, Inc. (jclouds) under one or more
+ * contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  jclouds licenses this file
+ * to you under the Apache License, String 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 org.jclouds.dynect.v3.domain;
+
+import static com.google.common.base.Objects.equal;
+import static com.google.common.base.Objects.toStringHelper;
+import static com.google.common.base.Preconditions.checkNotNull;
+
+import java.util.Map;
+
+import com.google.common.base.Objects;
+import com.google.common.primitives.UnsignedInteger;
+
+/**
+ * @author Adrian Cole
+ */
+public class CreateRecord<D extends Map<String, Object>> {
+
+   private final String fqdn;
+   private final String type;
+   private final UnsignedInteger ttl;
+   private final D rdata;
+
+   private CreateRecord(String fqdn, String type, UnsignedInteger ttl, D rdata) {
+      this.fqdn = checkNotNull(fqdn, "fqdn");
+      this.type = checkNotNull(type, "type of %s", fqdn);
+      this.ttl = checkNotNull(ttl, "ttl of %s", fqdn);
+      this.rdata = checkNotNull(rdata, "rdata of %s", fqdn);
+   }
+
+   /**
+    * @see RecordId#getFQDN()
+    */
+   public String getFQDN() {
+      return fqdn;
+   }
+
+   /**
+    * @see RecordId#getType()
+    */
+   public String getType() {
+      return type;
+   }
+
+   /**
+    * zero for zone default
+    * 
+    * @see Record#getTTL()
+    */
+   public UnsignedInteger getTTL() {
+      return ttl;
+   }
+
+   /**
+    * @see Record#getRData()
+    */
+   public D getRData() {
+      return rdata;
+   }
+
+   @Override
+   public boolean equals(Object obj) {
+      if (this == obj)
+         return true;
+      if (obj == null || !obj.getClass().equals(CreateRecord.class))
+         return false;
+      CreateRecord<?> that = CreateRecord.class.cast(obj);
+      return equal(this.fqdn, that.fqdn) && equal(this.type, that.type) && equal(this.ttl, that.ttl)
+            && equal(this.rdata, that.rdata);
+   }
+
+   @Override
+   public String toString() {
+      return toStringHelper(this).add("fqdn", fqdn).add("type", type).add("ttl", ttl).add("rdata", rdata).toString();
+   }
+
+   @Override
+   public int hashCode() {
+      return Objects.hashCode(fqdn, type, ttl, rdata);
+   }
+
+   public static <D extends Map<String, Object>> Builder<D> builder() {
+      return new Builder<D>();
+   }
+
+   public Builder<D> toBuilder() {
+      return new Builder<D>().from(this);
+   }
+
+   public static class Builder<D extends Map<String, Object>> {
+      protected String fqdn;
+      protected String type;
+      protected UnsignedInteger ttl = UnsignedInteger.ZERO;
+      protected D rdata;
+
+      /**
+       * @see CreateRecord#getFQDN()
+       */
+      public Builder<D> fqdn(String fqdn) {
+         this.fqdn = fqdn;
+         return this;
+      }
+
+      /**
+       * @see CreateRecord#getType()
+       */
+      public Builder<D> type(String type) {
+         this.type = type;
+         return this;
+      }
+
+      /**
+       * @see CreateRecord#getTTL()
+       */
+      public Builder<D> ttl(UnsignedInteger ttl) {
+         this.ttl = ttl;
+         return this;
+      }
+
+      /**
+       * @see CreateRecord#getTTL()
+       */
+      public Builder<D> ttl(int ttl) {
+         return ttl(UnsignedInteger.fromIntBits(ttl));
+      }
+
+      /**
+       * @see CreateRecord#getRData()
+       */
+      public Builder<D> rdata(D rdata) {
+         this.rdata = rdata;
+         return this;
+      }
+
+      public CreateRecord<D> build() {
+         return new CreateRecord<D>(fqdn, type, ttl, rdata);
+      }
+
+      public <Y extends D> Builder<D> from(CreateRecord<Y> in) {
+         return fqdn(in.fqdn).type(in.type).ttl(in.ttl).rdata(in.rdata);
+      }
+
+      public <Y extends D> Builder<D> from(Record<Y> in) {
+         return fqdn(in.getFQDN()).type(in.getType()).ttl(in.getTTL()).rdata(in.getRData());
+      }
+   }
+}
\ No newline at end of file
diff --git a/labs/dynect/src/main/java/org/jclouds/dynect/v3/features/RecordApi.java b/labs/dynect/src/main/java/org/jclouds/dynect/v3/features/RecordApi.java
index 63a019b..b0a51bc 100644
--- a/labs/dynect/src/main/java/org/jclouds/dynect/v3/features/RecordApi.java
+++ b/labs/dynect/src/main/java/org/jclouds/dynect/v3/features/RecordApi.java
@@ -21,6 +21,8 @@
 import java.util.Map;
 
 import org.jclouds.dynect.v3.DynECTExceptions.JobStillRunningException;
+import org.jclouds.dynect.v3.domain.CreateRecord;
+import org.jclouds.dynect.v3.domain.Job;
 import org.jclouds.dynect.v3.domain.Record;
 import org.jclouds.dynect.v3.domain.RecordId;
 import org.jclouds.dynect.v3.domain.SOARecord;
@@ -32,6 +34,7 @@
 import org.jclouds.dynect.v3.domain.rdata.PTRData;
 import org.jclouds.dynect.v3.domain.rdata.SRVData;
 import org.jclouds.dynect.v3.domain.rdata.TXTData;
+import org.jclouds.javax.annotation.Nullable;
 
 import com.google.common.collect.FluentIterable;
 
@@ -41,15 +44,52 @@
  */
 public interface RecordApi {
    /**
-    * Retrieves a list of resource record ids for all records of any type in the
-    * given zone throws JobStillRunningException;
+    * Retrieves a list of resource record ids for all records of any type in the given zone.
+    * 
+    * @throws JobStillRunningException
+    *            if a different job in the session is still running
     */
    FluentIterable<RecordId> list() throws JobStillRunningException;
 
    /**
+    * Retrieves a list of resource record ids for all records of the fqdn and type in the given zone
+    * 
+    * @throws JobStillRunningException
+    *            if a different job in the session is still running
+    */
+   FluentIterable<RecordId> listByFQDNAndType(String fqdn, String type) throws JobStillRunningException;
+
+   /**
+    * Schedules addition of a new record into the current session. Calling {@link ZoneApi#publish(String)} will publish
+    * the zone, creating the record.
+    * 
+    * @param newRecord
+    *           record to create
+    * @return job relating to the scheduled creation.
+    * @throws JobStillRunningException
+    *            if a different job in the session is still running
+    */
+   Job scheduleCreate(CreateRecord<?> newRecord) throws JobStillRunningException;
+
+   /**
+    * Schedules deletion of a record into the current session. Calling {@link ZoneApi#publish(String)} will publish the
+    * changes, deleting the record.
+    * 
+    * @param recordId
+    *           record to delete
+    * @return job relating to the scheduled deletion or null, if the record never existed.
+    * @throws JobStillRunningException
+    *            if a different job in the session is still running
+    */
+   @Nullable
+   Job scheduleDelete(RecordId recordId) throws JobStillRunningException;
+
+   /**
     * retrieves a resource record without regard to type
     * 
     * @return null if not found
+    * @throws JobStillRunningException
+    *            if a different job in the session is still running
     */
    Record<? extends Map<String, Object>> get(RecordId recordId) throws JobStillRunningException;
 
@@ -61,6 +101,8 @@
     * @param recordId
     *           {@link RecordId#getId()}
     * @return null if not found
+    * @throws JobStillRunningException
+    *            if a different job in the session is still running
     */
    Record<AAAAData> getAAAA(String fqdn, long recordId) throws JobStillRunningException;
 
@@ -72,6 +114,8 @@
     * @param recordId
     *           {@link RecordId#getId()}
     * @return null if not found
+    * @throws JobStillRunningException
+    *            if a different job in the session is still running
     */
    Record<AData> getA(String fqdn, long recordId) throws JobStillRunningException;
 
@@ -83,6 +127,8 @@
     * @param recordId
     *           {@link RecordId#getId()}
     * @return null if not found
+    * @throws JobStillRunningException
+    *            if a different job in the session is still running
     */
    Record<CNAMEData> getCNAME(String fqdn, long recordId) throws JobStillRunningException;
 
@@ -94,6 +140,8 @@
     * @param recordId
     *           {@link RecordId#getId()}
     * @return null if not found
+    * @throws JobStillRunningException
+    *            if a different job in the session is still running
     */
    Record<MXData> getMX(String fqdn, long recordId) throws JobStillRunningException;
 
@@ -105,6 +153,8 @@
     * @param recordId
     *           {@link RecordId#getId()}
     * @return null if not found
+    * @throws JobStillRunningException
+    *            if a different job in the session is still running
     */
    Record<NSData> getNS(String fqdn, long recordId) throws JobStillRunningException;
 
@@ -116,6 +166,8 @@
     * @param recordId
     *           {@link RecordId#getId()}
     * @return null if not found
+    * @throws JobStillRunningException
+    *            if a different job in the session is still running
     */
    Record<PTRData> getPTR(String fqdn, long recordId) throws JobStillRunningException;
 
@@ -127,6 +179,8 @@
     * @param recordId
     *           {@link RecordId#getId()}
     * @return null if not found
+    * @throws JobStillRunningException
+    *            if a different job in the session is still running
     */
    SOARecord getSOA(String fqdn, long recordId) throws JobStillRunningException;
 
@@ -138,6 +192,8 @@
     * @param recordId
     *           {@link RecordId#getId()}
     * @return null if not found
+    * @throws JobStillRunningException
+    *            if a different job in the session is still running
     */
    Record<SRVData> getSRV(String fqdn, long recordId) throws JobStillRunningException;
 
@@ -149,6 +205,8 @@
     * @param recordId
     *           {@link RecordId#getId()}
     * @return null if not found
+    * @throws JobStillRunningException
+    *            if a different job in the session is still running
     */
    Record<TXTData> getTXT(String fqdn, long recordId) throws JobStillRunningException;
 }
\ No newline at end of file
diff --git a/labs/dynect/src/main/java/org/jclouds/dynect/v3/features/RecordAsyncApi.java b/labs/dynect/src/main/java/org/jclouds/dynect/v3/features/RecordAsyncApi.java
index 20fce2b..6902a81 100644
--- a/labs/dynect/src/main/java/org/jclouds/dynect/v3/features/RecordAsyncApi.java
+++ b/labs/dynect/src/main/java/org/jclouds/dynect/v3/features/RecordAsyncApi.java
@@ -19,18 +19,26 @@
 package org.jclouds.dynect.v3.features;
 
 import static com.google.common.base.Preconditions.checkNotNull;
+import static javax.ws.rs.core.MediaType.APPLICATION_JSON;
 import static org.jclouds.http.Uris.uriBuilder;
 
 import java.net.URI;
 import java.util.Map;
 
+import javax.inject.Inject;
 import javax.inject.Named;
+import javax.ws.rs.Consumes;
+import javax.ws.rs.DELETE;
 import javax.ws.rs.GET;
+import javax.ws.rs.POST;
 import javax.ws.rs.Path;
 import javax.ws.rs.PathParam;
+import javax.ws.rs.Produces;
 
 import org.jclouds.Fallbacks.NullOnNotFoundOr404;
 import org.jclouds.dynect.v3.DynECTExceptions.JobStillRunningException;
+import org.jclouds.dynect.v3.domain.CreateRecord;
+import org.jclouds.dynect.v3.domain.Job;
 import org.jclouds.dynect.v3.domain.Record;
 import org.jclouds.dynect.v3.domain.RecordId;
 import org.jclouds.dynect.v3.domain.SOARecord;
@@ -46,6 +54,7 @@
 import org.jclouds.dynect.v3.filters.SessionManager;
 import org.jclouds.dynect.v3.functions.ToRecordIds;
 import org.jclouds.http.HttpRequest;
+import org.jclouds.json.Json;
 import org.jclouds.rest.Binder;
 import org.jclouds.rest.annotations.BinderParam;
 import org.jclouds.rest.annotations.Fallback;
@@ -79,6 +88,58 @@
    ListenableFuture<FluentIterable<RecordId>> list() throws JobStillRunningException;
 
    /**
+    * @see RecordApi#listByFQDNAndType
+    */
+   @Named("GetRecord")
+   @GET
+   @Path("/{type}Record/{zone}/{fqdn}")
+   @ResponseParser(ToRecordIds.class)
+   ListenableFuture<FluentIterable<RecordId>> listByFQDNAndType(@PathParam("fqdn") String fqdn,
+         @PathParam("type") String type) throws JobStillRunningException;
+
+   /**
+    * @see RecordApi#scheduleCreate
+    */
+   @Named("CreateRecord")
+   @POST
+   @Path("/{type}Record/{zone}/{fqdn}")
+   @Consumes(APPLICATION_JSON)
+   @Produces(APPLICATION_JSON)
+   ListenableFuture<Job> scheduleCreate(@BinderParam(CreateRecordBinder.class) CreateRecord<?> newRecord)
+         throws JobStillRunningException;
+
+   static class CreateRecordBinder implements Binder {
+      private final Json json;
+
+      @Inject
+      CreateRecordBinder(Json json){
+         this.json = checkNotNull(json, "json");
+      }
+
+      @SuppressWarnings("unchecked")
+      @Override
+      public <R extends HttpRequest> R bindToRequest(R request, Object arg) {
+         CreateRecord<?> in = CreateRecord.class.cast(checkNotNull(arg, "record to create"));
+         URI path = uriBuilder(request.getEndpoint())
+                     .build(ImmutableMap.<String, Object> builder()
+                                        .put("type", in.getType())
+                                        .put("fqdn", in.getFQDN()).build());
+         return (R) request.toBuilder()
+                           .endpoint(path)
+                           .payload(json.toJson(ImmutableMap.of("rdata", ImmutableMap.copyOf(in.getRData()), "ttl", in.getTTL().intValue()))).build();
+      }
+   }
+
+   /**
+    * @see RecordApi#scheduleDelete
+    */
+   @Named("DeleteRecord")
+   @DELETE
+   @Fallback(NullOnNotFoundOr404.class)
+   @Consumes(APPLICATION_JSON)
+   ListenableFuture<Job> scheduleDelete(@BinderParam(RecordIdBinder.class) RecordId recordId) throws JobStillRunningException;
+
+   /**
     * @see RecordApi#get
     */
    @Named("GetRecord")
diff --git a/labs/dynect/src/main/java/org/jclouds/dynect/v3/features/ZoneApi.java b/labs/dynect/src/main/java/org/jclouds/dynect/v3/features/ZoneApi.java
index b9cf4a5..0ebde9f 100644
--- a/labs/dynect/src/main/java/org/jclouds/dynect/v3/features/ZoneApi.java
+++ b/labs/dynect/src/main/java/org/jclouds/dynect/v3/features/ZoneApi.java
@@ -18,13 +18,14 @@
  */
 package org.jclouds.dynect.v3.features;
 
-import org.jclouds.dynect.v3.DynECTExceptions.TargetExistsException;
 import org.jclouds.dynect.v3.DynECTExceptions.JobStillRunningException;
+import org.jclouds.dynect.v3.DynECTExceptions.TargetExistsException;
 import org.jclouds.dynect.v3.domain.CreatePrimaryZone;
 import org.jclouds.dynect.v3.domain.Job;
 import org.jclouds.dynect.v3.domain.Zone;
 import org.jclouds.dynect.v3.domain.Zone.SerialStyle;
 import org.jclouds.javax.annotation.Nullable;
+import org.jclouds.rest.ResourceNotFoundException;
 
 import com.google.common.collect.FluentIterable;
 
@@ -35,57 +36,74 @@
 public interface ZoneApi {
    /**
     * Lists all zone ids.
+    * 
+    * @throws JobStillRunningException
+    *            if a different job in the session is still running
     */
    FluentIterable<String> list() throws JobStillRunningException;
 
    /**
-    * Creates a new primary zone.
+    * Schedules addition of a new primary zone into the current session. Calling {@link ZoneApi#publish(String)} will
+    * publish the zone, creating the zone.
     * 
     * @param zone
     *           required parameters to create the zone.
-    * @return unpublished zone
+    * @return job relating to the scheduled creation.
+    * @throws JobStillRunningException
+    *            if a different job in the session is still running
+    * @throws TargetExistsException
+    *            if the same fqdn exists
     */
-   Zone create(CreatePrimaryZone zone) throws JobStillRunningException, TargetExistsException;
+   Job scheduleCreate(CreatePrimaryZone zone) throws JobStillRunningException, TargetExistsException;
 
    /**
-    * Creates a new primary zone with one hour default TTL and
-    * {@link SerialStyle#INCREMENT}
+    * Schedules addition of a new primary zone with one hour default TTL and {@link SerialStyle#INCREMENT} into the
+    * current session. Calling {@link ZoneApi#publish(String)} will publish the zone, creating the zone.
     * 
     * @param fqdn
     *           fqdn of the zone to create {@ex. jclouds.org}
     * @param contact
     *           email address of the contact
-    * @return unpublished zone
+    * @return job relating to the scheduled creation.
+    * @throws JobStillRunningException
+    *            if a different job in the session is still running
+    * @throws TargetExistsException
+    *            if the same fqdn exists
     */
-   Zone createWithContact(String fqdn, String contact) throws JobStillRunningException, TargetExistsException;
+   Job scheduleCreateWithContact(String fqdn, String contact) throws JobStillRunningException, TargetExistsException;
 
    /**
     * Retrieves information about the specified zone.
     * 
     * @param fqdn
-    *           fqdn of the zone to get information about. ex
-    *           {@code jclouds.org}
+    *           fqdn of the zone to get information about. ex {@code jclouds.org}
     * @return null if not found
+    * @throws JobStillRunningException
+    *            if a different job in the session is still running
     */
    @Nullable
    Zone get(String fqdn) throws JobStillRunningException;
 
    /**
-    * deletes the specified zone.
+    * Deletes the zone.  No need to call @link ZoneApi#publish(String)}.
     * 
     * @param fqdn
-    *           fqdn of the zone to delete ex {@code jclouds.org}
-    * @return null if not found
+    *           zone to delete
+    * @return job relating to the scheduled deletion or null, if the zone never existed.
+    * @throws JobStillRunningException
+    *            if a different job in the session is still running
     */
    @Nullable
    Job delete(String fqdn) throws JobStillRunningException;
 
    /**
-    * Deletes changes to the specified zone that have been created during the
-    * current session but not yet published to the zone.
+    * Deletes changes to the specified zone that have been created during the current session but not yet published to
+    * the zone.
     * 
     * @param fqdn
     *           fqdn of the zone to delete changes from ex {@code jclouds.org}
+    * @throws JobStillRunningException
+    *            if a different job in the session is still running
     */
    Job deleteChanges(String fqdn) throws JobStillRunningException;
 
@@ -93,24 +111,31 @@
     * Publishes the current zone
     * 
     * @param fqdn
-    *           fqdn of the zone to publish. ex
-    *           {@code jclouds.org}
+    *           fqdn of the zone to publish. ex {@code jclouds.org}
+    * @throws JobStillRunningException
+    *            if a different job in the session is still running
+    * @throws ResourceNotFoundException
+    *            if the zone doesn't exist
     */
-   Zone publish(String fqdn) throws JobStillRunningException;
+   Zone publish(String fqdn) throws JobStillRunningException, ResourceNotFoundException;
 
    /**
     * freezes the specified zone.
     * 
     * @param fqdn
     *           fqdn of the zone to freeze ex {@code jclouds.org}
+    * @throws JobStillRunningException
+    *            if a different job in the session is still running
     */
    Job freeze(String fqdn) throws JobStillRunningException;
-   
+
    /**
     * thaws the specified zone.
     * 
     * @param fqdn
     *           fqdn of the zone to thaw ex {@code jclouds.org}
+    * @throws JobStillRunningException
+    *            if a different job in the session is still running
     */
    Job thaw(String fqdn) throws JobStillRunningException;
 }
\ No newline at end of file
diff --git a/labs/dynect/src/main/java/org/jclouds/dynect/v3/features/ZoneAsyncApi.java b/labs/dynect/src/main/java/org/jclouds/dynect/v3/features/ZoneAsyncApi.java
index 8aa2336..8569b81 100644
--- a/labs/dynect/src/main/java/org/jclouds/dynect/v3/features/ZoneAsyncApi.java
+++ b/labs/dynect/src/main/java/org/jclouds/dynect/v3/features/ZoneAsyncApi.java
@@ -40,6 +40,7 @@
 import org.jclouds.dynect.v3.filters.AlwaysAddContentType;
 import org.jclouds.dynect.v3.filters.SessionManager;
 import org.jclouds.dynect.v3.functions.ExtractZoneNames;
+import org.jclouds.rest.ResourceNotFoundException;
 import org.jclouds.rest.annotations.BinderParam;
 import org.jclouds.rest.annotations.Fallback;
 import org.jclouds.rest.annotations.Headers;
@@ -86,27 +87,27 @@
    ListenableFuture<Zone> get(@PathParam("fqdn") String fqdn) throws JobStillRunningException;
 
    /**
-    * @see ZoneApi#create
+    * @see ZoneApi#scheduleCreate
     */
    @Named("CreatePrimaryZone")
    @POST
    @Path("/Zone/{fqdn}")
-   @SelectJson("data")
-   ListenableFuture<Zone> create(
+   @Consumes(APPLICATION_JSON)
+   ListenableFuture<Job> scheduleCreate(
          @PathParam("fqdn") @ParamParser(ToFQDN.class) @BinderParam(BindToJsonPayload.class) CreatePrimaryZone createZone)
          throws JobStillRunningException, TargetExistsException;
 
    /**
-    * @see ZoneApi#createWithContact
+    * @see ZoneApi#scheduleCreateWithContact
     */
    @Named("CreatePrimaryZone")
    @POST
    @Produces(APPLICATION_JSON)
    @Payload("%7B\"rname\":\"{contact}\",\"serial_style\":\"increment\",\"ttl\":3600%7D")
    @Path("/Zone/{fqdn}")
-   @SelectJson("data")
-   ListenableFuture<Zone> createWithContact(@PathParam("fqdn") String fqdn, @PayloadParam("contact") String contact)
-         throws JobStillRunningException, TargetExistsException;
+   @Consumes(APPLICATION_JSON)
+   ListenableFuture<Job> scheduleCreateWithContact(@PathParam("fqdn") String fqdn,
+         @PayloadParam("contact") String contact) throws JobStillRunningException, TargetExistsException;
 
    /**
     * @see ZoneApi#delete
@@ -136,7 +137,7 @@
    @Produces(APPLICATION_JSON)
    @Payload("{\"publish\":true}")
    @SelectJson("data")
-   ListenableFuture<Zone> publish(@PathParam("fqdn") String fqdn) throws JobStillRunningException;
+   ListenableFuture<Zone> publish(@PathParam("fqdn") String fqdn) throws JobStillRunningException, ResourceNotFoundException;
    
    /**
     * @see ZoneApi#freeze
diff --git a/labs/dynect/src/test/java/org/jclouds/dynect/v3/features/RecordApiExpectTest.java b/labs/dynect/src/test/java/org/jclouds/dynect/v3/features/RecordApiExpectTest.java
index ea4e5e7..aea0721 100644
--- a/labs/dynect/src/test/java/org/jclouds/dynect/v3/features/RecordApiExpectTest.java
+++ b/labs/dynect/src/test/java/org/jclouds/dynect/v3/features/RecordApiExpectTest.java
@@ -19,13 +19,18 @@
 package org.jclouds.dynect.v3.features;
 
 import static com.google.common.net.HttpHeaders.CONTENT_TYPE;
+import static com.google.common.net.HttpHeaders.ACCEPT;
 import static javax.ws.rs.core.MediaType.APPLICATION_JSON;
 import static org.jclouds.dynect.v3.domain.RecordId.recordIdBuilder;
+import static org.jclouds.dynect.v3.domain.rdata.AData.a;
 import static org.testng.Assert.assertEquals;
 import static org.testng.Assert.assertNull;
 
 import org.jclouds.dynect.v3.DynECTApi;
+import org.jclouds.dynect.v3.domain.CreateRecord;
+import org.jclouds.dynect.v3.domain.Job;
 import org.jclouds.dynect.v3.domain.RecordId;
+import org.jclouds.dynect.v3.domain.rdata.AData;
 import org.jclouds.dynect.v3.internal.BaseDynECTApiExpectTest;
 import org.jclouds.dynect.v3.parse.GetAAAARecordResponseTest;
 import org.jclouds.dynect.v3.parse.GetARecordResponseTest;
@@ -48,33 +53,33 @@
 @Test(groups = "unit", testName = "RecordApiExpectTest")
 public class RecordApiExpectTest extends BaseDynECTApiExpectTest {
    HttpRequest getSOA = HttpRequest.builder().method("GET")
-                                .endpoint("https://api2.dynect.net/REST/SOARecord/adrianc.zone.dynecttest.jclouds.org/adrianc.zone.dynecttest.jclouds.org/50976579")
-                                .addHeader("API-Version", "3.3.8")
-                                .addHeader(CONTENT_TYPE, APPLICATION_JSON)
-                                .addHeader("Auth-Token", authToken).build();   
+                                   .endpoint("https://api2.dynect.net/REST/SOARecord/jclouds.org/jclouds.org/50976579")
+                                   .addHeader("API-Version", "3.3.8")
+                                   .addHeader(CONTENT_TYPE, APPLICATION_JSON)
+                                   .addHeader("Auth-Token", authToken).build();   
 
    HttpResponse soaResponse = HttpResponse.builder().statusCode(200)
          .payload(payloadFromResourceWithContentType("/get_record_soa.json", APPLICATION_JSON)).build();
 
    RecordId soaId = recordIdBuilder()
-                            .zone("adrianc.zone.dynecttest.jclouds.org")
-                            .fqdn("adrianc.zone.dynecttest.jclouds.org")
+                            .zone("jclouds.org")
+                            .fqdn("jclouds.org")
                             .type("SOA")
                             .id(50976579l).build();
 
    public void testGetWhenResponseIs2xx() {
       DynECTApi success = requestsSendResponses(createSession, createSessionResponse, getSOA, soaResponse);
-      assertEquals(success.getRecordApiForZone("adrianc.zone.dynecttest.jclouds.org").get(soaId).toString(),
+      assertEquals(success.getRecordApiForZone("jclouds.org").get(soaId).toString(),
                    new GetRecordResponseTest().expected().toString());
    }
 
    public void testGetWhenResponseIs404() {
       DynECTApi fail = requestsSendResponses(createSession, createSessionResponse, getSOA, notFound);
-      assertNull(fail.getRecordApiForZone("adrianc.zone.dynecttest.jclouds.org").get(soaId));
+      assertNull(fail.getRecordApiForZone("jclouds.org").get(soaId));
    }
 
    HttpRequest getAAAA = HttpRequest.builder().method("GET")
-         .endpoint("https://api2.dynect.net/REST/AAAARecord/adrianc.zone.dynecttest.jclouds.org/adrianc.zone.dynecttest.jclouds.org/50976579")
+         .endpoint("https://api2.dynect.net/REST/AAAARecord/jclouds.org/jclouds.org/50976579")
          .addHeader("API-Version", "3.3.8")
          .addHeader(CONTENT_TYPE, APPLICATION_JSON)
          .addHeader("Auth-Token", authToken).build();
@@ -83,24 +88,24 @@
          .payload(payloadFromResourceWithContentType("/get_record_aaaa.json", APPLICATION_JSON)).build();
 
    RecordId aaaaId = recordIdBuilder()
-        .zone("adrianc.zone.dynecttest.jclouds.org")
-        .fqdn("adrianc.zone.dynecttest.jclouds.org")
+        .zone("jclouds.org")
+        .fqdn("jclouds.org")
         .type("AAAA")
         .id(50976579l).build();
 
    public void testGetAAAAWhenResponseIs2xx() {
       DynECTApi success = requestsSendResponses(createSession, createSessionResponse, getAAAA, aaaaResponse);
-      assertEquals(success.getRecordApiForZone("adrianc.zone.dynecttest.jclouds.org").getAAAA(aaaaId.getFQDN(), aaaaId.getId()).toString(),
+      assertEquals(success.getRecordApiForZone("jclouds.org").getAAAA(aaaaId.getFQDN(), aaaaId.getId()).toString(),
                    new GetAAAARecordResponseTest().expected().toString());
    }
 
    public void testGetAAAAWhenResponseIs404() {
       DynECTApi fail = requestsSendResponses(createSession, createSessionResponse, getAAAA, notFound);
-      assertNull(fail.getRecordApiForZone("adrianc.zone.dynecttest.jclouds.org").getAAAA(aaaaId.getFQDN(), aaaaId.getId()));
+      assertNull(fail.getRecordApiForZone("jclouds.org").getAAAA(aaaaId.getFQDN(), aaaaId.getId()));
    }
 
    HttpRequest getA = HttpRequest.builder().method("GET")
-         .endpoint("https://api2.dynect.net/REST/ARecord/adrianc.zone.dynecttest.jclouds.org/adrianc.zone.dynecttest.jclouds.org/50976579")
+         .endpoint("https://api2.dynect.net/REST/ARecord/jclouds.org/jclouds.org/50976579")
          .addHeader("API-Version", "3.3.8")
          .addHeader(CONTENT_TYPE, APPLICATION_JSON)
          .addHeader("Auth-Token", authToken).build();   
@@ -109,24 +114,24 @@
          .payload(payloadFromResourceWithContentType("/get_record_a.json", APPLICATION_JSON)).build();
 
    RecordId aId = recordIdBuilder()
-        .zone("adrianc.zone.dynecttest.jclouds.org")
-        .fqdn("adrianc.zone.dynecttest.jclouds.org")
+        .zone("jclouds.org")
+        .fqdn("jclouds.org")
         .type("A")
         .id(50976579l).build();
 
    public void testGetAWhenResponseIs2xx() {
       DynECTApi success = requestsSendResponses(createSession, createSessionResponse, getA, aResponse);
-      assertEquals(success.getRecordApiForZone("adrianc.zone.dynecttest.jclouds.org").getA(aId.getFQDN(), aId.getId()).toString(),
+      assertEquals(success.getRecordApiForZone("jclouds.org").getA(aId.getFQDN(), aId.getId()).toString(),
                    new GetARecordResponseTest().expected().toString());
    }
 
    public void testGetAWhenResponseIs404() {
       DynECTApi fail = requestsSendResponses(createSession, createSessionResponse, getA, notFound);
-      assertNull(fail.getRecordApiForZone("adrianc.zone.dynecttest.jclouds.org").getA(aId.getFQDN(), aId.getId()));
+      assertNull(fail.getRecordApiForZone("jclouds.org").getA(aId.getFQDN(), aId.getId()));
    }
 
    HttpRequest getCNAME = HttpRequest.builder().method("GET")
-         .endpoint("https://api2.dynect.net/REST/CNAMERecord/adrianc.zone.dynecttest.jclouds.org/adrianc.zone.dynecttest.jclouds.org/50976579")
+         .endpoint("https://api2.dynect.net/REST/CNAMERecord/jclouds.org/jclouds.org/50976579")
          .addHeader("API-Version", "3.3.8")
          .addHeader(CONTENT_TYPE, APPLICATION_JSON)
          .addHeader("Auth-Token", authToken).build();   
@@ -135,24 +140,24 @@
          .payload(payloadFromResourceWithContentType("/get_record_cname.json", APPLICATION_JSON)).build();
 
    RecordId cnameId = recordIdBuilder()
-        .zone("adrianc.zone.dynecttest.jclouds.org")
-        .fqdn("adrianc.zone.dynecttest.jclouds.org")
+        .zone("jclouds.org")
+        .fqdn("jclouds.org")
         .type("CNAME")
         .id(50976579l).build();
 
    public void testGetCNAMEWhenResponseIs2xx() {
       DynECTApi success = requestsSendResponses(createSession, createSessionResponse, getCNAME, cnameResponse);
-      assertEquals(success.getRecordApiForZone("adrianc.zone.dynecttest.jclouds.org").getCNAME(cnameId.getFQDN(), cnameId.getId()).toString(),
+      assertEquals(success.getRecordApiForZone("jclouds.org").getCNAME(cnameId.getFQDN(), cnameId.getId()).toString(),
                    new GetCNAMERecordResponseTest().expected().toString());
    }
 
    public void testGetCNAMEWhenResponseIs404() {
       DynECTApi fail = requestsSendResponses(createSession, createSessionResponse, getCNAME, notFound);
-      assertNull(fail.getRecordApiForZone("adrianc.zone.dynecttest.jclouds.org").getCNAME(cnameId.getFQDN(), cnameId.getId()));
+      assertNull(fail.getRecordApiForZone("jclouds.org").getCNAME(cnameId.getFQDN(), cnameId.getId()));
    }
 
    HttpRequest getMX = HttpRequest.builder().method("GET")
-         .endpoint("https://api2.dynect.net/REST/MXRecord/adrianc.zone.dynecttest.jclouds.org/adrianc.zone.dynecttest.jclouds.org/50976579")
+         .endpoint("https://api2.dynect.net/REST/MXRecord/jclouds.org/jclouds.org/50976579")
          .addHeader("API-Version", "3.3.8")
          .addHeader(CONTENT_TYPE, APPLICATION_JSON)
          .addHeader("Auth-Token", authToken).build();   
@@ -161,24 +166,24 @@
          .payload(payloadFromResourceWithContentType("/get_record_mx.json", APPLICATION_JSON)).build();
 
    RecordId mxId = recordIdBuilder()
-        .zone("adrianc.zone.dynecttest.jclouds.org")
-        .fqdn("adrianc.zone.dynecttest.jclouds.org")
+        .zone("jclouds.org")
+        .fqdn("jclouds.org")
         .type("MX")
         .id(50976579l).build();
 
    public void testGetMXWhenResponseIs2xx() {
       DynECTApi success = requestsSendResponses(createSession, createSessionResponse, getMX, mxResponse);
-      assertEquals(success.getRecordApiForZone("adrianc.zone.dynecttest.jclouds.org").getMX(mxId.getFQDN(), mxId.getId()).toString(),
+      assertEquals(success.getRecordApiForZone("jclouds.org").getMX(mxId.getFQDN(), mxId.getId()).toString(),
                    new GetMXRecordResponseTest().expected().toString());
    }
 
    public void testGetMXWhenResponseIs404() {
       DynECTApi fail = requestsSendResponses(createSession, createSessionResponse, getMX, notFound);
-      assertNull(fail.getRecordApiForZone("adrianc.zone.dynecttest.jclouds.org").getMX(mxId.getFQDN(), mxId.getId()));
+      assertNull(fail.getRecordApiForZone("jclouds.org").getMX(mxId.getFQDN(), mxId.getId()));
    }
 
    HttpRequest getNS = HttpRequest.builder().method("GET")
-         .endpoint("https://api2.dynect.net/REST/NSRecord/adrianc.zone.dynecttest.jclouds.org/adrianc.zone.dynecttest.jclouds.org/50976579")
+         .endpoint("https://api2.dynect.net/REST/NSRecord/jclouds.org/jclouds.org/50976579")
          .addHeader("API-Version", "3.3.8")
          .addHeader(CONTENT_TYPE, APPLICATION_JSON)
          .addHeader("Auth-Token", authToken).build();   
@@ -187,24 +192,24 @@
          .payload(payloadFromResourceWithContentType("/get_record_ns.json", APPLICATION_JSON)).build();
 
    RecordId nsId = recordIdBuilder()
-        .zone("adrianc.zone.dynecttest.jclouds.org")
-        .fqdn("adrianc.zone.dynecttest.jclouds.org")
+        .zone("jclouds.org")
+        .fqdn("jclouds.org")
         .type("NS")
         .id(50976579l).build();
 
    public void testGetNSWhenResponseIs2xx() {
       DynECTApi success = requestsSendResponses(createSession, createSessionResponse, getNS, nsResponse);
-      assertEquals(success.getRecordApiForZone("adrianc.zone.dynecttest.jclouds.org").getNS(nsId.getFQDN(), nsId.getId()).toString(),
+      assertEquals(success.getRecordApiForZone("jclouds.org").getNS(nsId.getFQDN(), nsId.getId()).toString(),
                    new GetNSRecordResponseTest().expected().toString());
    }
 
    public void testGetNSWhenResponseIs404() {
       DynECTApi fail = requestsSendResponses(createSession, createSessionResponse, getNS, notFound);
-      assertNull(fail.getRecordApiForZone("adrianc.zone.dynecttest.jclouds.org").getNS(nsId.getFQDN(), nsId.getId()));
+      assertNull(fail.getRecordApiForZone("jclouds.org").getNS(nsId.getFQDN(), nsId.getId()));
    }
 
    HttpRequest getPTR = HttpRequest.builder().method("GET")
-         .endpoint("https://api2.dynect.net/REST/PTRRecord/adrianc.zone.dynecttest.jclouds.org/adrianc.zone.dynecttest.jclouds.org/50976579")
+         .endpoint("https://api2.dynect.net/REST/PTRRecord/jclouds.org/jclouds.org/50976579")
          .addHeader("API-Version", "3.3.8")
          .addHeader(CONTENT_TYPE, APPLICATION_JSON)
          .addHeader("Auth-Token", authToken).build();   
@@ -213,35 +218,35 @@
          .payload(payloadFromResourceWithContentType("/get_record_ptr.json", APPLICATION_JSON)).build();
 
    RecordId ptrId = recordIdBuilder()
-        .zone("adrianc.zone.dynecttest.jclouds.org")
-        .fqdn("adrianc.zone.dynecttest.jclouds.org")
+        .zone("jclouds.org")
+        .fqdn("jclouds.org")
         .type("PTR")
         .id(50976579l).build();
 
    public void testGetPTRWhenResponseIs2xx() {
       DynECTApi success = requestsSendResponses(createSession, createSessionResponse, getPTR, ptrResponse);
-      assertEquals(success.getRecordApiForZone("adrianc.zone.dynecttest.jclouds.org").getPTR(ptrId.getFQDN(), ptrId.getId()).toString(),
+      assertEquals(success.getRecordApiForZone("jclouds.org").getPTR(ptrId.getFQDN(), ptrId.getId()).toString(),
                    new GetPTRRecordResponseTest().expected().toString());
    }
 
    public void testGetPTRWhenResponseIs404() {
       DynECTApi fail = requestsSendResponses(createSession, createSessionResponse, getPTR, notFound);
-      assertNull(fail.getRecordApiForZone("adrianc.zone.dynecttest.jclouds.org").getPTR(ptrId.getFQDN(), ptrId.getId()));
+      assertNull(fail.getRecordApiForZone("jclouds.org").getPTR(ptrId.getFQDN(), ptrId.getId()));
    }
 
    public void testGetSOAWhenResponseIs2xx() {
       DynECTApi success = requestsSendResponses(createSession, createSessionResponse, getSOA, soaResponse);
-      assertEquals(success.getRecordApiForZone("adrianc.zone.dynecttest.jclouds.org").getSOA(soaId.getFQDN(), soaId.getId()).toString(),
+      assertEquals(success.getRecordApiForZone("jclouds.org").getSOA(soaId.getFQDN(), soaId.getId()).toString(),
                    new GetSOARecordResponseTest().expected().toString());
    }
 
    public void testGetSOAWhenResponseIs404() {
       DynECTApi fail = requestsSendResponses(createSession, createSessionResponse, getSOA, notFound);
-      assertNull(fail.getRecordApiForZone("adrianc.zone.dynecttest.jclouds.org").getSOA(soaId.getFQDN(), soaId.getId()));
+      assertNull(fail.getRecordApiForZone("jclouds.org").getSOA(soaId.getFQDN(), soaId.getId()));
    }
 
    HttpRequest getSRV = HttpRequest.builder().method("GET")
-         .endpoint("https://api2.dynect.net/REST/SRVRecord/adrianc.zone.dynecttest.jclouds.org/adrianc.zone.dynecttest.jclouds.org/50976579")
+         .endpoint("https://api2.dynect.net/REST/SRVRecord/jclouds.org/jclouds.org/50976579")
          .addHeader("API-Version", "3.3.8")
          .addHeader(CONTENT_TYPE, APPLICATION_JSON)
          .addHeader("Auth-Token", authToken).build();   
@@ -250,19 +255,19 @@
          .payload(payloadFromResourceWithContentType("/get_record_srv.json", APPLICATION_JSON)).build();
 
    RecordId srvId = recordIdBuilder()
-        .zone("adrianc.zone.dynecttest.jclouds.org")
-        .fqdn("adrianc.zone.dynecttest.jclouds.org")
+        .zone("jclouds.org")
+        .fqdn("jclouds.org")
         .type("SRV")
         .id(50976579l).build();
 
    public void testGetSRVWhenResponseIs2xx() {
       DynECTApi success = requestsSendResponses(createSession, createSessionResponse, getSRV, srvResponse);
-      assertEquals(success.getRecordApiForZone("adrianc.zone.dynecttest.jclouds.org").getSRV(srvId.getFQDN(), srvId.getId()).toString(),
+      assertEquals(success.getRecordApiForZone("jclouds.org").getSRV(srvId.getFQDN(), srvId.getId()).toString(),
                    new GetSRVRecordResponseTest().expected().toString());
    }
 
    HttpRequest getTXT = HttpRequest.builder().method("GET")
-         .endpoint("https://api2.dynect.net/REST/TXTRecord/adrianc.zone.dynecttest.jclouds.org/adrianc.zone.dynecttest.jclouds.org/50976579")
+         .endpoint("https://api2.dynect.net/REST/TXTRecord/jclouds.org/jclouds.org/50976579")
          .addHeader("API-Version", "3.3.8")
          .addHeader(CONTENT_TYPE, APPLICATION_JSON)
          .addHeader("Auth-Token", authToken).build();   
@@ -271,24 +276,24 @@
          .payload(payloadFromResourceWithContentType("/get_record_txt.json", APPLICATION_JSON)).build();
 
    RecordId txtId = recordIdBuilder()
-        .zone("adrianc.zone.dynecttest.jclouds.org")
-        .fqdn("adrianc.zone.dynecttest.jclouds.org")
+        .zone("jclouds.org")
+        .fqdn("jclouds.org")
         .type("TXT")
         .id(50976579l).build();
 
    public void testGetTXTWhenResponseIs2xx() {
       DynECTApi success = requestsSendResponses(createSession, createSessionResponse, getTXT, txtResponse);
-      assertEquals(success.getRecordApiForZone("adrianc.zone.dynecttest.jclouds.org").getTXT(txtId.getFQDN(), txtId.getId()).toString(),
+      assertEquals(success.getRecordApiForZone("jclouds.org").getTXT(txtId.getFQDN(), txtId.getId()).toString(),
                    new GetTXTRecordResponseTest().expected().toString());
    }
 
    public void testGetTXTWhenResponseIs404() {
       DynECTApi fail = requestsSendResponses(createSession, createSessionResponse, getTXT, notFound);
-      assertNull(fail.getRecordApiForZone("adrianc.zone.dynecttest.jclouds.org").getTXT(txtId.getFQDN(), txtId.getId()));
+      assertNull(fail.getRecordApiForZone("jclouds.org").getTXT(txtId.getFQDN(), txtId.getId()));
    }
 
    HttpRequest list = HttpRequest.builder().method("GET")
-                                 .endpoint("https://api2.dynect.net/REST/AllRecord/adrianc.zone.dynecttest.jclouds.org")
+                                 .endpoint("https://api2.dynect.net/REST/AllRecord/jclouds.org")
                                  .addHeader("API-Version", "3.3.8")
                                  .addHeader(CONTENT_TYPE, APPLICATION_JSON)
                                  .addHeader("Auth-Token", authToken).build();   
@@ -298,7 +303,69 @@
 
    public void testListWhenResponseIs2xx() {
       DynECTApi success = requestsSendResponses(createSession, createSessionResponse, list, listResponse);
-      assertEquals(success.getRecordApiForZone("adrianc.zone.dynecttest.jclouds.org").list().toString(),
+      assertEquals(success.getRecordApiForZone("jclouds.org").list().toString(),
                    new ListRecordsResponseTest().expected().toString());
    }
+
+   HttpRequest listByFQDNAndType = HttpRequest.builder().method("GET")
+                                              .endpoint("https://api2.dynect.net/REST/ARecord/jclouds.org/www.foo.com")
+                                              .addHeader("API-Version", "3.3.8")
+                                              .addHeader(CONTENT_TYPE, APPLICATION_JSON)
+                                              .addHeader("Auth-Token", authToken).build();   
+
+   public void testListByFQDNAndTypeWhenResponseIs2xx() {
+      DynECTApi success = requestsSendResponses(createSession, createSessionResponse, listByFQDNAndType, listResponse);
+      assertEquals(success.getRecordApiForZone("jclouds.org").listByFQDNAndType("www.foo.com", "A").toString(),
+            new ListRecordsResponseTest().expected().toString());
+   }
+
+   HttpRequest create = HttpRequest.builder().method("POST")
+         .endpoint("https://api2.dynect.net/REST/ARecord/jclouds.org/www.jclouds.org")
+         .addHeader("API-Version", "3.3.8")
+         .addHeader(ACCEPT, APPLICATION_JSON)
+         .addHeader("Auth-Token", authToken)
+         .payload(stringPayload("{\"rdata\":{\"address\":\"1.1.1.1\"},\"ttl\":86400}"))
+         .build();   
+
+   HttpResponse createResponse = HttpResponse.builder().statusCode(200)
+         .payload(payloadFromResourceWithContentType("/new_record.json", APPLICATION_JSON)).build();
+
+   public void testCreateWhenResponseIs2xx() {
+      DynECTApi success = requestsSendResponses(createSession, createSessionResponse, create, createResponse);
+      CreateRecord<AData> record = CreateRecord.<AData> builder()
+                                               .fqdn("www.jclouds.org")
+                                               .type("A")
+                                               .ttl(86400)
+                                               .rdata(a("1.1.1.1"))
+                                               .build();
+      assertEquals(success.getRecordApiForZone("jclouds.org").scheduleCreate(record), Job.success(285372440l));
+   }
+
+   HttpRequest delete = HttpRequest.builder().method("DELETE")
+                                             .endpoint("https://api2.dynect.net/REST/ARecord/jclouds.org/www.jclouds.org/285372440")
+                                             .addHeader("API-Version", "3.3.8")
+                                             .addHeader(ACCEPT, APPLICATION_JSON)
+                                             .addHeader(CONTENT_TYPE, APPLICATION_JSON)
+                                             .addHeader("Auth-Token", authToken).build();  
+
+   HttpResponse deleteResponse = HttpResponse.builder().statusCode(200)
+         .payload(payloadFromResourceWithContentType("/delete_record.json", APPLICATION_JSON)).build();
+
+   RecordId id = recordIdBuilder()
+                     .zone("jclouds.org")
+                     .fqdn("www.jclouds.org")
+                     .type("A")
+                     .id(285372440l)
+                     .build();
+
+   public void testDeleteWhenResponseIs2xx() {
+      DynECTApi success = requestsSendResponses(createSession, createSessionResponse, delete, deleteResponse);
+
+      assertEquals(success.getRecordApiForZone("jclouds.org").scheduleDelete(id), Job.success(285372457l));
+   }
+
+   public void testDeleteWhenResponseIs404() {
+      DynECTApi fail = requestsSendResponses(createSession, createSessionResponse, delete, notFound);
+      assertNull(fail.getRecordApiForZone("jclouds.org").scheduleDelete(id));
+   }
 }
diff --git a/labs/dynect/src/test/java/org/jclouds/dynect/v3/features/RecordApiLiveTest.java b/labs/dynect/src/test/java/org/jclouds/dynect/v3/features/RecordApiLiveTest.java
index 16fb2e2..9c58679 100644
--- a/labs/dynect/src/test/java/org/jclouds/dynect/v3/features/RecordApiLiveTest.java
+++ b/labs/dynect/src/test/java/org/jclouds/dynect/v3/features/RecordApiLiveTest.java
@@ -20,14 +20,21 @@
 
 import static com.google.common.base.Preconditions.checkNotNull;
 import static java.util.logging.Logger.getAnonymousLogger;
+import static org.jclouds.dynect.v3.domain.rdata.AData.a;
 import static org.testng.Assert.assertEquals;
 import static org.testng.Assert.assertTrue;
 
 import java.util.Map;
 
+import org.jclouds.JcloudsVersion;
+import org.jclouds.dynect.v3.DynECTExceptions.JobStillRunningException;
+import org.jclouds.dynect.v3.domain.CreateRecord;
+import org.jclouds.dynect.v3.domain.Job;
+import org.jclouds.dynect.v3.domain.Job.Status;
 import org.jclouds.dynect.v3.domain.Record;
 import org.jclouds.dynect.v3.domain.RecordId;
 import org.jclouds.dynect.v3.domain.SOARecord;
+import org.jclouds.dynect.v3.domain.Zone;
 import org.jclouds.dynect.v3.domain.rdata.AAAAData;
 import org.jclouds.dynect.v3.domain.rdata.AData;
 import org.jclouds.dynect.v3.domain.rdata.CNAMEData;
@@ -38,6 +45,7 @@
 import org.jclouds.dynect.v3.domain.rdata.SRVData;
 import org.jclouds.dynect.v3.domain.rdata.TXTData;
 import org.jclouds.dynect.v3.internal.BaseDynECTApiLiveTest;
+import org.testng.annotations.AfterClass;
 import org.testng.annotations.Test;
 
 import com.google.common.collect.ImmutableList;
@@ -64,8 +72,8 @@
 
    @Test
    protected void testListAndGetRecords() {
-      for (String zone : context.getApi().getZoneApi().list()) {
-         RecordApi api = context.getApi().getRecordApiForZone(zone);
+      for (String zone : zoneApi().list()) {
+         RecordApi api = api(zone);
          ImmutableList<RecordId> records = api.list().toList();
          getAnonymousLogger().info("zone: " + zone + " record count: " + records.size());
 
@@ -162,4 +170,85 @@
       checkNotNull(rdata.getTxtdata(), "rdata.txtdata cannot be null for TXTRecord: %s", record);
       return record;
    }
+
+   String zoneFQDN = System.getProperty("user.name").replace('.', '-') + ".record.dynecttest.jclouds.org";
+   String contact = JcloudsVersion.get() + ".jclouds.org";
+
+   private void createZone() {
+      Job job = zoneApi().scheduleCreateWithContact(zoneFQDN, contact);
+      checkNotNull(job, "unable to create zone %s", zoneFQDN);
+      getAnonymousLogger().info("created zone: " + job);
+      assertEquals(job.getStatus(), Status.SUCCESS);
+      assertEquals(context.getApi().getJob(job.getId()), job);
+      Zone zone = zoneApi().publish(zoneFQDN);
+      checkNotNull(zone, "unable to publish zone %s", zoneFQDN);
+      getAnonymousLogger().info("published zone: " + zone);
+   }
+
+   String fqdn = "www." + zoneFQDN;
+   CreateRecord<AData> record = CreateRecord.<AData> builder()
+                                            .fqdn("www." + zoneFQDN)
+                                            .type("A")
+                                            .ttl(86400)
+                                            .rdata(a("1.1.1.1"))
+                                            .build();
+
+   public void testCreateRecord() {
+      createZone();
+
+      Job job = null;
+      while (true) {
+         try {
+            job = api(zoneFQDN).scheduleCreate(record);
+            break;
+         } catch (JobStillRunningException e) {
+            continue;
+         }
+      }
+
+      checkNotNull(job, "unable to create record %s", record);
+      getAnonymousLogger().info("created record: " + job);
+      assertEquals(job.getStatus(), Status.SUCCESS);
+      assertEquals(context.getApi().getJob(job.getId()), job);
+      zoneApi().publish(zoneFQDN);
+   }
+
+   RecordId id;
+
+   @Test(dependsOnMethods = "testCreateRecord")
+   public void testListByFQDNAndType() {
+      id = api(zoneFQDN).listByFQDNAndType(record.getFQDN(), record.getType()).toList().get(0);
+      getAnonymousLogger().info(id.toString());
+      Record<? extends Map<String, Object>> newRecord = api(zoneFQDN).get(id);
+      assertEquals(newRecord.getFQDN(), record.getFQDN());
+      assertEquals(newRecord.getType(), record.getType());
+      assertEquals(newRecord.getTTL(), record.getTTL());
+      assertEquals(newRecord.getRData(), record.getRData());
+      checkRecord(newRecord);
+   }
+
+   @Test(dependsOnMethods = "testListByFQDNAndType")
+   public void testDeleteRecord() {
+      Job job = api(zoneFQDN).scheduleDelete(id);
+      checkNotNull(job, "unable to delete record %s", id);
+      getAnonymousLogger().info("deleted record: " + job);
+      assertEquals(job.getStatus(), Status.SUCCESS);
+      assertEquals(context.getApi().getJob(job.getId()), job);
+      zoneApi().publish(zoneFQDN);
+   }
+
+   protected RecordApi api(String zoneFQDN) {
+      return context.getApi().getRecordApiForZone(zoneFQDN);
+   }
+
+   protected ZoneApi zoneApi() {
+      return context.getApi().getZoneApi();
+   }
+
+   @Override
+   @AfterClass(groups = "live", alwaysRun = true)
+   protected void tearDownContext() {
+      zoneApi().delete(zoneFQDN);
+      super.tearDownContext();
+   }
 }
diff --git a/labs/dynect/src/test/java/org/jclouds/dynect/v3/features/ZoneApiExpectTest.java b/labs/dynect/src/test/java/org/jclouds/dynect/v3/features/ZoneApiExpectTest.java
index c5ca17c..e6cf359 100644
--- a/labs/dynect/src/test/java/org/jclouds/dynect/v3/features/ZoneApiExpectTest.java
+++ b/labs/dynect/src/test/java/org/jclouds/dynect/v3/features/ZoneApiExpectTest.java
@@ -26,6 +26,7 @@
 
 import org.jclouds.dynect.v3.DynECTApi;
 import org.jclouds.dynect.v3.domain.CreatePrimaryZone;
+import org.jclouds.dynect.v3.domain.Job;
 import org.jclouds.dynect.v3.internal.BaseDynECTApiExpectTest;
 import org.jclouds.dynect.v3.parse.DeleteZoneChangesResponseTest;
 import org.jclouds.dynect.v3.parse.DeleteZoneResponseTest;
@@ -54,27 +55,29 @@
       assertEquals(success.getZoneApi().get("jclouds.org").toString(),
                    new GetZoneResponseTest().expected().toString());
    }
-   
+
    HttpRequest create = HttpRequest.builder().method("POST")
          .endpoint("https://api2.dynect.net/REST/Zone/jclouds.org")
          .addHeader("API-Version", "3.3.8")
+         .addHeader(ACCEPT, APPLICATION_JSON)
          .addHeader("Auth-Token", authToken)
          .payload(stringPayload("{\"rname\":\"jimmy@jclouds.org\",\"serial_style\":\"increment\",\"ttl\":3600}"))
          .build();   
 
+   HttpResponse createResponse = HttpResponse.builder().statusCode(200)
+         .payload(payloadFromResourceWithContentType("/new_zone.json", APPLICATION_JSON)).build();
+
    public void testCreateWhenResponseIs2xx() {
-      DynECTApi success = requestsSendResponses(createSession, createSessionResponse, create, getResponse);
-      assertEquals(success.getZoneApi().create(CreatePrimaryZone.builder()
-                                                                .fqdn("jclouds.org")
-                                                                .contact("jimmy@jclouds.org")
-                                                                .build()).toString(),
-                   new GetZoneResponseTest().expected().toString());
+      DynECTApi success = requestsSendResponses(createSession, createSessionResponse, create, createResponse);
+      assertEquals(success.getZoneApi().scheduleCreate(CreatePrimaryZone.builder()
+                                                                        .fqdn("jclouds.org")
+                                                                        .contact("jimmy@jclouds.org")
+                                                                        .build()), Job.success(285351593l));
    }
 
    public void testCreateWithContactWhenResponseIs2xx() {
-      DynECTApi success = requestsSendResponses(createSession, createSessionResponse, create, getResponse);
-      assertEquals(success.getZoneApi().createWithContact("jclouds.org", "jimmy@jclouds.org").toString(),
-                   new GetZoneResponseTest().expected().toString());
+      DynECTApi success = requestsSendResponses(createSession, createSessionResponse, create, createResponse);
+      assertEquals(success.getZoneApi().scheduleCreateWithContact("jclouds.org", "jimmy@jclouds.org"), Job.success(285351593l));
    }
 
    public void testGetWhenResponseIs404() {
diff --git a/labs/dynect/src/test/java/org/jclouds/dynect/v3/features/ZoneApiLiveTest.java b/labs/dynect/src/test/java/org/jclouds/dynect/v3/features/ZoneApiLiveTest.java
index 231c9f5..afd3984 100644
--- a/labs/dynect/src/test/java/org/jclouds/dynect/v3/features/ZoneApiLiveTest.java
+++ b/labs/dynect/src/test/java/org/jclouds/dynect/v3/features/ZoneApiLiveTest.java
@@ -67,14 +67,15 @@
    }
 
    String fqdn = System.getProperty("user.name").replace('.', '-') + ".zone.dynecttest.jclouds.org";
-   String contact = JcloudsVersion.get() + "@jclouds.org";
+   String contact = JcloudsVersion.get() + ".jclouds.org";
 
    @Test
    public void testCreateZone() {
-      Zone zone = api().createWithContact(fqdn, contact);
-      checkNotNull(zone, "unable to create zone %s", fqdn);
-      getAnonymousLogger().info("created zone: " + zone);
-      checkZone(zone);
+      Job job = api().scheduleCreateWithContact(fqdn, contact);
+      checkNotNull(job, "unable to create zone %s", fqdn);
+      getAnonymousLogger().info("created zone: " + job);
+      assertEquals(job.getStatus(), Status.SUCCESS);
+      assertEquals(context.getApi().getJob(job.getId()), job);
    }
 
    @Test(dependsOnMethods = "testCreateZone")
@@ -121,7 +122,7 @@
    }
 
    @Override
-   @AfterClass(groups = "live")
+   @AfterClass(groups = "live", alwaysRun = true)
    protected void tearDownContext() {
       api().delete(fqdn);
       super.tearDownContext();
diff --git a/labs/dynect/src/test/resources/delete_record.json b/labs/dynect/src/test/resources/delete_record.json
new file mode 100644
index 0000000..af0bf96
--- /dev/null
+++ b/labs/dynect/src/test/resources/delete_record.json
@@ -0,0 +1 @@
+{"status": "success", "data": {}, "job_id": 285372457, "msgs": [{"INFO": "delete: Record will be deleted on zone publish", "SOURCE": "BLL", "ERR_CD": null, "LVL": "INFO"}, {"INFO": "remove_node: www.adriancole.zone.dynecttest.jclouds.org removed from tree.", "SOURCE": "BLL", "ERR_CD": null, "LVL": "INFO"}]}
\ No newline at end of file
diff --git a/labs/dynect/src/test/resources/new_record.json b/labs/dynect/src/test/resources/new_record.json
new file mode 100644
index 0000000..99a3c49
--- /dev/null
+++ b/labs/dynect/src/test/resources/new_record.json
@@ -0,0 +1 @@
+{"status": "success", "data": {"zone": "adriancole.zone.dynecttest.jclouds.org", "ttl": 86400, "fqdn": "www.adriancole.zone.dynecttest.jclouds.org", "record_type": "A", "rdata": {"address": "1.1.1.1"}, "record_id": 0}, "job_id": 285372440, "msgs": [{"INFO": "add: Record added", "SOURCE": "BLL", "ERR_CD": null, "LVL": "INFO"}]}
\ No newline at end of file
diff --git a/labs/dynect/src/test/resources/new_zone.json b/labs/dynect/src/test/resources/new_zone.json
new file mode 100644
index 0000000..1fbbafb
--- /dev/null
+++ b/labs/dynect/src/test/resources/new_zone.json
@@ -0,0 +1 @@
+{"status": "success", "data": {"zone_type": "Primary", "serial_style": "increment", "serial": 0, "zone": "adriancole.zone.dynecttest.jclouds.org"}, "job_id": 285351593, "msgs": [{"INFO": "create: New zone adriancole.zone.dynecttest.jclouds.org created.  Publish it to put it on our server.", "SOURCE": "BLL", "ERR_CD": null, "LVL": "INFO"}, {"INFO": "setup: If you plan to provide your own secondary DNS for the zone, allow notify requests from these IP addresses on your nameserver: 204.13.249.66, 208.78.68.66, 2600:2001:0:1::66, 2600:2003:0:1::66", "SOURCE": "BLL", "ERR_CD": null, "LVL": "INFO"}]}
\ No newline at end of file