You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@jclouds.apache.org by na...@apache.org on 2015/07/13 00:03:31 UTC

[43/47] jclouds-labs git commit: JCLOUDS-613: Implement the DigitalOcean v2 API

http://git-wip-us.apache.org/repos/asf/jclouds-labs/blob/a141b21b/digitalocean2/src/test/java/org/jclouds/digitalocean2/compute/functions/ImageToImageTest.java
----------------------------------------------------------------------
diff --git a/digitalocean2/src/test/java/org/jclouds/digitalocean2/compute/functions/ImageToImageTest.java b/digitalocean2/src/test/java/org/jclouds/digitalocean2/compute/functions/ImageToImageTest.java
new file mode 100644
index 0000000..6ab020c
--- /dev/null
+++ b/digitalocean2/src/test/java/org/jclouds/digitalocean2/compute/functions/ImageToImageTest.java
@@ -0,0 +1,57 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.jclouds.digitalocean2.compute.functions;
+
+import static org.jclouds.compute.domain.Image.Status.AVAILABLE;
+import static org.testng.Assert.assertEquals;
+
+import java.util.Date;
+
+import org.jclouds.compute.domain.ImageBuilder;
+import org.jclouds.compute.domain.OperatingSystem;
+import org.jclouds.compute.domain.OsFamily;
+import org.jclouds.digitalocean2.domain.Image;
+import org.testng.annotations.Test;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+
+@Test(groups = "unit", testName = "ImageToImageTest")
+public class ImageToImageTest {
+
+   @Test
+   public void testConvertImage() {
+      Image image = Image.create(1, "14.04 x64", "distribution", "Ubuntu", "ubuntu-1404-x86", true,
+            ImmutableList.of("sfo1"), new Date());
+      org.jclouds.compute.domain.Image expected = new ImageBuilder()
+            .id("ubuntu-1404-x86")
+            .providerId("1")
+            .name("14.04 x64")
+            .description("Ubuntu 14.04 x64")
+            .status(AVAILABLE)
+            .operatingSystem(
+                  OperatingSystem.builder().name("Ubuntu").description("Ubuntu 14.04 x64").family(OsFamily.UBUNTU)
+                        .version("14.04").arch("x64").is64Bit(true).build())
+            .userMetadata(ImmutableMap.of("publicImage", "true")).build();
+
+      org.jclouds.compute.domain.Image result = new ImageToImage().apply(image);
+      assertEquals(result, expected);
+      assertEquals(result.getDescription(), expected.getDescription());
+      assertEquals(result.getOperatingSystem(), expected.getOperatingSystem());
+      assertEquals(result.getStatus(), expected.getStatus());
+   }
+}

http://git-wip-us.apache.org/repos/asf/jclouds-labs/blob/a141b21b/digitalocean2/src/test/java/org/jclouds/digitalocean2/compute/functions/RegionToLocationTest.java
----------------------------------------------------------------------
diff --git a/digitalocean2/src/test/java/org/jclouds/digitalocean2/compute/functions/RegionToLocationTest.java b/digitalocean2/src/test/java/org/jclouds/digitalocean2/compute/functions/RegionToLocationTest.java
new file mode 100644
index 0000000..879091b
--- /dev/null
+++ b/digitalocean2/src/test/java/org/jclouds/digitalocean2/compute/functions/RegionToLocationTest.java
@@ -0,0 +1,52 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.jclouds.digitalocean2.compute.functions;
+
+import static com.google.common.collect.Iterables.getOnlyElement;
+import static org.testng.Assert.assertEquals;
+
+import java.net.URI;
+
+import org.jclouds.digitalocean2.DigitalOcean2ProviderMetadata;
+import org.jclouds.digitalocean2.domain.Region;
+import org.jclouds.domain.Location;
+import org.jclouds.domain.LocationBuilder;
+import org.jclouds.domain.LocationScope;
+import org.jclouds.location.suppliers.all.JustProvider;
+import org.testng.annotations.Test;
+
+import com.google.common.base.Suppliers;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
+
+@Test(groups = "unit", testName = "RegionToLocationTest")
+public class RegionToLocationTest {
+
+   @Test
+   public void testConvertRegion() {
+      DigitalOcean2ProviderMetadata metadata = new DigitalOcean2ProviderMetadata();
+      JustProvider locationsSupplier = new JustProvider(metadata.getId(), Suppliers.<URI> ofInstance(URI
+            .create(metadata.getEndpoint())), ImmutableSet.<String> of());
+
+      Region region = Region.create("reg1", "Region1", ImmutableList.<String> of(), true, ImmutableList.<String> of());
+      Location expected = new LocationBuilder().id("reg1").description("reg1/Region 1")
+            .parent(getOnlyElement(locationsSupplier.get())).scope(LocationScope.REGION).build();
+
+      RegionToLocation function = new RegionToLocation(locationsSupplier);
+      assertEquals(function.apply(region), expected);
+   }
+}

http://git-wip-us.apache.org/repos/asf/jclouds-labs/blob/a141b21b/digitalocean2/src/test/java/org/jclouds/digitalocean2/compute/functions/SizeToHardwareTest.java
----------------------------------------------------------------------
diff --git a/digitalocean2/src/test/java/org/jclouds/digitalocean2/compute/functions/SizeToHardwareTest.java b/digitalocean2/src/test/java/org/jclouds/digitalocean2/compute/functions/SizeToHardwareTest.java
new file mode 100644
index 0000000..cba55bf
--- /dev/null
+++ b/digitalocean2/src/test/java/org/jclouds/digitalocean2/compute/functions/SizeToHardwareTest.java
@@ -0,0 +1,49 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.jclouds.digitalocean2.compute.functions;
+
+import static org.testng.Assert.assertEquals;
+
+import org.jclouds.compute.domain.Hardware;
+import org.jclouds.compute.domain.HardwareBuilder;
+import org.jclouds.compute.domain.Processor;
+import org.jclouds.compute.domain.Volume.Type;
+import org.jclouds.compute.domain.VolumeBuilder;
+import org.jclouds.digitalocean2.domain.Size;
+import org.testng.annotations.Test;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+
+/**
+ * Unit tests for the {@link SizeToHardware} class.
+ */
+@Test(groups = "unit", testName = "SizeToHardwareTest")
+public class SizeToHardwareTest {
+
+   @Test
+   public void testConvertSize() {
+      Size size = Size.create("2gb", true, 1.0f, 10f, 0.05f, 2048, 1, 20, ImmutableList.<String> of());
+      Hardware expected = new HardwareBuilder().id("2gb").providerId("2gb").name("2gb")
+            .processor(new Processor(1.0, 1.0)).ram(2048)
+            .volume(new VolumeBuilder().size(20f).type(Type.LOCAL).build())
+            .userMetadata(ImmutableMap.of("costPerHour", "0.05", "costPerMonth", "10")).build();
+
+      SizeToHardware function = new SizeToHardware();
+      assertEquals(function.apply(size), expected);
+   }
+}

http://git-wip-us.apache.org/repos/asf/jclouds-labs/blob/a141b21b/digitalocean2/src/test/java/org/jclouds/digitalocean2/compute/functions/TemplateOptionsToStatementWithoutPublicKeyTest.java
----------------------------------------------------------------------
diff --git a/digitalocean2/src/test/java/org/jclouds/digitalocean2/compute/functions/TemplateOptionsToStatementWithoutPublicKeyTest.java b/digitalocean2/src/test/java/org/jclouds/digitalocean2/compute/functions/TemplateOptionsToStatementWithoutPublicKeyTest.java
new file mode 100644
index 0000000..c3a6cd2
--- /dev/null
+++ b/digitalocean2/src/test/java/org/jclouds/digitalocean2/compute/functions/TemplateOptionsToStatementWithoutPublicKeyTest.java
@@ -0,0 +1,75 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.jclouds.digitalocean2.compute.functions;
+
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertNull;
+import static org.testng.Assert.assertTrue;
+
+import java.util.Map;
+
+import org.jclouds.compute.options.TemplateOptions;
+import org.jclouds.scriptbuilder.domain.OsFamily;
+import org.jclouds.scriptbuilder.domain.Statement;
+import org.jclouds.scriptbuilder.domain.StatementList;
+import org.jclouds.scriptbuilder.statements.ssh.InstallRSAPrivateKey;
+import org.jclouds.ssh.SshKeys;
+import org.testng.annotations.Test;
+
+/**
+ * Unit tests for the {@link TemplateOptionsToStatementWithoutPublicKey} class.
+ */
+@Test(groups = "unit", testName = "TemplateOptionsToStatementWithoutPublicKeyTest")
+public class TemplateOptionsToStatementWithoutPublicKeyTest {
+
+   @Test
+   public void testPublicKeyDoesNotGenerateAuthorizePublicKeyStatementIfOnlyPublicKeyOptionsConfigured() {
+      Map<String, String> keys = SshKeys.generate();
+      TemplateOptions options = TemplateOptions.Builder.authorizePublicKey(keys.get("public"));
+
+      TemplateOptionsToStatementWithoutPublicKey function = new TemplateOptionsToStatementWithoutPublicKey();
+      assertNull(function.apply(options));
+   }
+
+   @Test
+   public void testPublicAndRunScriptKeyDoesNotGenerateAuthorizePublicKeyStatementIfRunScriptPresent() {
+      Map<String, String> keys = SshKeys.generate();
+      TemplateOptions options = TemplateOptions.Builder.authorizePublicKey(keys.get("public")).runScript("uptime");
+
+      TemplateOptionsToStatementWithoutPublicKey function = new TemplateOptionsToStatementWithoutPublicKey();
+      Statement statement = function.apply(options);
+
+      assertEquals(statement.render(OsFamily.UNIX), "uptime\n");
+   }
+
+   @Test
+   public void testPublicAndPrivateKeyAndRunScriptDoesNotGenerateAuthorizePublicKeyStatementIfOtherOptionsPresent() {
+      Map<String, String> keys = SshKeys.generate();
+      TemplateOptions options = TemplateOptions.Builder.authorizePublicKey(keys.get("public"))
+            .installPrivateKey(keys.get("private")).runScript("uptime");
+
+      TemplateOptionsToStatementWithoutPublicKey function = new TemplateOptionsToStatementWithoutPublicKey();
+      Statement statement = function.apply(options);
+
+      assertTrue(statement instanceof StatementList);
+      StatementList statements = (StatementList) statement;
+
+      assertEquals(statements.size(), 2);
+      assertEquals(statements.get(0).render(OsFamily.UNIX), "uptime\n");
+      assertTrue(statements.get(1) instanceof InstallRSAPrivateKey);
+   }
+}

http://git-wip-us.apache.org/repos/asf/jclouds-labs/blob/a141b21b/digitalocean2/src/test/java/org/jclouds/digitalocean2/compute/options/DigitalOcean2TemplateOptionsTest.java
----------------------------------------------------------------------
diff --git a/digitalocean2/src/test/java/org/jclouds/digitalocean2/compute/options/DigitalOcean2TemplateOptionsTest.java b/digitalocean2/src/test/java/org/jclouds/digitalocean2/compute/options/DigitalOcean2TemplateOptionsTest.java
new file mode 100644
index 0000000..982224c
--- /dev/null
+++ b/digitalocean2/src/test/java/org/jclouds/digitalocean2/compute/options/DigitalOcean2TemplateOptionsTest.java
@@ -0,0 +1,52 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.jclouds.digitalocean2.compute.options;
+
+import static org.testng.Assert.assertEquals;
+
+import org.jclouds.compute.options.TemplateOptions;
+import org.testng.annotations.Test;
+
+import com.google.common.collect.ImmutableSet;
+
+@Test(groups = "unit", testName = "DigitalOcean2TemplateOptionsTest")
+public class DigitalOcean2TemplateOptionsTest {
+
+   @Test
+   public void testSShKeyIds() {
+      TemplateOptions options = new DigitalOcean2TemplateOptions().sshKeyIds(ImmutableSet.of(1, 2, 3));
+      assertEquals(options.as(DigitalOcean2TemplateOptions.class).getSshKeyIds(), ImmutableSet.of(1, 2, 3));
+   }
+
+   @Test
+   public void testPrivateNetworking() {
+      TemplateOptions options = new DigitalOcean2TemplateOptions().privateNetworking(true);
+      assertEquals(options.as(DigitalOcean2TemplateOptions.class).getPrivateNetworking(), true);
+   }
+
+   @Test
+   public void testBackupsEnabled() {
+      TemplateOptions options = new DigitalOcean2TemplateOptions().backupsEnabled(true);
+      assertEquals(options.as(DigitalOcean2TemplateOptions.class).getBackupsEnabled(), true);
+   }
+   
+   @Test
+   public void testAutoCreateKeyPair() {
+      TemplateOptions options = new DigitalOcean2TemplateOptions().autoCreateKeyPair(false);
+      assertEquals(options.as(DigitalOcean2TemplateOptions.class).getAutoCreateKeyPair(), false);
+   }
+}

http://git-wip-us.apache.org/repos/asf/jclouds-labs/blob/a141b21b/digitalocean2/src/test/java/org/jclouds/digitalocean2/domain/OperatingSystemTest.java
----------------------------------------------------------------------
diff --git a/digitalocean2/src/test/java/org/jclouds/digitalocean2/domain/OperatingSystemTest.java b/digitalocean2/src/test/java/org/jclouds/digitalocean2/domain/OperatingSystemTest.java
new file mode 100644
index 0000000..d6bd0fc
--- /dev/null
+++ b/digitalocean2/src/test/java/org/jclouds/digitalocean2/domain/OperatingSystemTest.java
@@ -0,0 +1,104 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.jclouds.digitalocean2.domain;
+
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertFalse;
+import static org.testng.Assert.assertTrue;
+
+import org.testng.annotations.Test;
+
+@Test(groups = "unit", testName = "OperatingSystemTest")
+public class OperatingSystemTest {
+
+   public void testParseStandard64bit() {
+      OperatingSystem os = OperatingSystem.create("12.10 x64", "Ubuntu");
+
+      assertEquals(os.distribution(), Distribution.UBUNTU);
+      assertEquals(os.version(), "12.10");
+      assertEquals(os.arch(), "x64");
+      assertTrue(os.is64bit());
+   }
+
+   public void testLongVersionStandard64bit() {
+      OperatingSystem os = OperatingSystem.create("12.10.1 x64", "Ubuntu");
+
+      assertEquals(os.distribution(), Distribution.UBUNTU);
+      assertEquals(os.version(), "12.10.1");
+      assertEquals(os.arch(), "x64");
+      assertTrue(os.is64bit());
+   }
+
+   public void testParseStandard64bitWithPrefix() {
+      OperatingSystem os = OperatingSystem.create("Arch Linux 12.10 x64 Desktop", "Arch Linux");
+
+      assertEquals(os.distribution(), Distribution.ARCHLINUX);
+      assertEquals(os.version(), "12.10");
+      assertEquals(os.arch(), "x64");
+      assertTrue(os.is64bit());
+   }
+
+   public void testParseStandard() {
+      OperatingSystem os = OperatingSystem.create("12.10 x32", "Ubuntu");
+
+      assertEquals(os.distribution(), Distribution.UBUNTU);
+      assertEquals(os.version(), "12.10");
+      assertEquals(os.arch(), "x32");
+      assertFalse(os.is64bit());
+
+      os = OperatingSystem.create("6.5 x64", "CentOS");
+
+      assertEquals(os.distribution(), Distribution.CENTOS);
+      assertEquals(os.version(), "6.5");
+      assertEquals(os.arch(), "x64");
+      assertTrue(os.is64bit());
+
+      os = OperatingSystem.create("6.5 x64", "Centos");
+
+      assertEquals(os.distribution(), Distribution.CENTOS);
+      assertEquals(os.version(), "6.5");
+      assertEquals(os.arch(), "x64");
+      assertTrue(os.is64bit());
+   }
+
+   public void testParseNoArch() {
+      OperatingSystem os = OperatingSystem.create("12.10", "Ubuntu");
+
+      assertEquals(os.distribution(), Distribution.UBUNTU);
+      assertEquals(os.version(), "12.10");
+      assertEquals(os.arch(), "");
+      assertFalse(os.is64bit());
+   }
+
+   public void testParseNoVersion() {
+      OperatingSystem os = OperatingSystem.create("x64", "Ubuntu");
+
+      assertEquals(os.distribution(), Distribution.UBUNTU);
+      assertEquals(os.version(), "");
+      assertEquals(os.arch(), "x64");
+      assertTrue(os.is64bit());
+   }
+
+   public void testParseUnknownDistribution() {
+      OperatingSystem os = OperatingSystem.create("12.04 x64", "Foo");
+
+      assertEquals(os.distribution(), Distribution.UNRECOGNIZED);
+      assertEquals(os.version(), "12.04");
+      assertEquals(os.arch(), "x64");
+      assertTrue(os.is64bit());
+   }
+}

http://git-wip-us.apache.org/repos/asf/jclouds-labs/blob/a141b21b/digitalocean2/src/test/java/org/jclouds/digitalocean2/features/ActionApiLiveTest.java
----------------------------------------------------------------------
diff --git a/digitalocean2/src/test/java/org/jclouds/digitalocean2/features/ActionApiLiveTest.java b/digitalocean2/src/test/java/org/jclouds/digitalocean2/features/ActionApiLiveTest.java
new file mode 100644
index 0000000..44a9a17
--- /dev/null
+++ b/digitalocean2/src/test/java/org/jclouds/digitalocean2/features/ActionApiLiveTest.java
@@ -0,0 +1,70 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.jclouds.digitalocean2.features;
+
+import static org.jclouds.digitalocean2.domain.options.ImageListOptions.Builder.page;
+import static org.testng.Assert.assertNotNull;
+import static org.testng.Assert.assertTrue;
+import static org.testng.util.Strings.isNullOrEmpty;
+
+import java.util.concurrent.atomic.AtomicInteger;
+
+import org.jclouds.digitalocean2.domain.Action;
+import org.jclouds.digitalocean2.internal.BaseDigitalOcean2ApiLiveTest;
+import org.testng.annotations.Test;
+
+import com.google.common.base.Optional;
+import com.google.common.base.Predicate;
+
+@Test(groups = "live", testName = "ActionApiLiveTest")
+public class ActionApiLiveTest extends BaseDigitalOcean2ApiLiveTest {
+
+   public void testListActions() {
+      final AtomicInteger found = new AtomicInteger(0);
+      // DigitalOcean return 25 records per page by default. Inspect at most 2 pages
+      assertTrue(api().list().concat().limit(50).allMatch(new Predicate<Action>() {
+         @Override
+         public boolean apply(Action input) {
+            found.incrementAndGet();
+            return !isNullOrEmpty(input.type());
+         }
+      }), "All actions must have the 'type' field populated");
+      assertTrue(found.get() > 0, "Expected some actions to be returned");
+   }
+   
+   public void testListActionsOnePage() {
+      final AtomicInteger found = new AtomicInteger(0);
+      assertTrue(api().list(page(1).perPage(5)).allMatch(new Predicate<Action>() {
+         @Override
+         public boolean apply(Action input) {
+            found.incrementAndGet();
+            return !isNullOrEmpty(input.type());
+         }
+      }), "All actions must have the 'type' field populated");
+      assertTrue(found.get() > 0, "Expected some actions to be returned");
+   }
+   
+   public void testGetAction() {
+      Optional<Action> first = api().list().concat().first();
+      assertTrue(first.isPresent(), "At least one action was expected to exist");
+      assertNotNull(api().get(first.get().id()));
+   }
+   
+   private ActionApi api() {
+      return api.actionApi();
+   }
+}

http://git-wip-us.apache.org/repos/asf/jclouds-labs/blob/a141b21b/digitalocean2/src/test/java/org/jclouds/digitalocean2/features/ActionApiMockTest.java
----------------------------------------------------------------------
diff --git a/digitalocean2/src/test/java/org/jclouds/digitalocean2/features/ActionApiMockTest.java b/digitalocean2/src/test/java/org/jclouds/digitalocean2/features/ActionApiMockTest.java
new file mode 100644
index 0000000..aa890d5
--- /dev/null
+++ b/digitalocean2/src/test/java/org/jclouds/digitalocean2/features/ActionApiMockTest.java
@@ -0,0 +1,110 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.jclouds.digitalocean2.features;
+
+import static com.google.common.collect.Iterables.isEmpty;
+import static com.google.common.collect.Iterables.size;
+import static org.jclouds.digitalocean2.domain.options.ListOptions.Builder.page;
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertNull;
+import static org.testng.Assert.assertTrue;
+
+import java.util.Map;
+
+import org.jclouds.digitalocean2.domain.Action;
+import org.jclouds.digitalocean2.internal.BaseDigitalOcean2ApiMockTest;
+import org.testng.annotations.Test;
+
+import com.google.common.reflect.TypeToken;
+
+@Test(groups = "unit", testName = "ActionApiMockTest", singleThreaded = true)
+public class ActionApiMockTest extends BaseDigitalOcean2ApiMockTest {
+
+   public void testListActions() throws InterruptedException {
+      server.enqueue(jsonResponse("/actions-first.json"));
+      server.enqueue(jsonResponse("/actions-last.json"));
+
+      Iterable<Action> actions = api.actionApi().list().concat();
+
+      assertEquals(size(actions), 8); // Force the PagedIterable to advance
+      assertEquals(server.getRequestCount(), 2);
+
+      assertSent(server, "GET", "/actions");
+      assertSent(server, "GET", "/actions?page=2&per_page=5");
+   }
+
+   public void testListActionsReturns404() throws InterruptedException {
+      server.enqueue(response404());
+
+      Iterable<Action> actions = api.actionApi().list().concat();
+
+      assertTrue(isEmpty(actions));
+
+      assertEquals(server.getRequestCount(), 1);
+      assertSent(server, "GET", "/actions");
+   }
+
+   public void testListActionsWithOptions() throws InterruptedException {
+      server.enqueue(jsonResponse("/actions-first.json"));
+
+      Iterable<Action> actions = api.actionApi().list(page(1).perPage(5));
+
+      assertEquals(size(actions), 5);
+      assertEquals(server.getRequestCount(), 1);
+
+      assertSent(server, "GET", "/actions?page=1&per_page=5");
+   }
+
+   public void testListActionsWithOptionsReturns404() throws InterruptedException {
+      server.enqueue(response404());
+
+      Iterable<Action> actions = api.actionApi().list(page(1).perPage(5));
+
+      assertTrue(isEmpty(actions));
+
+      assertEquals(server.getRequestCount(), 1);
+      assertSent(server, "GET", "/actions?page=1&per_page=5");
+   }
+   
+   public void testGetAction() throws InterruptedException {
+      server.enqueue(jsonResponse("/action.json"));
+
+      Action action = api.actionApi().get(1);
+
+      assertEquals(action, actionFromResource("/action.json"));
+      
+      assertEquals(server.getRequestCount(), 1);
+      assertSent(server, "GET", "/actions/1");
+   }
+
+   public void testGetActionReturns404() throws InterruptedException {
+      server.enqueue(response404());
+
+      Action action = api.actionApi().get(1);
+
+      assertNull(action);
+
+      assertEquals(server.getRequestCount(), 1);
+      assertSent(server, "GET", "/actions/1");
+   }
+   
+   private Action actionFromResource(String resource) {
+      return onlyObjectFromResource(resource, new TypeToken<Map<String, Action>>() {
+         private static final long serialVersionUID = 1L;
+      }); 
+   }
+}

http://git-wip-us.apache.org/repos/asf/jclouds-labs/blob/a141b21b/digitalocean2/src/test/java/org/jclouds/digitalocean2/features/DropletApiLiveTest.java
----------------------------------------------------------------------
diff --git a/digitalocean2/src/test/java/org/jclouds/digitalocean2/features/DropletApiLiveTest.java b/digitalocean2/src/test/java/org/jclouds/digitalocean2/features/DropletApiLiveTest.java
new file mode 100644
index 0000000..f451d2e
--- /dev/null
+++ b/digitalocean2/src/test/java/org/jclouds/digitalocean2/features/DropletApiLiveTest.java
@@ -0,0 +1,195 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.jclouds.digitalocean2.features;
+
+import static com.google.common.collect.Iterables.getOnlyElement;
+import static java.util.logging.Logger.getAnonymousLogger;
+import static org.jclouds.digitalocean2.domain.Droplet.Status.ACTIVE;
+import static org.jclouds.digitalocean2.domain.Droplet.Status.OFF;
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertNotNull;
+import static org.testng.Assert.assertNull;
+import static org.testng.Assert.assertTrue;
+
+import java.util.List;
+import java.util.Map;
+
+import org.jclouds.compute.ComputeTestUtils;
+import org.jclouds.digitalocean2.domain.Action;
+import org.jclouds.digitalocean2.domain.Backup;
+import org.jclouds.digitalocean2.domain.Droplet;
+import org.jclouds.digitalocean2.domain.DropletCreate;
+import org.jclouds.digitalocean2.domain.Image;
+import org.jclouds.digitalocean2.domain.Kernel;
+import org.jclouds.digitalocean2.domain.Key;
+import org.jclouds.digitalocean2.domain.Region;
+import org.jclouds.digitalocean2.domain.Size;
+import org.jclouds.digitalocean2.domain.Snapshot;
+import org.jclouds.digitalocean2.domain.options.CreateDropletOptions;
+import org.jclouds.digitalocean2.internal.BaseDigitalOcean2ApiLiveTest;
+import org.testng.annotations.AfterClass;
+import org.testng.annotations.BeforeClass;
+import org.testng.annotations.Test;
+
+import com.google.common.base.Predicate;
+import com.google.common.collect.FluentIterable;
+
+@Test(groups = "live", testName = "DropletApiLiveTest")
+public class DropletApiLiveTest extends BaseDigitalOcean2ApiLiveTest {
+
+   private Region region;
+   private Size size;
+   private Image image;
+   private Key key;
+   private int dropletId = -1;
+   
+   @BeforeClass
+   public void setupDroplet() {
+      region = firstAvailableRegion();
+      size = cheapestSizeInRegion(region);
+      image = ubuntuImageInRegion(region);
+      
+      Map<String, String> keyPair = ComputeTestUtils.setupKeyPair();
+      key = api.keyApi().create(prefix + "-droplet-livetest", keyPair.get("public"));
+   }
+   
+   @AfterClass(alwaysRun = true)
+   public void tearDown() {
+      if (key != null) {
+         api.keyApi().delete(key.id());
+      }
+   }
+   
+   public void testCreate() {
+      DropletCreate dropletCreate = api().create(prefix + "-droplet-livetest", region.slug(), size.slug(), image.slug(),
+            CreateDropletOptions.builder().backupsEnabled(true).addSshKeyId(key.id()).build());
+      assertActionCompleted(getOnlyElement(dropletCreate.links().actions()).id());
+      dropletId = dropletCreate.droplet().id();
+      Droplet droplet = api().get(dropletId);
+      assertNotNull(droplet, "Droplet should not be null");
+   }
+
+   @Test(groups = "live", dependsOnMethods = "testCreate")
+   public void testListDroplets() {
+      assertTrue(api().list().concat().anyMatch(new Predicate<Droplet>() {
+         @Override
+         public boolean apply(Droplet input) {
+            return input.id() == dropletId;
+         }
+      }), "The created droplet must be in the list");
+   }
+
+   @Test(dependsOnMethods = "testCreate")
+   public void testListKernels() {
+      Iterable<Kernel> kernels = api().listKernels(dropletId).concat();
+      assertEquals(kernels.iterator().next().name(), "DO-recovery-static-fsck");
+   }
+   
+   @Test(dependsOnMethods = "testListKernels")
+   public void testPowerOff() {
+      Action action = api().powerOff(dropletId);
+      assertActionCompleted(action.id());
+      Droplet droplet = api().get(dropletId);
+      assertEquals(droplet.status(), OFF, "Droplet should be off");
+   }
+
+   @Test(groups = "live", dependsOnMethods = "testPowerOff")
+   public void testSnapshots() {
+      Action action = api().snapshot(dropletId, prefix + dropletId + "-snapshot");
+      assertActionCompleted(action.id());
+      
+      List<Snapshot> snapshots = api().listSnapshots(dropletId).concat().toList();
+      assertEquals(snapshots.size(), 1, "Must contain 1 snapshot");
+      
+      for (Snapshot snapshot : snapshots) {
+         try {
+            api.imageApi().delete(snapshot.id());
+         } catch (Exception ex) {
+            getAnonymousLogger().warning("Could not delete snapshot: " + snapshot.id());
+         }
+      }
+   }
+
+   @Test(groups = "live", dependsOnMethods = "testSnapshots")
+   public void testBackups() {
+      Iterable<Backup> backups = api().listBackups(dropletId).concat();
+      // Backups are automatically taken by DO on a weekly basis, so we can't guarantee
+      // there will be any backup available. Just check that the call succeeds
+      assertNotNull(backups);
+   }
+
+   @Test(groups = "live", dependsOnMethods = "testSnapshots")
+   public void testListActions() {
+      FluentIterable<Action> actions = api().listActions(dropletId).concat();
+      assertTrue(actions.anyMatch(new Predicate<Action>() {
+         @Override
+         public boolean apply(Action input) {
+            return "snapshot".equals(input.type());
+         }
+      }));
+   }
+
+   @Test(groups = "live", dependsOnMethods = "testSnapshots")
+   public void testPowerOn() {
+      // Apparently droplets are automatically powered on after the snapshot process
+      Action action = api().powerOff(dropletId);
+      assertActionCompleted(action.id());
+      
+      action = api().powerOn(dropletId);
+      assertActionCompleted(action.id());
+      Droplet droplet = api().get(dropletId);
+      assertEquals(droplet.status(), ACTIVE, "Droplet should be Active");
+   }
+   
+   @Test(groups = "live", dependsOnMethods = "testPowerOn")
+   public void testReboot() {
+      Action action = api().reboot(dropletId);
+      assertActionCompleted(action.id());
+      Droplet droplet = api().get(dropletId);
+      assertEquals(droplet.status(), ACTIVE, "Droplet should be off");
+   }
+   
+   @Test(groups = "live", dependsOnMethods = "testReboot")
+   public void testPowerCycle() {
+      Action action = api().powerCycle(dropletId);
+      assertActionCompleted(action.id());
+      Droplet droplet = api().get(dropletId);
+      assertEquals(droplet.status(), ACTIVE, "Droplet should be off");
+   }
+   
+   @Test(groups = "live", dependsOnMethods = "testPowerCycle")
+   public void testShutdown() {
+      Action action = api().shutdown(dropletId);
+      assertActionCompleted(action.id());
+      // The shutdown action can fail if the shutdown command fails in the guest OS
+      // We can not guarantee that a graceful shutdown action will en up in the droplet
+      // being in OFF state
+   }
+
+   @Test(groups = "live", dependsOnMethods = "testShutdown", alwaysRun = true)
+   public void testDelete() throws InterruptedException {
+      if (dropletId != -1) {
+         api().delete(dropletId);
+         assertNodeTerminated(dropletId);
+         assertNull(api().get(dropletId));
+      }
+   }
+   
+   private DropletApi api() {
+      return api.dropletApi();
+   }
+}

http://git-wip-us.apache.org/repos/asf/jclouds-labs/blob/a141b21b/digitalocean2/src/test/java/org/jclouds/digitalocean2/features/DropletApiMockTest.java
----------------------------------------------------------------------
diff --git a/digitalocean2/src/test/java/org/jclouds/digitalocean2/features/DropletApiMockTest.java b/digitalocean2/src/test/java/org/jclouds/digitalocean2/features/DropletApiMockTest.java
new file mode 100644
index 0000000..dcd6352
--- /dev/null
+++ b/digitalocean2/src/test/java/org/jclouds/digitalocean2/features/DropletApiMockTest.java
@@ -0,0 +1,401 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.jclouds.digitalocean2.features;
+
+import static com.google.common.collect.Iterables.isEmpty;
+import static com.google.common.collect.Iterables.size;
+import static org.jclouds.digitalocean2.domain.options.ListOptions.Builder.page;
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertNull;
+import static org.testng.Assert.assertTrue;
+
+import java.util.Map;
+
+import org.jclouds.digitalocean2.domain.Action;
+import org.jclouds.digitalocean2.domain.Backup;
+import org.jclouds.digitalocean2.domain.Droplet;
+import org.jclouds.digitalocean2.domain.DropletCreate;
+import org.jclouds.digitalocean2.domain.Kernel;
+import org.jclouds.digitalocean2.domain.Snapshot;
+import org.jclouds.digitalocean2.domain.options.CreateDropletOptions;
+import org.jclouds.digitalocean2.internal.BaseDigitalOcean2ApiMockTest;
+import org.testng.annotations.Test;
+
+import com.google.common.reflect.TypeToken;
+
+@Test(groups = "unit", testName = "DropletApiMockTest", singleThreaded = true)
+public class DropletApiMockTest extends BaseDigitalOcean2ApiMockTest {
+
+   public void testListDroplets() throws InterruptedException {
+      server.enqueue(jsonResponse("/droplets-first.json"));
+      server.enqueue(jsonResponse("/droplets-last.json"));
+
+      Iterable<Droplet> droplets = api.dropletApi().list().concat();
+
+      assertEquals(size(droplets), 2); // Force the PagedIterable to advance
+      assertEquals(server.getRequestCount(), 2);
+
+      assertSent(server, "GET", "/droplets");
+      assertSent(server, "GET", "/droplets?page=2&per_page=1");
+   }
+
+   public void testListDropletsReturns404() throws InterruptedException {
+      server.enqueue(response404());
+
+      Iterable<Droplet> droplets = api.dropletApi().list().concat();
+
+      assertTrue(isEmpty(droplets));
+
+      assertEquals(server.getRequestCount(), 1);
+      assertSent(server, "GET", "/droplets");
+   }
+
+   public void testListDropletsWithOptions() throws InterruptedException {
+      server.enqueue(jsonResponse("/droplets-first.json"));
+
+      Iterable<Droplet> droplets = api.dropletApi().list(page(1).perPage(20));
+
+      assertEquals(size(droplets), 1);
+      assertEquals(server.getRequestCount(), 1);
+
+      assertSent(server, "GET", "/droplets?page=1&per_page=20");
+   }
+
+   public void testListDropletsWithOptionsReturns404() throws InterruptedException {
+      server.enqueue(response404());
+
+      Iterable<Droplet> droplets = api.dropletApi().list(page(1).perPage(20));
+
+      assertTrue(isEmpty(droplets));
+
+      assertEquals(server.getRequestCount(), 1);
+      assertSent(server, "GET", "/droplets?page=1&per_page=20");
+   }
+   
+   public void testGetDroplet() throws InterruptedException {
+      server.enqueue(jsonResponse("/droplet.json"));
+
+      Droplet droplet = api.dropletApi().get(1);
+
+      assertEquals(droplet, dropletFromResource("/droplet.json"));
+      
+      assertEquals(server.getRequestCount(), 1);
+      assertSent(server, "GET", "/droplets/1");
+   }
+
+   public void testGetDropletReturns404() throws InterruptedException {
+      server.enqueue(response404());
+
+      Droplet droplet = api.dropletApi().get(1);
+
+      assertNull(droplet);
+
+      assertEquals(server.getRequestCount(), 1);
+      assertSent(server, "GET", "/droplets/1");
+   }
+   
+   public void testCreateDroplet() throws InterruptedException {
+      server.enqueue(jsonResponse("/droplet-create-res.json"));
+
+      DropletCreate droplet = api.dropletApi().create("digitalocean2-s-d5e", "sfo1", "512mb", "6374124", CreateDropletOptions.builder().addSshKeyId(421192).build());
+
+      assertEquals(droplet, objectFromResource("/droplet-create-res.json", DropletCreate.class));
+      
+      assertEquals(server.getRequestCount(), 1);
+      assertSent(server, "POST", "/droplets", stringFromResource("/droplet-create-req.json"));
+   }
+   
+   public void testListKernels() throws InterruptedException {
+      server.enqueue(jsonResponse("/kernels-first.json"));
+      server.enqueue(jsonResponse("/kernels-last.json"));
+
+      Iterable<Kernel> kernels = api.dropletApi().listKernels(5425561).concat();
+
+      assertEquals(size(kernels), 10); // Force the PagedIterable to advance
+      assertEquals(server.getRequestCount(), 2);
+
+      assertSent(server, "GET", "/droplets/5425561/kernels");
+      assertSent(server, "GET", "/droplets/5425561/kernels?page=2");
+   }
+
+   public void testListKernelsReturns404() throws InterruptedException {
+      server.enqueue(response404());
+
+      Iterable<Kernel> kernels = api.dropletApi().listKernels(5425561).concat();
+
+      assertTrue(isEmpty(kernels));
+
+      assertEquals(server.getRequestCount(), 1);
+      assertSent(server, "GET", "/droplets/5425561/kernels");
+   }
+   
+   public void testListKernelsWithOptions() throws InterruptedException {
+      server.enqueue(jsonResponse("/kernels-first.json"));
+
+      Iterable<Kernel> kernels = api.dropletApi().listKernels(5425561, page(1).perPage(20));
+
+      assertEquals(size(kernels), 5);
+      assertEquals(server.getRequestCount(), 1);
+
+      assertSent(server, "GET", "/droplets/5425561/kernels?page=1&per_page=20");
+   }
+
+   public void testListKernelsWithOptionsReturns404() throws InterruptedException {
+      server.enqueue(response404());
+
+      Iterable<Kernel> kernels = api.dropletApi().listKernels(5425561, page(1).perPage(20));
+
+      assertTrue(isEmpty(kernels));
+
+      assertEquals(server.getRequestCount(), 1);
+      assertSent(server, "GET", "/droplets/5425561/kernels?page=1&per_page=20");
+   }
+   
+   public void testListActions() throws InterruptedException {
+      server.enqueue(jsonResponse("/actions-first.json"));
+      server.enqueue(jsonResponse("/actions-last.json"));
+
+      Iterable<Action> actions = api.dropletApi().listActions(1).concat();
+
+      assertEquals(size(actions), 8); // Force the PagedIterable to advance
+      assertEquals(server.getRequestCount(), 2);
+
+      assertSent(server, "GET", "/droplets/1/actions");
+      assertSent(server, "GET", "/droplets/1/actions?page=2&per_page=5");
+   }
+
+   public void testListActionsReturns404() throws InterruptedException {
+      server.enqueue(response404());
+
+      Iterable<Action> actions = api.dropletApi().listActions(1).concat();
+
+      assertTrue(isEmpty(actions));
+
+      assertEquals(server.getRequestCount(), 1);
+      assertSent(server, "GET", "/droplets/1/actions");
+   }
+
+   public void testListActionsWithOptions() throws InterruptedException {
+      server.enqueue(jsonResponse("/actions-first.json"));
+
+      Iterable<Action> actions = api.dropletApi().listActions(1, page(1).perPage(5));
+
+      assertEquals(size(actions), 5);
+      assertEquals(server.getRequestCount(), 1);
+
+      assertSent(server, "GET", "/droplets/1/actions?page=1&per_page=5");
+   }
+
+   public void testListActionsWithOptionsReturns404() throws InterruptedException {
+      server.enqueue(response404());
+
+      Iterable<Action> actions = api.dropletApi().listActions(1, page(1).perPage(5));
+
+      assertTrue(isEmpty(actions));
+
+      assertEquals(server.getRequestCount(), 1);
+      assertSent(server, "GET", "/droplets/1/actions?page=1&per_page=5");
+   }
+   
+   public void testListBackups() throws InterruptedException {
+      server.enqueue(jsonResponse("/backups-first.json"));
+      server.enqueue(jsonResponse("/backups-last.json"));
+
+      Iterable<Backup> backups = api.dropletApi().listBackups(5425561).concat();
+
+      assertEquals(size(backups), 2); // Force the PagedIterable to advance
+      assertEquals(server.getRequestCount(), 2);
+
+      assertSent(server, "GET", "/droplets/5425561/backups");
+      assertSent(server, "GET", "/droplets/5425561/backups?page=2");
+   }
+
+   public void testListBackupsReturns404() throws InterruptedException {
+      server.enqueue(response404());
+
+      Iterable<Backup> backups = api.dropletApi().listBackups(5425561).concat();
+
+      assertTrue(isEmpty(backups));
+
+      assertEquals(server.getRequestCount(), 1);
+      assertSent(server, "GET", "/droplets/5425561/backups");
+   }
+   
+   public void testListBackupsWithOptions() throws InterruptedException {
+      server.enqueue(jsonResponse("/backups-first.json"));
+
+      Iterable<Backup> backups = api.dropletApi().listBackups(5425561, page(1).perPage(20));
+
+      assertEquals(size(backups), 1);
+      assertEquals(server.getRequestCount(), 1);
+
+      assertSent(server, "GET", "/droplets/5425561/backups?page=1&per_page=20");
+   }
+
+   public void testListBackupsWithOptionsReturns404() throws InterruptedException {
+      server.enqueue(response404());
+
+      Iterable<Backup> backups = api.dropletApi().listBackups(5425561, page(1).perPage(20));
+
+      assertTrue(isEmpty(backups));
+
+      assertEquals(server.getRequestCount(), 1);
+      assertSent(server, "GET", "/droplets/5425561/backups?page=1&per_page=20");
+   }
+   
+   public void testListSnapshots() throws InterruptedException {
+      server.enqueue(jsonResponse("/snapshots-first.json"));
+      server.enqueue(jsonResponse("/snapshots-last.json"));
+
+      Iterable<Snapshot> snapshots = api.dropletApi().listSnapshots(5425561).concat();
+
+      assertEquals(size(snapshots), 2); // Force the PagedIterable to advance
+      assertEquals(server.getRequestCount(), 2);
+
+      assertSent(server, "GET", "/droplets/5425561/snapshots");
+      assertSent(server, "GET", "/droplets/5425561/snapshots?page=2");
+   }
+
+   public void testListSnapshotsReturns404() throws InterruptedException {
+      server.enqueue(response404());
+
+      Iterable<Snapshot> snapshots = api.dropletApi().listSnapshots(5425561).concat();
+
+      assertTrue(isEmpty(snapshots));
+
+      assertEquals(server.getRequestCount(), 1);
+      assertSent(server, "GET", "/droplets/5425561/snapshots");
+   }
+   
+   public void testListSnapshotsWithOptions() throws InterruptedException {
+      server.enqueue(jsonResponse("/snapshots-first.json"));
+
+      Iterable<Snapshot> snapshots = api.dropletApi().listSnapshots(5425561, page(1).perPage(20));
+
+      assertEquals(size(snapshots), 1);
+      assertEquals(server.getRequestCount(), 1);
+
+      assertSent(server, "GET", "/droplets/5425561/snapshots?page=1&per_page=20");
+   }
+
+   public void testListSnapshotsWithOptionsReturns404() throws InterruptedException {
+      server.enqueue(response404());
+
+      Iterable<Snapshot> snapshots = api.dropletApi().listSnapshots(5425561, page(1).perPage(20));
+
+      assertTrue(isEmpty(snapshots));
+
+      assertEquals(server.getRequestCount(), 1);
+      assertSent(server, "GET", "/droplets/5425561/snapshots?page=1&per_page=20");
+   }
+   
+   public void testDeleteDroplet() throws InterruptedException {
+      server.enqueue(response204());
+
+      api.dropletApi().delete(1);
+      
+      assertEquals(server.getRequestCount(), 1);
+      assertSent(server, "DELETE", "/droplets/1");
+   }
+
+   public void testDeleteDropletReturns404() throws InterruptedException {
+      server.enqueue(response404());
+
+      api.dropletApi().delete(1);
+
+      assertEquals(server.getRequestCount(), 1);
+      assertSent(server, "DELETE", "/droplets/1");
+   }
+   
+   public void testPowerCycleDroplet() throws InterruptedException {
+      server.enqueue(jsonResponse("/power-cycle.json"));
+
+      Action action = api.dropletApi().powerCycle(1);
+      
+      assertEquals(action, actionFromResource("/power-cycle.json"));
+      
+      assertEquals(server.getRequestCount(), 1);
+      assertSent(server, "POST", "/droplets/1/actions", "{\"type\":\"power_cycle\"}");
+   }
+   
+   public void testPowerOn() throws InterruptedException {
+      server.enqueue(jsonResponse("/power-on.json"));
+
+      Action action = api.dropletApi().powerOn(1);
+      
+      assertEquals(action, actionFromResource("/power-on.json"));
+      
+      assertEquals(server.getRequestCount(), 1);
+      assertSent(server, "POST", "/droplets/1/actions", "{\"type\":\"power_on\"}");
+   }
+   
+   public void testPowerOff() throws InterruptedException {
+      server.enqueue(jsonResponse("/power-off.json"));
+
+      Action action = api.dropletApi().powerOff(1);
+      
+      assertEquals(action, actionFromResource("/power-off.json"));
+      
+      assertEquals(server.getRequestCount(), 1);
+      assertSent(server, "POST", "/droplets/1/actions", "{\"type\":\"power_off\"}");
+   }
+   
+   public void testReboot() throws InterruptedException {
+      server.enqueue(jsonResponse("/reboot.json"));
+
+      Action action = api.dropletApi().reboot(1);
+      
+      assertEquals(action, actionFromResource("/reboot.json"));
+      
+      assertEquals(server.getRequestCount(), 1);
+      assertSent(server, "POST", "/droplets/1/actions", "{\"type\":\"reboot\"}");
+   }
+   
+   public void testShutdown() throws InterruptedException {
+      server.enqueue(jsonResponse("/shutdown.json"));
+
+      Action action = api.dropletApi().shutdown(1);
+      
+      assertEquals(action, actionFromResource("/shutdown.json"));
+      
+      assertEquals(server.getRequestCount(), 1);
+      assertSent(server, "POST", "/droplets/1/actions", "{\"type\":\"shutdown\"}");
+   }
+   
+   public void testSnapshot() throws InterruptedException {
+      server.enqueue(jsonResponse("/snapshot.json"));
+
+      Action action = api.dropletApi().snapshot(1, "foo");
+      
+      assertEquals(action, actionFromResource("/snapshot.json"));
+      
+      assertEquals(server.getRequestCount(), 1);
+      assertSent(server, "POST", "/droplets/1/actions", "{\"type\":\"snapshot\",\"name\":\"foo\"}");
+   }
+   
+   private Droplet dropletFromResource(String resource) {
+      return onlyObjectFromResource(resource, new TypeToken<Map<String, Droplet>>() {
+         private static final long serialVersionUID = 1L;
+      }); 
+   }
+   
+   private Action actionFromResource(String resource) {
+      return onlyObjectFromResource(resource, new TypeToken<Map<String, Action>>() {
+         private static final long serialVersionUID = 1L;
+      }); 
+   }
+}

http://git-wip-us.apache.org/repos/asf/jclouds-labs/blob/a141b21b/digitalocean2/src/test/java/org/jclouds/digitalocean2/features/ImageApiLiveTest.java
----------------------------------------------------------------------
diff --git a/digitalocean2/src/test/java/org/jclouds/digitalocean2/features/ImageApiLiveTest.java b/digitalocean2/src/test/java/org/jclouds/digitalocean2/features/ImageApiLiveTest.java
new file mode 100644
index 0000000..8c4c96e
--- /dev/null
+++ b/digitalocean2/src/test/java/org/jclouds/digitalocean2/features/ImageApiLiveTest.java
@@ -0,0 +1,97 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.jclouds.digitalocean2.features;
+
+import static org.jclouds.digitalocean2.domain.options.ImageListOptions.Builder.page;
+import static org.jclouds.digitalocean2.domain.options.ImageListOptions.Builder.type;
+import static org.testng.Assert.assertNotNull;
+import static org.testng.Assert.assertTrue;
+import static org.testng.util.Strings.isNullOrEmpty;
+
+import java.util.concurrent.atomic.AtomicInteger;
+
+import org.jclouds.digitalocean2.domain.Image;
+import org.jclouds.digitalocean2.internal.BaseDigitalOcean2ApiLiveTest;
+import org.testng.annotations.Test;
+
+import com.google.common.base.Optional;
+import com.google.common.base.Predicate;
+
+@Test(groups = "live", testName = "ImageApiLiveTest")
+public class ImageApiLiveTest extends BaseDigitalOcean2ApiLiveTest {
+
+   public void testListImages() {
+      final AtomicInteger found = new AtomicInteger(0);
+      // DigitalOcean return 25 records per page by default. Inspect at most 2 pages
+      assertTrue(api().list().concat().limit(50).allMatch(new Predicate<Image>() {
+         @Override
+         public boolean apply(Image input) {
+            found.incrementAndGet();
+            return !isNullOrEmpty(input.name());
+         }
+      }), "All images must have the 'name' field populated");
+      assertTrue(found.get() > 0, "Expected some images to be returned");
+   }
+   
+   public void testListImagesOnePage() {
+      final AtomicInteger found = new AtomicInteger(0);
+      assertTrue(api().list(page(1).perPage(5)).allMatch(new Predicate<Image>() {
+         @Override
+         public boolean apply(Image input) {
+            found.incrementAndGet();
+            return !isNullOrEmpty(input.name());
+         }
+      }), "All images must have the 'name' field populated");
+      assertTrue(found.get() > 0, "Expected some images to be returned");
+   }
+   
+   public void testListImagesByType() {
+      final AtomicInteger found = new AtomicInteger(0);
+      assertTrue(api().list(type("distribution").perPage(5)).allMatch(new Predicate<Image>() {
+         @Override
+         public boolean apply(Image input) {
+            found.incrementAndGet();
+            return !isNullOrEmpty(input.distribution());
+         }
+      }), "All images must have the 'distribution' field populated");
+      assertTrue(found.get() > 0, "Expected some images to be returned");
+   }
+   
+   public void testGetImage() {
+      Optional<Image> first = api().list().concat().first();
+      assertTrue(first.isPresent(), "At least one image was expected to exist");
+      assertNotNull(api().get(first.get().id()));
+   }
+   
+   public void testGetImageBySlug() {
+      Optional<Image> first = api().list().concat().firstMatch(new Predicate<Image>() {
+         @Override
+         public boolean apply(Image input) {
+            return !isNullOrEmpty(input.slug());
+         }
+      });
+      
+      assertTrue(first.isPresent(), "At least one image with the 'slug' field set was expected to exist");
+      assertNotNull(api().get(first.get().slug()));
+   }
+   
+   // TODO: Delete live test once the create/transfer operations are implemented
+   
+   private ImageApi api() {
+      return api.imageApi();
+   }
+}

http://git-wip-us.apache.org/repos/asf/jclouds-labs/blob/a141b21b/digitalocean2/src/test/java/org/jclouds/digitalocean2/features/ImageApiMockTest.java
----------------------------------------------------------------------
diff --git a/digitalocean2/src/test/java/org/jclouds/digitalocean2/features/ImageApiMockTest.java b/digitalocean2/src/test/java/org/jclouds/digitalocean2/features/ImageApiMockTest.java
new file mode 100644
index 0000000..d172174
--- /dev/null
+++ b/digitalocean2/src/test/java/org/jclouds/digitalocean2/features/ImageApiMockTest.java
@@ -0,0 +1,150 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.jclouds.digitalocean2.features;
+
+import static com.google.common.collect.Iterables.isEmpty;
+import static com.google.common.collect.Iterables.size;
+import static org.jclouds.digitalocean2.domain.options.ImageListOptions.Builder.page;
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertNull;
+import static org.testng.Assert.assertTrue;
+
+import java.util.Map;
+
+import org.jclouds.digitalocean2.domain.Image;
+import org.jclouds.digitalocean2.internal.BaseDigitalOcean2ApiMockTest;
+import org.testng.annotations.Test;
+
+import com.google.common.reflect.TypeToken;
+
+@Test(groups = "unit", testName = "ImageApiMockTest", singleThreaded = true)
+public class ImageApiMockTest extends BaseDigitalOcean2ApiMockTest {
+
+   public void testListImages() throws InterruptedException {
+      server.enqueue(jsonResponse("/images-first.json"));
+      server.enqueue(jsonResponse("/images-last.json"));
+
+      Iterable<Image> images = api.imageApi().list().concat();
+
+      assertEquals(size(images), 10); // Force the PagedIterable to advance
+      assertEquals(server.getRequestCount(), 2);
+
+      assertSent(server, "GET", "/images");
+      assertSent(server, "GET", "/images?page=2&per_page=5&type=distribution");
+   }
+
+   public void testListImagesReturns404() throws InterruptedException {
+      server.enqueue(response404());
+
+      Iterable<Image> images = api.imageApi().list().concat();
+
+      assertTrue(isEmpty(images));
+
+      assertEquals(server.getRequestCount(), 1);
+      assertSent(server, "GET", "/images");
+   }
+
+   public void testListImagesWithOptions() throws InterruptedException {
+      server.enqueue(jsonResponse("/images-first.json"));
+
+      Iterable<Image> images = api.imageApi().list(page(1).perPage(5).type("distribution"));
+
+      assertEquals(size(images), 5);
+      assertEquals(server.getRequestCount(), 1);
+
+      assertSent(server, "GET", "/images?page=1&per_page=5&type=distribution");
+   }
+
+   public void testListImagesWithOptionsReturns404() throws InterruptedException {
+      server.enqueue(response404());
+
+      Iterable<Image> images = api.imageApi().list(page(1).perPage(5).type("distribution"));
+
+      assertTrue(isEmpty(images));
+
+      assertEquals(server.getRequestCount(), 1);
+      assertSent(server, "GET", "/images?page=1&per_page=5&type=distribution");
+   }
+   
+   public void testGetImage() throws InterruptedException {
+      server.enqueue(jsonResponse("/image.json"));
+
+      Image image = api.imageApi().get(1);
+
+      assertEquals(image, imageFromResource("/image.json"));
+      
+      assertEquals(server.getRequestCount(), 1);
+      assertSent(server, "GET", "/images/1");
+   }
+
+   public void testGetImageReturns404() throws InterruptedException {
+      server.enqueue(response404());
+
+      Image image = api.imageApi().get(1);
+
+      assertNull(image);
+
+      assertEquals(server.getRequestCount(), 1);
+      assertSent(server, "GET", "/images/1");
+   }
+   
+   public void testGetImageUsingSlug() throws InterruptedException {
+      server.enqueue(jsonResponse("/image.json"));
+
+      Image image = api.imageApi().get("foo");
+
+      assertEquals(image, imageFromResource("/image.json"));
+      
+      assertEquals(server.getRequestCount(), 1);
+      assertSent(server, "GET", "/images/foo");
+   }
+
+   public void testGetImageUsingSlugReturns404() throws InterruptedException {
+      server.enqueue(response404());
+
+      Image image = api.imageApi().get("foo");
+
+      assertNull(image);
+
+      assertEquals(server.getRequestCount(), 1);
+      assertSent(server, "GET", "/images/foo");
+   }
+   
+   public void testDeleteImage() throws InterruptedException {
+      server.enqueue(response204());
+
+      api.imageApi().delete(1);
+      
+      assertEquals(server.getRequestCount(), 1);
+      assertSent(server, "DELETE", "/images/1");
+   }
+
+   public void testDeleteImageReturns404() throws InterruptedException {
+      server.enqueue(response404());
+
+      api.imageApi().delete(1);
+
+      assertEquals(server.getRequestCount(), 1);
+      assertSent(server, "DELETE", "/images/1");
+   }
+   
+   private Image imageFromResource(String resource) {
+      return onlyObjectFromResource(resource, new TypeToken<Map<String, Image>>() {
+         private static final long serialVersionUID = 1L;
+      }); 
+   }
+}

http://git-wip-us.apache.org/repos/asf/jclouds-labs/blob/a141b21b/digitalocean2/src/test/java/org/jclouds/digitalocean2/features/KeyApiLiveTest.java
----------------------------------------------------------------------
diff --git a/digitalocean2/src/test/java/org/jclouds/digitalocean2/features/KeyApiLiveTest.java b/digitalocean2/src/test/java/org/jclouds/digitalocean2/features/KeyApiLiveTest.java
new file mode 100644
index 0000000..2911d79
--- /dev/null
+++ b/digitalocean2/src/test/java/org/jclouds/digitalocean2/features/KeyApiLiveTest.java
@@ -0,0 +1,101 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.jclouds.digitalocean2.features;
+
+import static org.jclouds.digitalocean2.domain.options.ListOptions.Builder.page;
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertFalse;
+import static org.testng.Assert.assertTrue;
+
+import java.io.IOException;
+
+import org.jclouds.digitalocean2.domain.Key;
+import org.jclouds.digitalocean2.internal.BaseDigitalOcean2ApiLiveTest;
+import org.testng.annotations.AfterClass;
+import org.testng.annotations.Test;
+
+import com.google.common.base.Charsets;
+import com.google.common.base.Throwables;
+import com.google.common.collect.FluentIterable;
+import com.google.common.io.Resources;
+
+@Test(groups = "live", testName = "KeyApiLiveTest")
+public class KeyApiLiveTest extends BaseDigitalOcean2ApiLiveTest {
+
+   private Key dsa;
+   private Key ecdsa;
+
+   public void testCreateKey() {
+      dsa = api().create("jclouds-test-dsa", loadKey("/ssh-dsa.pub"));
+      ecdsa = api().create("jclouds-test-ecdsa", loadKey("/ssh-ecdsa.pub"));
+      
+      assertEquals(dsa.name(), "jclouds-test-dsa");
+      assertEquals(ecdsa.name(), "jclouds-test-ecdsa");
+   }
+   
+   @Test(dependsOnMethods = "testCreateKey")
+   public void testListKeys() {
+      FluentIterable<Key> keys = api().list().concat();
+      assertTrue(keys.size() >= 2, "At least the two created keys must exist");
+   }
+   
+   @Test(dependsOnMethods = "testCreateKey")
+   public void testListKeysOnePAge() {
+      FluentIterable<Key> keys = api().list(page(1));
+      assertTrue(keys.size() >= 2, "At least the two created keys must exist");
+   }
+
+   @Test(dependsOnMethods = "testCreateKey")
+   public void testGetKey() {
+      assertEquals(api().get(dsa.id()).fingerprint(), dsa.fingerprint());
+      assertEquals(api().get(ecdsa.fingerprint()).id(), ecdsa.id());
+   }
+   
+   @Test(dependsOnMethods = "testCreateKey")
+   public void testUpdateKey() {
+      api().update(dsa.id(), "jclouds-test-dsa-updated");
+      assertEquals(api().get(dsa.id()).name(), "jclouds-test-dsa-updated");
+      api().update(dsa.fingerprint(), "jclouds-test-dsa-updated2");
+      assertEquals(api().get(dsa.id()).name(), "jclouds-test-dsa-updated2");
+   }
+
+   @AfterClass(alwaysRun = true)
+   public void testDeleteKey() {
+      if (dsa != null) {
+         api().delete(dsa.id());
+         FluentIterable<Key> keys = api().list().concat();
+         assertFalse(keys.contains(dsa), "dsa key must not be present in list");
+      }
+      if (ecdsa != null) {
+         api().delete(ecdsa.fingerprint());
+         FluentIterable<Key>  keys = api().list().concat();
+         assertFalse(keys.contains(ecdsa), "dsa key must not be present in list");
+      }
+   }
+   
+   private String loadKey(String resourceName) {
+      try {
+         return Resources.toString(getClass().getResource(resourceName), Charsets.UTF_8);
+      } catch (IOException e) {
+         throw Throwables.propagate(e);
+      }
+   }
+
+   private KeyApi api() {
+      return api.keyApi();
+   }
+}

http://git-wip-us.apache.org/repos/asf/jclouds-labs/blob/a141b21b/digitalocean2/src/test/java/org/jclouds/digitalocean2/features/KeyApiMockTest.java
----------------------------------------------------------------------
diff --git a/digitalocean2/src/test/java/org/jclouds/digitalocean2/features/KeyApiMockTest.java b/digitalocean2/src/test/java/org/jclouds/digitalocean2/features/KeyApiMockTest.java
new file mode 100644
index 0000000..2f9d5d3
--- /dev/null
+++ b/digitalocean2/src/test/java/org/jclouds/digitalocean2/features/KeyApiMockTest.java
@@ -0,0 +1,203 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.jclouds.digitalocean2.features;
+
+import static com.google.common.collect.Iterables.isEmpty;
+import static com.google.common.collect.Iterables.size;
+import static org.jclouds.digitalocean2.domain.options.ListOptions.Builder.page;
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertNull;
+import static org.testng.Assert.assertTrue;
+
+import java.util.Map;
+
+import org.jclouds.digitalocean2.domain.Key;
+import org.jclouds.digitalocean2.internal.BaseDigitalOcean2ApiMockTest;
+import org.testng.annotations.Test;
+
+import com.google.common.reflect.TypeToken;
+
+@Test(groups = "unit", testName = "KeyApiMockTest", singleThreaded = true)
+public class KeyApiMockTest extends BaseDigitalOcean2ApiMockTest {
+
+   public void testListKeys() throws InterruptedException {
+      server.enqueue(jsonResponse("/keys-first.json"));
+      server.enqueue(jsonResponse("/keys-last.json"));
+
+      Iterable<Key> keys = api.keyApi().list().concat();
+
+      assertEquals(size(keys), 7); // Force the PagedIterable to advance
+      assertEquals(server.getRequestCount(), 2);
+
+      assertSent(server, "GET", "/account/keys");
+      assertSent(server, "GET", "/account/keys?page=2&per_page=5");
+   }
+
+   public void testListKeysReturns404() throws InterruptedException {
+      server.enqueue(response404());
+
+      Iterable<Key> keys = api.keyApi().list().concat();
+
+      assertTrue(isEmpty(keys));
+
+      assertEquals(server.getRequestCount(), 1);
+      assertSent(server, "GET", "/account/keys");
+   }
+
+   public void testListKeysWithOptions() throws InterruptedException {
+      server.enqueue(jsonResponse("/keys-first.json"));
+
+      Iterable<Key> keys = api.keyApi().list(page(1).perPage(5));
+
+      assertEquals(size(keys), 5);
+      assertEquals(server.getRequestCount(), 1);
+
+      assertSent(server, "GET", "/account/keys?page=1&per_page=5");
+   }
+
+   public void testListKeysWithOptionsReturns404() throws InterruptedException {
+      server.enqueue(response404());
+
+      Iterable<Key> keys = api.keyApi().list(page(1).perPage(5));
+
+      assertTrue(isEmpty(keys));
+
+      assertEquals(server.getRequestCount(), 1);
+      assertSent(server, "GET", "/account/keys?page=1&per_page=5");
+   }
+   
+   public void testCreateKey() throws InterruptedException {
+      server.enqueue(jsonResponse("/key.json").setStatus("HTTP/1.1 201 Created"));
+      
+      String dsa = stringFromResource("/ssh-dsa.pub");
+      
+      Key key = api.keyApi().create("foo", dsa);
+      
+      assertEquals(key, keyFromResource("/key.json"));
+      
+      assertEquals(server.getRequestCount(), 1);
+      assertSent(server, "POST", "/account/keys", String.format("{\"name\":\"foo\", \"public_key\":\"%s\"}", dsa));
+   }
+   
+   public void testGetKey() throws InterruptedException {
+      server.enqueue(jsonResponse("/key.json"));
+
+      Key key = api.keyApi().get(1);
+
+      assertEquals(key, keyFromResource("/key.json"));
+      
+      assertEquals(server.getRequestCount(), 1);
+      assertSent(server, "GET", "/account/keys/1");
+   }
+
+   public void testGetKeyReturns404() throws InterruptedException {
+      server.enqueue(response404());
+
+      Key key = api.keyApi().get(1);
+
+      assertNull(key);
+
+      assertEquals(server.getRequestCount(), 1);
+      assertSent(server, "GET", "/account/keys/1");
+   }
+   
+   public void testGetKeyUsingFingerprint() throws InterruptedException {
+      server.enqueue(jsonResponse("/key.json"));
+
+      Key key = api.keyApi().get("1a:cc:9b:88:c8:4f:b8:77:96:15:d2:0c:95:86:ff:90");
+
+      assertEquals(key, keyFromResource("/key.json"));
+      
+      assertEquals(server.getRequestCount(), 1);
+      assertSent(server, "GET", "/account/keys/1a:cc:9b:88:c8:4f:b8:77:96:15:d2:0c:95:86:ff:90");
+   }
+
+   public void testGetKeyUsingFingerprintReturns404() throws InterruptedException {
+      server.enqueue(response404());
+
+      Key key = api.keyApi().get("1a:cc:9b:88:c8:4f:b8:77:96:15:d2:0c:95:86:ff:90");
+
+      assertNull(key);
+
+      assertEquals(server.getRequestCount(), 1);
+      assertSent(server, "GET", "/account/keys/1a:cc:9b:88:c8:4f:b8:77:96:15:d2:0c:95:86:ff:90");
+   }
+   
+   public void testUpdateKey() throws InterruptedException {
+      server.enqueue(jsonResponse("/key.json"));
+
+      Key key = api.keyApi().update(1, "foo");
+
+      assertEquals(key, keyFromResource("/key.json"));
+      
+      assertEquals(server.getRequestCount(), 1);
+      assertSent(server, "PUT", "/account/keys/1", "{\"name\":\"foo\"}");
+   }
+   
+   public void testUpdateKeyUsingFingerprint() throws InterruptedException {
+      server.enqueue(jsonResponse("/key.json"));
+
+      Key key = api.keyApi().update("1a:cc:9b:88:c8:4f:b8:77:96:15:d2:0c:95:86:ff:90", "foo");
+
+      assertEquals(key, keyFromResource("/key.json"));
+      
+      assertEquals(server.getRequestCount(), 1);
+      assertSent(server, "PUT", "/account/keys/1a:cc:9b:88:c8:4f:b8:77:96:15:d2:0c:95:86:ff:90", "{\"name\":\"foo\"}");
+   }
+   
+   public void testDeleteKey() throws InterruptedException {
+      server.enqueue(response204());
+
+      api.keyApi().delete(1);
+      
+      assertEquals(server.getRequestCount(), 1);
+      assertSent(server, "DELETE", "/account/keys/1");
+   }
+
+   public void testDeleteKeyReturns404() throws InterruptedException {
+      server.enqueue(response404());
+
+      api.keyApi().delete(1);
+
+      assertEquals(server.getRequestCount(), 1);
+      assertSent(server, "DELETE", "/account/keys/1");
+   }
+   
+   public void testDeleteKeyUsingFingerprint() throws InterruptedException {
+      server.enqueue(response204());
+
+      api.keyApi().delete("1a:cc:9b:88:c8:4f:b8:77:96:15:d2:0c:95:86:ff:90");
+      
+      assertEquals(server.getRequestCount(), 1);
+      assertSent(server, "DELETE", "/account/keys/1a:cc:9b:88:c8:4f:b8:77:96:15:d2:0c:95:86:ff:90");
+   }
+
+   public void testDeleteKeyUsingfingerprintReturns404() throws InterruptedException {
+      server.enqueue(response404());
+
+      api.keyApi().delete("1a:cc:9b:88:c8:4f:b8:77:96:15:d2:0c:95:86:ff:90");
+
+      assertEquals(server.getRequestCount(), 1);
+      assertSent(server, "DELETE", "/account/keys/1a:cc:9b:88:c8:4f:b8:77:96:15:d2:0c:95:86:ff:90");
+   }
+   
+   private Key keyFromResource(String resource) {
+      return onlyObjectFromResource(resource, new TypeToken<Map<String, Key>>() {
+         private static final long serialVersionUID = 1L;
+      }); 
+   }
+}

http://git-wip-us.apache.org/repos/asf/jclouds-labs/blob/a141b21b/digitalocean2/src/test/java/org/jclouds/digitalocean2/features/RegionApiLiveTest.java
----------------------------------------------------------------------
diff --git a/digitalocean2/src/test/java/org/jclouds/digitalocean2/features/RegionApiLiveTest.java b/digitalocean2/src/test/java/org/jclouds/digitalocean2/features/RegionApiLiveTest.java
new file mode 100644
index 0000000..73c7451
--- /dev/null
+++ b/digitalocean2/src/test/java/org/jclouds/digitalocean2/features/RegionApiLiveTest.java
@@ -0,0 +1,62 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.jclouds.digitalocean2.features;
+
+import static org.testng.Assert.assertTrue;
+import static org.jclouds.digitalocean2.domain.options.ListOptions.Builder.page;
+
+import java.util.concurrent.atomic.AtomicInteger;
+
+import org.jclouds.digitalocean2.domain.Region;
+import org.jclouds.digitalocean2.internal.BaseDigitalOcean2ApiLiveTest;
+import org.testng.annotations.Test;
+import org.testng.util.Strings;
+
+import com.google.common.base.Predicate;
+
+@Test(groups = "live", testName = "RegionApiLiveTest")
+public class RegionApiLiveTest extends BaseDigitalOcean2ApiLiveTest {
+   
+   public void testListRegions() {
+      final AtomicInteger found = new AtomicInteger(0);
+      // DigitalOcean return 25 records per page by default. Inspect at most 2 pages
+      assertTrue(api().list().concat().limit(50).allMatch(new Predicate<Region>() {
+         @Override
+         public boolean apply(Region input) {
+            found.incrementAndGet();
+            return !Strings.isNullOrEmpty(input.slug());
+         }
+      }), "All regions must have the 'slug' field populated");
+      assertTrue(found.get() > 0, "Expected some regions to be returned");
+   }
+   
+   public void testListRegionsOnePage() {
+      final AtomicInteger found = new AtomicInteger(0);
+      assertTrue(api().list(page(1)).allMatch(new Predicate<Region>() {
+         @Override
+         public boolean apply(Region input) {
+            found.incrementAndGet();
+            return !Strings.isNullOrEmpty(input.slug());
+         }
+      }), "All regions must have the 'slug' field populated");
+      assertTrue(found.get() > 0, "Expected some regions to be returned");
+   }
+   
+   private RegionApi api() {
+      return api.regionApi();
+   }
+}

http://git-wip-us.apache.org/repos/asf/jclouds-labs/blob/a141b21b/digitalocean2/src/test/java/org/jclouds/digitalocean2/features/RegionApiMockTest.java
----------------------------------------------------------------------
diff --git a/digitalocean2/src/test/java/org/jclouds/digitalocean2/features/RegionApiMockTest.java b/digitalocean2/src/test/java/org/jclouds/digitalocean2/features/RegionApiMockTest.java
new file mode 100644
index 0000000..8c8c326
--- /dev/null
+++ b/digitalocean2/src/test/java/org/jclouds/digitalocean2/features/RegionApiMockTest.java
@@ -0,0 +1,77 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.jclouds.digitalocean2.features;
+
+import static com.google.common.collect.Iterables.isEmpty;
+import static com.google.common.collect.Iterables.size;
+import static org.jclouds.digitalocean2.domain.options.ListOptions.Builder.page;
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertTrue;
+
+import org.jclouds.digitalocean2.domain.Region;
+import org.jclouds.digitalocean2.internal.BaseDigitalOcean2ApiMockTest;
+import org.testng.annotations.Test;
+
+@Test(groups = "unit", testName = "RegionApiMockTest", singleThreaded = true)
+public class RegionApiMockTest extends BaseDigitalOcean2ApiMockTest {
+
+   public void testListRegions() throws InterruptedException {
+      server.enqueue(jsonResponse("/regions-first.json"));
+      server.enqueue(jsonResponse("/regions-last.json"));
+
+      Iterable<Region> regions = api.regionApi().list().concat();
+
+      assertEquals(size(regions), 10); // Force the PagedIterable to advance
+      assertEquals(server.getRequestCount(), 2);
+
+      assertSent(server, "GET", "/regions");
+      assertSent(server, "GET", "/regions?page=2&per_page=5");
+   }
+
+   public void testListRegionsReturns404() throws InterruptedException {
+      server.enqueue(response404());
+
+      Iterable<Region> regions = api.regionApi().list().concat();
+
+      assertTrue(isEmpty(regions));
+
+      assertEquals(server.getRequestCount(), 1);
+      assertSent(server, "GET", "/regions");
+   }
+
+   public void testListRegionsWithOptions() throws InterruptedException {
+      server.enqueue(jsonResponse("/regions-first.json"));
+
+      Iterable<Region> regions = api.regionApi().list(page(1).perPage(5));
+
+      assertEquals(size(regions), 5);
+      assertEquals(server.getRequestCount(), 1);
+
+      assertSent(server, "GET", "/regions?page=1&per_page=5");
+   }
+
+   public void testListRegionsWithOptionsReturns404() throws InterruptedException {
+      server.enqueue(response404());
+
+      Iterable<Region> regions = api.regionApi().list(page(1).perPage(5));
+
+      assertTrue(isEmpty(regions));
+
+      assertEquals(server.getRequestCount(), 1);
+      assertSent(server, "GET", "/regions?page=1&per_page=5");
+   }
+}

http://git-wip-us.apache.org/repos/asf/jclouds-labs/blob/a141b21b/digitalocean2/src/test/java/org/jclouds/digitalocean2/features/SizeApiLiveTest.java
----------------------------------------------------------------------
diff --git a/digitalocean2/src/test/java/org/jclouds/digitalocean2/features/SizeApiLiveTest.java b/digitalocean2/src/test/java/org/jclouds/digitalocean2/features/SizeApiLiveTest.java
new file mode 100644
index 0000000..a9a8566
--- /dev/null
+++ b/digitalocean2/src/test/java/org/jclouds/digitalocean2/features/SizeApiLiveTest.java
@@ -0,0 +1,62 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.jclouds.digitalocean2.features;
+
+import static org.jclouds.digitalocean2.domain.options.ListOptions.Builder.page;
+import static org.testng.Assert.assertTrue;
+
+import java.util.concurrent.atomic.AtomicInteger;
+
+import org.jclouds.digitalocean2.domain.Size;
+import org.jclouds.digitalocean2.internal.BaseDigitalOcean2ApiLiveTest;
+import org.testng.annotations.Test;
+import org.testng.util.Strings;
+
+import com.google.common.base.Predicate;
+
+@Test(groups = "live", testName = "SizeApiLiveTest")
+public class SizeApiLiveTest extends BaseDigitalOcean2ApiLiveTest {
+   
+   public void testListSizes() {
+      final AtomicInteger found = new AtomicInteger(0);
+      // DigitalOcean return 25 records per page by default. Inspect at most 2 pages
+      assertTrue(api().list().concat().limit(50).allMatch(new Predicate<Size>() {
+         @Override
+         public boolean apply(Size input) {
+            found.incrementAndGet();
+            return !Strings.isNullOrEmpty(input.slug());
+         }
+      }), "All sizes must have the 'slug' field populated");
+      assertTrue(found.get() > 0, "Expected some sizes to be returned");
+   }
+   
+   public void testListSizesOnePage() {
+      final AtomicInteger found = new AtomicInteger(0);
+      assertTrue(api().list(page(1)).allMatch(new Predicate<Size>() {
+         @Override
+         public boolean apply(Size input) {
+            found.incrementAndGet();
+            return !Strings.isNullOrEmpty(input.slug());
+         }
+      }), "All sizes must have the 'slug' field populated");
+      assertTrue(found.get() > 0, "Expected some sizes to be returned");
+   }
+   
+   private SizeApi api() {
+      return api.sizeApi();
+   }
+}

http://git-wip-us.apache.org/repos/asf/jclouds-labs/blob/a141b21b/digitalocean2/src/test/java/org/jclouds/digitalocean2/features/SizeApiMockTest.java
----------------------------------------------------------------------
diff --git a/digitalocean2/src/test/java/org/jclouds/digitalocean2/features/SizeApiMockTest.java b/digitalocean2/src/test/java/org/jclouds/digitalocean2/features/SizeApiMockTest.java
new file mode 100644
index 0000000..7403518
--- /dev/null
+++ b/digitalocean2/src/test/java/org/jclouds/digitalocean2/features/SizeApiMockTest.java
@@ -0,0 +1,77 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.jclouds.digitalocean2.features;
+
+import static com.google.common.collect.Iterables.isEmpty;
+import static com.google.common.collect.Iterables.size;
+import static org.jclouds.digitalocean2.domain.options.ListOptions.Builder.page;
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertTrue;
+
+import org.jclouds.digitalocean2.domain.Size;
+import org.jclouds.digitalocean2.internal.BaseDigitalOcean2ApiMockTest;
+import org.testng.annotations.Test;
+
+@Test(groups = "unit", testName = "SizeApiMockTest", singleThreaded = true)
+public class SizeApiMockTest extends BaseDigitalOcean2ApiMockTest {
+
+   public void testListSizes() throws InterruptedException {
+      server.enqueue(jsonResponse("/sizes-first.json"));
+      server.enqueue(jsonResponse("/sizes-last.json"));
+
+      Iterable<Size> sizes = api.sizeApi().list().concat();
+
+      assertEquals(size(sizes), 9); // Force the PagedIterable to advance
+      assertEquals(server.getRequestCount(), 2);
+
+      assertSent(server, "GET", "/sizes");
+      assertSent(server, "GET", "/sizes?page=2&per_page=5");
+   }
+
+   public void testListSizesReturns404() throws InterruptedException {
+      server.enqueue(response404());
+
+      Iterable<Size> sizes = api.sizeApi().list().concat();
+
+      assertTrue(isEmpty(sizes));
+
+      assertEquals(server.getRequestCount(), 1);
+      assertSent(server, "GET", "/sizes");
+   }
+
+   public void testListSizesWithOptions() throws InterruptedException {
+      server.enqueue(jsonResponse("/sizes-first.json"));
+
+      Iterable<Size> sizes = api.sizeApi().list(page(1).perPage(5));
+
+      assertEquals(size(sizes), 5);
+      assertEquals(server.getRequestCount(), 1);
+
+      assertSent(server, "GET", "/sizes?page=1&per_page=5");
+   }
+
+   public void testListSizesWithOptionsReturns404() throws InterruptedException {
+      server.enqueue(response404());
+
+      Iterable<Size> sizes = api.sizeApi().list(page(1).perPage(5));
+
+      assertTrue(isEmpty(sizes));
+
+      assertEquals(server.getRequestCount(), 1);
+      assertSent(server, "GET", "/sizes?page=1&per_page=5");
+   }
+}